作者:曹国波 时跃堂
智能合约又称链码(Chaincode),是用计算机语言描述合约条款、交易的条件、交易的业务逻辑等,通过调用智能合约实现交易的自动执行和对账本数据的操作。一个BSN应用可以部署多个链码,每个链码包含多个方法。
链码支持多种语言编写,包括golang、java、node.js。每个链码程序都必须实现Chaincode接口,链码包含:Init ,Invoke ,Query三个基本操作:
▶ Init :链码初始化的方法,在链码实例化或者升级的时候调用一次,以便链码可以执行任何必要的初始化,包括应用程序状态的初始化。
▶ Invoke :接收和处理链下业务系统调用事务处理提案,其参数包含调用的链码程序中函数的名称和具体业务处理数据参数。即在Invoke中根据不同的方法参数调用其他分支处理响应的业务。Invoke可以简单的理解为链码方法的入口。
▶ Query:提供查询链码数据的方法,该方法只作为查询使用,不提供操作链上数据的操作。可在Query操作时调用,亦可在Invoke方法中作为某些方法的分支被调用。该方法可以不实现。
本文主要介绍用户如何用golang语言开发智能合约,以及在BSN中对智能合约开发的一些规范和建议。
如何开发智能合约
编写链码,关键是实现 Init 与 Invoke 两个方法。
▶ Init:在链码实例化或者升级的时候调用一次, 完成初始化数据的工作。建议处理一些简单的处理,禁止使用该方法去初始化大量基础数据,如果有需要初始化的数据,建议在Invoke中处理。
▶Invoke:更新或查询提案事务中的帐本数据状态时,Invoke 方法被调用。因此响应调用或查询的业务实现逻辑都需要在此方法中编写实现。
在实际开发中,开发人员可以自定义一个结构体,然后重写 Chaincode接口的 Init 与 Invoke方法,并将两个方法指定为自定义结构体的成员方法。两个方法被调用时都会传入 一个存根对象(stub),链码可以利用该对象来获取请求的相关信息,例如调用 者身份、目标通道、参数等等。下面以通用数据链码包为例,具体说一下如何开发智能合约。
目录说明:main.go:程序入口;bsnchaincode:链码文件夹;models:实体;test:单元测试;utils通用工具包;META-INF/statedb/couchdb/indexes:创建索引
▶ 依赖包
"github.com/hyperledger/fabric/core/chaincode/shim" "github.com/hyperledger/fabric/protos/peer"
shim 包为链码提供了 API 用来访问/操作数据状态、事务上下文和调用其他链代码;peer 包提供了链码执行后的响应信息。shim.ChaincodeStubInterface提供的方法来读取和修改账本的状态;peer.Response:封装的响应信息
▶ 链码方法介绍
◆ GetFunctionAndParameters() (string, []string)
返回一个方法调用描述对象,第一个值调用的链码方法名,第二个值要传入目标方法的参数对象。
◆GetArgs() [][]byte
从链码调用请求中返回参数字符串数组,等价getStringArgs()。
◆ GetStringArgs() []string
返回链码调用请求中的参数字符串数组。
◆ GetTxID() string
返回当前链码调用请求的交易ID。交易ID在通道范围内唯一标识一个交易。
◆ GetChannelID() string
返回链码处理提议的通道ID
◆ InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response
跨链提交链码:如果被调用的链码在同一个通道,那么它只是简单地将被调用链码的读写集添加到被调用交易中。如果被调用的链码处于不同的通道,那么只会返回响应结果,在被调用链码中的PutState调用不会影响账本的状态。具体调用参数如下:
● chaincodeName:要调用的链码名称。
● args:调用参数列表,字节数组的数组。
● channel:要调用的链码所在通道名称。
◆ GetState(key string) ([]byte, error)
获取指定状态变量键的当前值。具体调用参数如下:
●key: 要提取当前值的状态变量键。
◆PutState(key string, value []byte) error:更新状态库中指定的状态变量键。如果变量已经存在,那么覆盖已有的值。具体调用参数如下:
●key:要更新的状态键,字符串。
●value:状态变量的新值,字节数组或字符串。
◆ DelState(key string) error:从状态库中删除指定的状态变量键。具体调用参数如下:
●key:要从状态库中删除的状态变量键
◆ GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)
返回一个账本状态键的迭代器,可用来 遍历在起始键和结束键之间的所有状态键,返回结果按词典顺序排列。当使用完毕后,调用返回的StateQueryIterator迭代器对象的close()方法关闭迭代器。具体调用参数如下:
●startKey:起始键。
●endKey:结束键。
◆GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)
基于给定的部分复合键查询账本状态。该方法返回的迭代器可用于遍历查询结果集。
当使用完毕后,调用返回的StateQueryIterator迭代器的close()方法关闭迭代器。具体调用参数如下:
●objectType:结果键前缀。
●keys:用于拼接复合键值的属性值列表,字符串数组。
◆CreateCompositeKey(objectType string, attributes []string) (string, error)
通过组合对象类别和给定的属性创建一个组合键。对象类别及属性都必须是 有效的utf8字符串,并且不能包含U+0000 (空字节) 和 U+10FFFF (最大未分配代码点)。结果组合键可以用作PushState()调用中的参数键。具体调用参数如下:
● objectType:组合键前缀。
●attributes:要拼接到组合键的各属性值,string数组。
◆SplitCompositeKey(compositeKey string) (string, []string, error)
将组合键分离,返回数据1:组合键前缀;返回数据2:要拼接到组合键的各属性值,string数组;返回数据3:错误信息。具体调用参数如下:
●compositeKey:组合键。
◆GetQueryResult(query string) (StateQueryIteratorInterface, error)
在状态数据库上执行一个rich查询。该方法 仅在支持rich查询的状态数据库上有效,例如CouchDB。查询语句采用 底层状态数据库的语法。返回的StateQueryIterator可用于遍历查询 结果集。具体调用参数如下:
●query:查询语句。
◆GetQueryResultWithPagination(query string, pageSize int32,bookmark string) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error)
在状态数据库上执行一个rich查询, 该方法仅在支持rich查询的状态数据库上有效,例如CouchDB。查询语法依据 所采用的底层数据库。具体调用参数如下:
●query:查询语句,字符串。
●pageSize:分页大小,整数。
●bookmark:书签,字符串。
◆GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)
返回指定状态键的值历史记录。每次历史更新,都记录有 当时的值和关联的交易id、时间戳。时间戳取自交易提议头。具体调用参数如下:
●key:状态键。
◆GetCreator() ([]byte, error)
返回链码调用者身份。
◆GetTxTimestamp() (*timestamp.Timestamp, error)
返回交易创建时的时间戳,值取自交易的ChannelHeader部分, 因此它表示的是客户端的时间戳,并且在所有的背书服务节点上有相同的值。
◆SetEvent(name string, payload []byte) error
设置链码事件,事件只有在Invoke中有效,具体调用参数如下:
●name:时间名称
●payload:通知内容。
◆GetTransient() (map[string][]byte, error)
返回交易中带有的一些临时信息,可以存放一些应用相关的保密信息,这些信息不会被写到账本中。
链码开发规范
▶ 所有链码方法参数信息必须校验。
●校验参数个数。
●校验参数值(长度、类型等等,根据业务场景定义)。
▶ Init方法不能大量初始化数据。
需要初始化数据,单独写方法进行处理。
▶ 引用第三方包,需要使用govendor管理。
使用golang依赖包管理工具:govendor。
安装:
go get -u -v github.com/kardianos/govendor
使用:
●进入项目目录
●初始化vendor目录:govendor init
●将GOPATH中本工程使用到的依赖包自动移动到vendor目录中
govendor add +external 或使用缩写:govendor add +e
▶ main函数,必须在项目中所有链码的上级或同级。
▶ 发布服务时,链码包打包时进入项目根目录进行打包,格式为.zip。
▶ 发布服务时,添加链码包的链码名称要与项目名称相同。
链码开发建议
▶ 关于key的定义
●描述
现阶段所有业务数据都存在于一个账本数据库中,并存储方式是以key-value的形式存储,可能存在不同业务的key值相同的情况。
●解决方案
在不同的业务key值添加业务前缀
●例子
如用户和角色他们的标识相同,如果以标识作为key存储时,后者保存会覆盖前者信息;但是如果用户:user_用户标识,角色:role_角色标识这样存储就会避免这个问题
▶ 关于根据key值模糊查询
●描述
根据key查询同一个业务数据时。
●解决方案
查询语句使用正则表达式进行查询的,{\"_id\":{\"$regex\":\"ChargeUnit_.*\"}修改为{\"_id\":{\"$regex\":\"^ChargeUnit_.*\"};前者检索key中只要含有“ChargeUnit”的数据,后者检索key以“ChargeUnit”开头的数据。
●例子
正则表达式:特殊符号转义:例:()[] {} . \
▶ 关于浮点数计算
●描述
由于浮点数在计算机内的表示方式问题导致有一部分数据会出现问题。
●解决方案
使用"github.com/shopspring/decimal"包,将对浮点数进行精确计算。
●例子
正常计算
var v = 67.6
fmt.Println(int64(v *100)) 输出结果为:6759。
使用decimal
f1 := decimal.NewFromFloat(v)
f2 := decimal.NewFromFloat(100)
fmt.Println(f1.Mul(f2).IntPart()) 输出结果为6760。
▶ 关于链码索引
●描述
随着数据量不断增加,查询数据可能会变得很慢,可以适当的创建索引 。
●解决方案
在main.go的同级目录下创建文件META-INF\statedb\couchdb\indexes,在该indexes目录下创建.json文件,文件名随便定。.json文件个数没有限制。编辑该json文件。
●例子
{ "index": { "fields": [ "fileId" ] }, "ddoc": "fileIdIndex", "name": "fileId-json-index", "type": "json" }
▶ 关于跨链调用(InvokeChaincode)
●描述
由于BSN是提供的是公用的Fabric环境,为了保障通道ID(channelId)与链码名称(chaincodeName)的唯一性,链码部署完成后,用户才能拿到通道ID(channelId)与链码名称(chaincodeName)。那么链码中该如何得到这些值?
●解决方案
需要跨链调用的链码,需将channelId和chaincodeName作为业务参数传递。
●例子