fabric-Contract-Nodejs
Wei Jieyang Lv4

前言

本部分主要讲解使用nodejs开发fabric链码的过程

fabric链码开发需要注意的8个事项

  1. 启用peer节点的开发模式
  2. 使用Fabric链码的日志
    • Golang:shim ChaincodeLogger
    • NodeJS:shim newLogger
    • Java:可以使用任何标准的日志框架,例如log4j
  3. 避免在Fabric链码中使用全局键
  4. 聪明地使用CouchDB查询(Mongo查询)
  5. 编写确定性的Fabric链码
  6. 调用其他通道的Fabric链码时要小心
    • 目前,跨通道的链码调用不会修改数据,因此, 一个交易一次只能写入一个通道。
  7. 记得设置Fabric链码的执行超时时间
  8. 避免从Fabric链码中访问外部资源

fabric-shim

fabric-contract-api提供了一个高度封装的开发接口,使用该API可以在高层级 编写智能合约的业务逻辑。而fabric-shim则提供了底层的链码开发接口。

  • fabric-shim 要求开发者实现ChaincodeInterface接口,即实现Invoke和Init方法:

下载

1
$ npm install fabric-shim

demo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Chaincode = class {
async Init(stub) {
// use the instantiate input arguments to decide initial chaincode state values

// save the initial states
await stub.putState(key, Buffer.from(aStringValue));

return shim.success(Buffer.from('Initialized Successfully!'));
}

async Invoke(stub) {
// use the invoke input arguments to decide intended changes

// retrieve existing chaincode states
let oldValue = await stub.getState(key);

// calculate new state values and saves them
let newValue = oldValue + delta;
await stub.putState(key, Buffer.from(newValue));

return shim.success(Buffer.from(newValue.toString()));
}
};
shim.start(new Chaincode());

运行链码

1
$ node demo.js

源码分析

ChaincodeInterface

链码必须实现ChaincodeInterface接口中定义的方法。

Init()方法在链码初始化或升级时被调用,以便进行必要的应用状态的初始化。

Invoke()方法当处理交易或查询请求时被调用。两个方法被调用时都会传入一个存根对象(stub),链码可以利用该对象来获取请求的相关信息,例如调用 者身份、目标通道、参数等等,同时也需要利用存根对象与peer节点通信,以便 获取或更新应用状态。

init

invoke

ChaincodeStub

ChaincodeStub封装了链码实现和Fabric的peer节点之间的API。

API文档

构造函数

1
new ChaincodeStub(client, channel_id, txId, chaincodeInput, signedProposal)
  • client:Handler实例
  • channel_id:通道id,string
  • txId:交易id,string
  • chaincodeInput:来自peer节点旳解码消息
  • signedProposal:签名提议

putState()

更新状态库中指定的状态变量键。如果变量已经存在,那么覆盖已有的值。

1
2
3
4
async putState(key, value) {
...
return await this.handler.handlePutState(collection, key, value, this.channel_id, this.txId);
}
  • key:要更新的状态键,字符串
  • value:状态变量的新值,字节数组或字符串
  • 返回:一个Promise对象

createCompositeKey()

通过组合对象类别和给定的属性创建一个组合键。对象类别及属性都必须是有效的utf8字符串,并且不能包含U+0000 (空字节) 和 U+10FFFF (最大未分配代码点)。

  • 组合键可以用作 putState() 调用中的参数键 key

Hyperledger Fabric使用一个简单的key/value模型来保存链码状态。在有些场景下, 可能需要跟踪多个属性,也可能需要使多种属性可搜索。组合键可用来满足这些需求。

类似于关系数据库中的组合键,你可以认为这里的可搜索属性就是组合键的组成列, 属性的值称为键的一部分,因此可以使用像 getStateByRange()getStateByPartialCompositeKey() 这样的方法进行搜索。

1
function createCompositeKey(objectType, attributes){}
  • objectType:组合键的前缀(一般使用name)
  • attributes:拼接到组合键的各属性值,string数组
  • 返回:string类型的组合键

getStateByRange()

该方法返回一个账本状态键的迭代器,可用来遍历在起始键和结束键之间的所有状态键,返回结果按词典顺序排列。

  • 如果起始键和结束键之间的数量大于节点配置文件 core.yaml 中定义的 totalQueryLimit, 那么返回结果数量将受限于 totalQueryLimit 的约定。
  • 调用返回的 StateQueryIterator 迭代器对象的close()方法关闭迭代器。
1
2
3
4
<async> getStateByRange(startKey, endKey){
...
return convertToAsyncIterator(promise);
}
  • startKey:起始键,字符串
  • endKey:结束键,字符串
  • 返回:一个Promise对象,其解析值为StateQueryIterator迭代器。

getStateByPartialCompositeKey()

基于给定的部分复合键查询账本状态。

  • 该方法返回的迭代器可用于遍历查询结果集。

  • 当使用完毕后,调用返回的 StateQueryIterator 迭代器的close()方法关闭迭代器。

