作者:时跃堂 陈曦
智能合约又称链码(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作为业务参数传递。
●例子