作者:时跃堂 陈曦

智能合约又称链码(Chaincode),是用计算机语言描述合约条款、交易的条件、交易的业务逻辑等,通过调用智能合约实现交易的自动执行和对账本数据的操作。一个BSN应用可以部署多个链码,每个链码包含多个方法。

链码支持多种语言编写,包括Nodejs、golang、java。每个链码程序都必须实现Chaincode接口,链码包含:Init ,Invoke ,Query三个基本操作:

▶Init :链码初始化的方法,在链码实例化或者升级的时候调用一次,以便链码可以执行任何必要的初始化,包括应用程序状态的初始化。

▶Invoke :接收和处理链下业务系统调用事务处理提案,其参数包含调用的链码程序中函数的名称和具体业务处理数据参数。即在Invoke中根据不同的方法参数调用其他分支处理响应的业务。Invoke可以简单的理解为链码方法的入口。

▶Query:提供查询链码数据的方法,该方法只作为查询使用,不提供操作链上数据的操作。可在Query操作时调用,亦可在Invoke方法中作为某些方法的分支被调用。该方法可以不实现。

本文主要介绍用户如何用Nodejs语言开发智能合约,以及在BSN中对智能合约开发的一些规范和建议。

如何开发智能合约

编写链码,关键是实现 Init 与 Invoke 两个方法。

▶Init:在链码实例化或者升级的时候调用一次, 完成初始化数据的工作。建议处理一些简单的处理,禁止使用该方法去初始化大量基础数据,如果有需要初始化的数据,建议在Invoke中处理。

▶Invoke:更新或查询提案事务中的帐本数据状态时,Invoke 方法被调用。因此响应调用或查询的业务实现逻辑都需要在此方法中编写实现。

在实际开发中,开发人员可以自定义一个结构体,然后重写 Chaincode接口的 Init 与 Invoke方法,并将两个方法指定为自定义结构体的成员方法。两个方法被调用时都会传入 一个存根对象(stub),链码可以利用该对象来获取请求的相关信息,例如调用 者身份、目标通道、参数等等。下面具体说一下如何开发智能合约。

目录说明:main.js:程序入口;bsnchaincode:链码文件夹;models:数据转换包;utils通用工具包;

chaincode.js 代码示例

base.js  set方法示例

▶依赖包

"fabric-shim": "~1.4.0"

fabric-shim 包为链码提供了 API 用来访问/操作数据状态、事务上下文和调用其他链代码;

▶链码方法介绍

◆getFunctionAndParameters()

返回一个方法调用描述对象,第一个值调用的链码方法名,第二个值要传入目标方法的参数对象。

◆getArgs() 

从链码调用请求中返回参数字符串数组,等价于getStringArgs()。

◆getStringArgs() 

返回链码调用请求中的参数字符串数组。

◆getTxID() 

返回当前链码调用请求的交易ID。交易ID在通道范围内唯一标识一个交易。

◆getChannelID() 

返回链码处理提议的通道ID

◆invokeChaincode(chaincodeName, args, channel) 

跨链提交链码:

如果被调用的链码在同一个通道,那么它只是简单地将被调用链码的读写集添加到被调用交易中。如果被调用的链码处于不同的通道,那么只会返回响应结果,在被调用链码中的putState调用不会影响账本的状态。

调用参数:

chaincodeName:要调用的链码名称。

args:调用参数列表,字节数组的数组。

channel:要调用的链码所在通道名称。

◆getState(key) 

获取指定状态变量键的当前值。

●参数

key: 要提取当前值的状态变量键。

◆putState(key, value)

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

●参数:

key:要更新的状态键,字符串。

value:状态变量的新值,字节数组或字符串。

◆deleteState(key)

从状态库中删除指定的状态变量键。

●参数

key:要从状态库中删除的状态变量键

◆getStateByRange(startKey, endKey) 

返回一个账本状态键的迭代器,可用来 遍历在起始键和结束键之间的所有状态键,返回结果按词典顺序排列。当使用完毕后,调用返回的StateQueryIterator迭代器对象的close()方法关闭迭代器。

●参数

startKey:起始键。

endKey:结束键。

◆getStateByPartialCompositeKey(objectType, keys)

