5. 智能合约
智能合约是区块链中的一个非常重要的概念和组成部分。在Fabric中将智能合约称之为ChainCode - 链码。
下面将会介绍ChainCode的基本概念、代码结构、开发等过程。
链代码:
- 系统链码
- 合约链码 -> 需要安装到peer节点上
- go -> 推荐
- node.js
- java
5.1 ChainCode介绍
Fabric中的Chaicode包含了Chaicode代码和Chaicode管理命令这两部分的内容。
其中:
- Chaicode代码是业务的承载体,负责具体的业务逻辑;
- Chaincode管理命令负责Chaincdoe的部署、安装、维护等工作。
1. Chaincode代码
Fabric的Chaincode是一段运行在容器中的程序,这些程序可以是Go、Java、Node-js等语言开发的。 Chaincode是客户端程序和Fabric之间的桥梁。通过客户端程序可以发起交易、查询交易。Chaincode是运行在 Docker容器中的,因此Chaincode相对比较安全。在Chaincode开发语言的支持中,目前Fabric支持使用Golang、Java和node.js这三种程语言开发Chaincode。在这些开发语言中,Golang是比较成熟稳定的,Java和Node.js这两个语言的版本还在完善中。
2.Chaincode管理命令
Chaincode管理命令主要用来对Chaincode进行安装、实例化、调用、打包、签名等操作。
Chaincode命令包含在peer模块中,是peer模块的一个子命令,该子命令的名称为
chaincode,该子命令的格式如下所示:
peer chaincodepeer模块的Chaincode子命令及其参数选项的作用,
请参考第3.4章节。
5.2 Golang版本的ChainCode的代码结构
-
包名
// 一个chaincode通常是一个Golang源代码文件, 在这份源代码中, 包名必须是mainpackage main -
引入包
// ChainCode需要引入Fabric提供的一些系统包, 这些系统提供了ChainCode和Fabric进行通信的接口.// 必须要引入包的如下:import ("github.com/hyperledger/fabric/core/chaincode/shim"pb "github.com/hyperledger/fabric/protos/peer")/*在引入包中 "github.com/hyperledger/fabric/core/chaincode/shim" 是Fabric提供的上下文环境,包含了Fabric和ChainCode交互的接口。在ChainCode中,执行赋值、查询等功能都需要通过shim。*/ -
定义结构体并实现
/*每个ChainCode都需要定义一个结构体,结构体的名字可以是任意符合Golang命名规范的字符串。*/// 自定义结构体名为: chainCodeStudytype TestStudy struct {}/*Chaincode结构体是ChainCode的主体结构。ChainCode结构体需要实现Fabric提供的接口:"github.com/hyperledger/fabric/protos/peer",其中必须实现下面两个方法:*/// 系统初始化func (t *TestStudy) Init(stub shim.ChaincodeStubInterface) pb.Response {};// 数据写入func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response{}; -
Init方法
Init方法是系统初始化方法。当执行命令
peer chaincode instantiate实例化chaincode时候会调用该方法,同时命令中-c选项后面内容会作为参数传入Init方法中。以下面的chaincode实例化命令为例:Terminal window $ peer chaincode instantiate -o orderer.test.com:7050 -C mychanne -n mytestcc -v 1.0 -c '{"Args": ["init","a", "100","b","200"]}'上面命令给Chaincode传入4个参数
“a”、“100”、“b”、“200”。注意命令中Args后面一共有5个参数,其中第一个参数init是固定值,后面的才是参数。传参数的个数是没有限制的,但是实际应用的时候不要太多。如果有很多参数需要传递给ChainCode,可以采用一些数据格式(比如Json),把数据格式化之后传递给ChainCode。在Init方法中可以通过下列方法获取传入参数。func (t *TestStudy) Init(stub shim.ChaincodeStubInterface) pb.Response {// 获取客户端传入的参数, args是一个字符串, 存储传入的字符串参数_, args := stub.GetFunctionAndParameters()return shim.Success([]byte("sucess init!!!"))}; -
Invoke方法
Invoke方法的主要作用是写入数据,比如发起交易等。在执行命令
peer chaincode invoke的时候系统会调用该方法,同时会把命令中-c后面的参数传入Invoke方法中,以下面的Invoke命令为例:Terminal window $ peer chaincode invoke -o 192.168.1.100:7050 -C mychanne -n mytestcc -c '{"Args": ["invoke","a","b","10"]}'上面的命令调用Chaincode的Invoke方法并且传入三个参数
“a”、"b”、“10”。注意Args后面数组中的第一个值“invoke”是默认的固定参数。func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {// 进行交易操作的源代码, 调用ChaincodeStubInterface接口中的方法// stub.xxx()// stub.yyy()return shim.Success([]byte("sucess invoke!!!"))}; -
shim包的核心方法
在Fabric的Golang语言的Chaincode源代码中如需要引入系统包
"github.com/hyperledger/fabric/core/chaincode/shim"。shim包主要负责和客户端进行通信。shim提供了一组核心方法和客户端进行交互,这些方法如下所示。shim中API 查看地址:
https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim-
Success
/*Sucess 方法负责将正确的消息返回给调用ChainCode的客户端, Sucess方法的定义和调用如下:*/// 方法定义func Success(payload []byte) pb.Response;// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {return shim.Success([]byte("sucess invoke!!!"))}; -
Error
// Error方法负责将错误信息返回给调用ChainCode的客户端, Error方法的定义和调用如下// 方法定义func Error(msg string) pb.Response;// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {return shim.Error("operation fail!!!")}; -
LogLevel
// LogLevel方法负责修改ChainCode中运行日志的级别, LogLevel方法的定义和调用如下// 将日志级别描述字符串转为 LoggingLevel 类型func LogLevel(levelString string) (LoggingLevel, error);- levelString可用参数:- CRITICAL, 级别最高, 写日志最少- ERROR- WARNING- NOTICE- INFO- DEBUG, 级别最低, 写日志最多// 设置日志级别func SetLoggingLevel(level LoggingLevel);// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {loglevel, _ := shim.LogLevel("debug")shim.setLoggingLevel(loglevel)return shim.Success([]byte("operation fail!!!"))};
-
-
ChaincodeStubInterface接口中的核心方法
在shim包中有一个接口ChaincodeStubInterface,该接口提供了一组方法,通过这组方法可以非常方便的操作Fabric中账本数据。ChaincodeStubInterface接口的核心方法大概可以分为四大类:系统管理、存储管理、交易管理、调动外部chaincode。
-
系统管理相关的方法
peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n mycc -c '{"Args":["invoke","a","b","10"]}'peer chaincode invoke -o orderer.itcast.com:7050// 赋值接收调用chaincode的客户端传递过来的参数func GetFunctionAndParameters() (function string, params []string);// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {// 获取客户端传入的参数, args是一个字符串, 存储传入的字符串参数_, args := stub.GetFunctionAndParameters()var a_param = args[0]var b_param = args[1]var c_param = args[2]return shim.Success([]byte("sucess init!!!"))}; -
存储管理相关的方法
-
PutState
// 把客户端传递过来的数据保存到Fabric中, 数据格式为键值对func PutState(key string, value []byte) error;// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {// 数据写入stub.PutState("user1", []byte("putvalue"))return shim.Success([]byte("sucess invoke user1"))}; -
GetState
// 从Fabric中取出数据, 然后把这些数据交给chaincode处理.func GetState(key string) ([]byte, error);// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {// 读数据keyvalue, err := stub.GetState("user1")return shim.Success(keyvalue)}; -
GetStateByRange
// 根据key的访问查询相关数据func GetStateByRange(startKey,endKey string)(StateQueryIteratorInterface, error);// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {startKey := "startkey"endKey := "endkey"// 根据范围查询, 得到StateQueryIteratorInterface迭代器接口keysIter, err := stub.getStateByRange(startKey, endKey)// 最后关闭迭代器接口defer keysIter.Close()var keys []stringfor keysIter.HasNext() { // 如果有下一个节点// 得到下一个键值对response, iterErr := keysIter.Next()if iterErr != nil {return shim.Error(fmt.Sprintf("find an error %s", iterErr))}keys = append(keys, response.Key) // 存储键值到数组中}// 遍历keys数组for key, value := range keys {fmt.Printf("key %d contains %s\n", key, value)}// 编码keys数组成json格式jsonKeys, err := json.Marshal(keys)if err := nil {return shim.Error(fmt.Sprintf("data Marshal json error: %s", err))}// 将编码之后的json字符串传递给客户端return shim.Success(jsonKeys)}; -
GetHistoryForKey
// 查询某个键的历史记录func GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error);// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {keysIter, err := stub.GetHistoryForKey("user1") // user1是一个假设的键值if err := nil {return shim.Error(fmt.Sprintf("GetHistoryForKey error: %s", err))}defer keysIter.Close()var keys []stringfor keysIter.HasNext() { // 遍历集合,如果有下一个节点// 得到下一个键值对response, iterErr := keysIter.Next()if iterErr != nil {return shim.Error(fmt.Sprintf("find an error %s", iterErr))}// 交易编号txid := response.TxId// 交易的值txvalue := response.Value// 当前交易的状态txStatus := response.IsDelete// 交易发生的时间戳txtimestamp := response.Timestamp// 计算从1970.1.1到时间戳的秒数tm := time.Unix(txtimestamp.Seconds, 0)// 根据指定的格式将日期格式化datestr := tm.Format("2018-11-11 11:11:11 AM")fmt.Printf("info - txid:%s, value:%s, isDel:%t, dateTime:%s\n", txid, string(txvalue), txStatus, datestr)keys = append(keys, txid)}// 将数组中历史信息的key编码为json格式jsonKeys, err := json.Marshal(keys)if err := nil {return shim.Error(fmt.Sprintf("data Marshal json error: %s", err))}// 将编码之后的json字符串传递给客户端return shim.Success(jsonKeys)} -
DelState
// 删除一个keyfunc DelState(key string) error;// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {err := stub.DelState("delKey")if err != nil {return shim.Error("delete key error !!!")}return shim.Success("delete key Success !!!")} -
CreateCompositeKey
// 给定一组属性,将这些属性组合起来构造一个复合键func CreateCompositeKey(objectType string, attributes []string) (string, error);// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {parms := []string("go1", "go2", "go3", "go4", "go5", "go6")ckey, _ := stub.CreateCompositeKey("testkey", parms)// 复合键存储到账本中err := stub.putState(ckey, []byte("hello, go"))if err != nil {fmt.Println("find errors %s", err)}// print value: testkeygo1go2go3go4go5go6fmt.Println(ckey)return shim.Success([]byte(ckey))} -
GetStateByPartialCompositeKey / SplitCompositeKey
// 根据局部的复合键返回所有的匹配的键值func GetStateByPartialCompositeKey(objectType string, keys []string)(StateQueryIteratorInterface, error);// 给定一个复合键,将其拆分为复合键所有的属性func SplitCompositeKey(compositeKey string) (string, []string, error)// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {searchparm := []string{"go1"}rs, err := stub.GetStateByPartialCompositeKey("testkey", searchparm)if err != nil {error_str := fmt.Sprintf("find error %s", err)return shim.Error(error_str)}defer rs.Close()var tlist []stringfor rs.HasNext() {responseRange, err := rs.Next()if err != nil {error_str := fmt.Sprintf("find error %s", err)fmt.Println(error_str)return shim.Error(error_str)}value1,compositeKeyParts,_ := stub.SplitCompositeKey(responseRange)value2 := compositeKeyParts[0]value3 := compositeKeyParts[1]// print: find value v1:testkey, v2:go1, v3go2fmt.Printf("find value v1:%s, v2:%s, V3:%s\n", value1, value2, value3)}return shim.Success("success")}
-
-
交易管理相关的方法
// 获取当前客户端发送的交易时间戳func GetTxTimestamp() (*timestamp.Timestamp, error);// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {txtime, err := stub.GetTxTimestamp()if err != nil {fmt.printf("Error getting transaction timestamp: %s", error)return shim.Error(fmt.Sprintf("get transaction timestamp error: %s", error))}tm := time.Unix(txtime.Second, 0)return shim.Success([]byte(fmt.Sprint("time is: %s", tm.Format("2006-01-02 03:04:05 PM"))))} -
调用其他chaincode的方法
// 调用另一个链码中的Invoke方法func InvokeChaincode(chaincodeName string,args [][]byte,channel string) pb.Response// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {// 设置参数, a向b转转11trans:=[][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("11")}// 调用chaincoderesponse := stub.InvokeChaincode("mycc", trans, "mychannel")// 判断是否操作成功了// 课查询: https://godoc.org/github.com/hyperledger/fabric/protos/peer#Responseif response.Status != shim.OK {errStr := fmt.Sprintf("Invoke failed, error: %s", response.Payload)return shim.Error(errStr)}return shim.Success([]byte("转账成功..."))}// ==================================================// 获取客户端发送的交易编号func GetTxID() string// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {txid := stub.GetTxID()return shim.Success([]byte(txid))}
-
5.3 ChainCode交易的背书(endorse)
区块链是一个去中心的,所有参与方集体维护的公共账本。Fabric作为一个典型的区块链的技平台当然也具备这样的特点。Fabric中对数据参与方对数据的确认是通过Chaincode来进行的。
在Fabric中有一个非常重要的概念称为Endorsement,中文名为背书。背书的过程是一笔交易被确认的过程。而背书策略被用来指示对相关的参与方如何对交易讲行确认。当一个节点接收到一个交易请求的时候,会调用VSCC(系统Chaincode,专门负责处理背书相关的操作)与交易的chaincode共同来验证交易的合法性。在VSCC和交易的Chaincode共同对交易的确认中, 通常会做以下的校验。
- 所有的背书是否有效(参与的背书的签名是否有效)。
- 参与背书的数量是否满足要求。
- 所有背书参与方是否满足要求。
背书政策是指定第二和第三点的一种方式。这些概念看起来还是比较难懂的,理解它们最好的办法是通过一个具体的实例。背书策略的设置是通过部署时
instantiate命令中-P-参数来设置的。命令样式如下: &&
$ peer chaincode instantiate -o oraderer.test.com:7050 -C mychannel -n mycc —v 1.0 -c '{"Args":["init", "a", "100", "b", "200"]}' -P "AND ('Org1MSP.member', 'Org2MSP.member')"# 上述命令是对Chaincode进行实例化的操作,我们提取-P后面的参数:"AND ('Org1MSP.member', 'Org2MSP.member')"这个参数包说明的是当前Chaincode发起的交易,需要组织编号为
Org1MSP和组织编号为Org2MSP的组织中的任何一个用户共同参与交易的确认并且同意,这样交易才能生效并被记录到区块链中。通过上述背书策略的实例我们可以知道背书策略是通过一定的关键字和系统的属性组成的。根据Fabric的系统定义,可以将上面的背书策略拆解如下:
AND参与背书者之间的关系,AND表示所有参与方共同对交易进行确认。除了AND之外还可以使用关键字OR,如果使用关键字OR表示参与方的任何一方参与背书即完成交易的确认。Org1MSP.member表示参与背书的组和组织中参与背书的用户。Org1MSP表示组织的编号,这个值是怎么来的呢?之前我们介绍过
cryptogen模块,该模块根据配置文件生成系统的配置和账号信息。在cryptogen的配置文件中有一个节点Organizations->ID,该节点的值就是该组织的编号,也是在配置背书策略时需要用到的组织的编号。member泛指组织内的任何一个用户,当然也可以是组织某个具体的用户。通过上面的描述,基本上可以了解背书策略的编写规则,下面通过几个实例进一步了解背书策略的编写规则。
-
背书规则示例1
Terminal window # 按照该背书规则进行交易, 必须通过组织Org1MSP,Org2MSP,Org3MSP中的用户共同验证交易才能生效"AND ('Org1MSP.member', 'Org2MSP.member', 'Org3MSP.member')" -
背书规则示例2
Terminal window # 按照该背书规则进行交易,只需要通过组织 Org1MSP 或 Org2MSP 或 Org3MSP 中的任何一个成员验证,即可生效"OR ('Org1MSP.member', 'Org2MSP.member', 'Org3MSP.member')" -
背书规则示例3
Terminal window # 按照该背书规则进行交易,有两种办法让交易生效# 1. 组织Org1MSP中的某个成员对交易进行验证。# 2. 组织Org2MSP和组织Org3MSP中的成员共同就交易进行验证。"OR ('Org1MSP.member', AND('Org2MSP.member', 'Org3MSP.member'))""AND ('Org1MSP.member', OR('Org2MSP.member', 'Org3MSP.member'))"
以上介绍了背书的规则,有一点需要注意:背书规则只针对chaincode中写入数据的操作进行校验,对于查询类操作不背书。以golang版本的chaincode为例, 需要利用背书规则对操作进行校验的方法如下:
PutState(key string, value []byte) error;DelState(key string) error;Fabric中的背书是发生在客户端的,需要进行相关的代码的编写才能完成整个背书的操作。