1
2
3
4
<async> getStateByPartialCompositeKey(objectType, attributes){
...
return convertToAsyncIterator(promise);
}
  • objectType:结果键前缀,字符串
  • attributes:用于拼接复合键值的属性值列表,字符串数组
  • 返回:一个Promise对象,其解析值为 StateQueryIterator 迭代器对象。

getHistoryForKey()

查看指定状态键的值历史记录。

  • 每次历史更新,都记录有当时的值和关联的交易id、时间戳。
  • 时间戳取自交易提议头。

该方法需要通过peer节点配置中的如下选项开启:

  • core.ledger.history.enableHistoryDatabase = true
    
    1
    2
    3
    4
    5



    ```javascript
    <async> getHistoryForKey(key){}
  • key:状态键

  • 返回一个Promise对象,其解析值为HistoryQueryIterator对象。

getCreator()

返回链码调用的提交者的身份对象

1
2
3
const creator = ctx.stub.getCreator();
console.log(`create type: ${typeof(creator)}`);
console.log(`create: ${JSON.stringify(creator)}`);
  • 返回一个 object 对象,其中包含了两个属性mspididBytes
截屏2021-04-20 下午11.11.33

getChannelID()

返回提案的channelID,以供链码处理。

  • 这将是交易建议的“ channel_id”(请参阅protos / common / common.proto中的ChannelHeader),除非链码在一个通道上调用另一个
1
2
3
const channel = ctx.stub.getChannelID();
console.log(`channel: ${channel}`);
// channel: datachannel

getTxTimestamp()

返回创建事务时的时间戳

  • 这是从事务ChannelHeader中获取的,因此它将指示客户端的时间戳,并且在所有背书人中都具有相同的值。
  • 返回的对象:{ seconds: [Long] { low: [int32], high: [int32], unsigned: [bool] }, nanos: [int32] }
1
2
3
const time = ctx.stub.getTxTimestamp();
console.log(`time type: ${typeof(time)}`);
console.log(`time: ${JSON.stringify(time)}`);
  • 截屏2021-04-20 下午11.18.23

在fabric 2.0 中不再有 getDateTimestamp() 函数,统一使用上面的获取时间

ClientIdentity

ClientIdentity表示有关提交交易的用户的身份信息

  • 链码可以使用此类获取有关提交者的身份信息,包括唯一ID,MSP(会员服务提供者)ID和属性。
  • 此类信息对于通过链码实施访问控制很有用。
1
2
3
4
5
6
7
const ClientIdentity = require('fabric-shim').ClientIdentity;

let cid = new ClientIdentity(stub);
// "stub" is the ChaincodeStub object passed to Init() and Invoke() methods
if (cid.assertAttributeValue('hf.role', 'auditor')) {
// proceed to carry out auditing
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
// 在 contract.js 中
class DataAssetContract extends Contract {
...
try(ctx) {
const cid = ctx.ClientIdentity;
const id = cid.getID();
// x509::/OU=client/CN=user1::/C=US/ST=North Carolina/L=Durham/O=org1.gov.com/CN=ca.org1.gov.com
const idb = cid.getIDBytes();
const msp = cid.getMSPID();
// Org1MSP
}
}
  • getID() 返回与调用身份关联的ID,该ID在MSP中保证是唯一的。
    • A string in the format: "x509::{subject DN}::{issuer DN}"
  • getIDBytes() 返回与调用身份关联的ID字节
    • 如果MSP是使用X.509证书实现的,则这些ID字节将是X.509证书的ID字节。如果希望检查X.509证书的内容,则必须使用X.509解析库(例如jsrsasign或@ fidm / x509)来解码这些ID字节。
  • getMSPID() 返回调用身份的MSPID。
    • ctx.stub.getCreator() 返回中的 ‘mspid’ 相同
截屏2021-04-21 下午2.34.52

fabric-contract-api

1
const { Contract, Context } = require('fabric-contract-api');

context

源码

1
2
3
4
5
6
7
8
9
10
11
12
class Context {
constructor() {
}

setChaincodeStub(stub) {
this.stub = stub;
}

setClientIdentity(clientIdentity) {
this.clientIdentity = clientIdentity;
}
}

使用

1

contract

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Contract {
constructor(name) {
this.__isContract = true;
if (typeof name === 'undefined' || name === null) {
this.name = this.constructor.name;
} else {
this.name = name.trim();
}

logger.info('Creating new Contract', name);
}

static _isContract(obj) {
return obj instanceof Contract || Boolean(obj.__isContract);
}

async beforeTransaction(ctx) {
// default implementation is do nothing
}

async afterTransaction(ctx, result) {
// default implementation is do nothing
}

async unknownTransaction(ctx) {
const {fcn, params} = ctx.stub.getFunctionAndParameters();

logger.error(`[${ctx.stub.getTxID()}] ${this.name} contract-api.Contract unknown transaction`, fcn, params);

throw new Error(`You've asked to invoke a function that does not exist: ${fcn}`);
}

createContext() {
return new Context();
}

getName() {
return this.name;
}
}

使用

1

  • Post title:fabric-Contract-Nodejs
  • Post author:Wei Jieyang
  • Create time:2021-03-31 13:28:34
  • Post link:https://jieyang-wei.github.io/2021/03/31/fabric-Contract-Nodejs/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.