基于给定的部分复合键查询账本状态。 该方法返回的迭代器可用于遍历查询结果集。

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

●参数

objectType:结果键前缀。

keys:用于拼接复合键值的属性值列表,字符串数组。

◆createCompositeKey(objectType, attributes)

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

●参数

objectType:组合键前缀。

attributes:要拼接到组合键的各属性值,string数组。

◆splitCompositeKey(compositeKey)

将组合键分离,返回数据1:组合键前缀;返回数据2:要拼接到组合键的各属性值,string数组;返回数据3:错误信息。

●参数

compositeKey:组合键。

◆getQueryResult(query)

在状态数据库上执行一个rich查询。该方法 仅在支持rich查询的状态数据库上有效,例如CouchDB。查询语句采用 底层状态数据库的语法。返回的StateQueryIterator可用于遍历查询 结果集。

●参数

query:查询语句。

◆getQueryResultWithPagination(query, pageSize, bookmark) 

在状态数据库上执行一个rich查询, 该方法仅在支持rich查询的状态数据库上有效,例如CouchDB。查询语法依据 所采用的底层数据库。

●参数

query:查询语句,字符串。

pageSize:分页大小,整数。

bookmark:书签,字符串。

◆getHistoryForKey(key)

返回指定状态键的值历史记录。每次历史更新,都记录有 当时的值和关联的交易id、时间戳。时间戳取自交易提议头。

●参数

key:状态键。

◆getCreator()

返回链码调用者身份。

◆getTxTimestamp()

返回交易创建时的时间戳,值取自交易的ChannelHeader部分, 因此它表示的是客户端的时间戳,并且在所有的背书服务节点上有相同的值。

◆setEvent(name, payload)

设置链码事件,事件只有在Invoke中有效

●参数

name:时间名称

payload:通知内容。

◆getTransient()

返回交易中带有的一些临时信息,可以存放一些应用相关的保密信息,这些信息不会被写到账本中。

链码开发规范

▶所有链码方法参数信息必须校验。

●校验参数个数。

●校验参数值(长度、类型等等,根据业务场景定义)。

▶Init方法不能大量初始化数据。

需要初始化数据,单独写方法进行处理。

▶引用第三方包,需要使用npm管理。

使用Nodejs依赖包管理工具:npm。

安装:

新版的nodejs已经集成了npm,安装了Nodejs同时就安装了npm,可以通过输入 "npm -v" 来测试是否成功安装。如果你安装的是旧版本的 npm,可以通过 npm 命令来升级,命令如下:

npm install npm -g

使用:

●进入项目目录

●安装所需依赖:npm install package.json

●项目目录下自动生成 node_modules 文件

▶main函数,必须在项目中所有链码的上级或同级。

▶发布服务时,链码包打包时进入项目根目录进行打包,格式为.zip。

▶发布服务时,添加链码包的链码名称要与项目名称相同。

链码开发建议

▶关于key的定义

●描述

现阶段所有业务数据都存在于一个账本数据库中,并存储方式是以key-value的形式存储,可能存在不同业务的key值相同的情况。

●解决方案

在不同的业务key值添加业务前缀

●例子

如用户和角色他们的标识相同,如果以标识作为key存储时,后者保存会覆盖前者信息;但是如果用户:user_用户标识,角色:role_角色标识这样存储就会避免这个问题

▶关于根据key值模糊查询

●描述

根据key查询同一个业务数据时。

●解决方案

查询语句使用正则表达式进行查询的,{\"_id\":{\"$regex\":\"ChargeUnit_.*\"}修改为{\"_id\":{\"$regex\":\"^ChargeUnit_.*\"};前者检索key中只要含有“ChargeUnit”的数据,后者检索key以“ChargeUnit”开头的数据。

●例子

正则表达式:特殊符号转义:例:()[] {} . \

▶关于跨链调用(invokeChaincode)

●描述

由于BSN是提供的是公用的Fabric环境,为了保障通道ID(channelId)与链码名称(chaincodeName)的唯一性,链码部署完成后,用户才能拿到通道ID(channelId)与链码名称(chaincodeName)。那么链码中该如何得到这些值?

●解决方案

需要跨链调用的链码,需将channelId和chaincodeName作为业务参数传递。

●例子