-
智能合约是什么?
chaincode(链码)称为智能合约,是一段程序,由Go、node.js、或JAVA编写,来实现一些预定义接口,链码运行在一个和背书节点分开的独立进程中,通过应用程序提交的交易来初始化和管理账本状态。
智能合约一般处理网络中的成员一致同意的商业逻辑,所以它类似于“智能合约”。链码可以在提案交易中被调用用来升级或者查询账本。赋予适当的权限,链码就可以调用其他链码访问它的状态,不管是在同一个通道还是不同的通道。注意,如果被调用链码和调用链码在不同的通道,就只能执行只读查询。就是说,调用不同通道的链码只能进行“查询”,在提交的子语句中不能参与状态的合法性检查。
智能合约是一种旨在以信息化方式传播、验证或执行合同的计算机协议。智能合约允许在没有第三方的情况下进行可信交易。这些交易可追踪且不可逆转,Fabric为智能合约提供了一个可信的执行环境。
-
智能合约的编写
每一个chaincode 程序都必须实现 Chaincode 接口,其中2个接口是每个程序必不可少的,Init 和Invoke接口(如下)。接口详情详见:https://pkg.go.dev/github.com/hyperledger/fabric-chaincode-go/shim#Chaincode
type Chaincode interface { // Init is called during Instantiate transaction after the chaincode container // has been established for the first time, allowing the chaincode to // initialize its internal data Init(stub ChaincodeStubInterface) pb.Response // Invoke is called to update or query the ledger in a proposal transaction. // Updated state variables are not committed to the ledger until the // transaction is committed. Invoke(stub ChaincodeStubInterface) pb.Response }
所以一个最小的合约实例结构如下:
package main import ( "github.com/hyperledger/fabric/core/chaincode/shim" pb "github.com/hyperledger/fabric/protos/peer" ) type SimpleChaincode struct {}
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response { //初始化逻辑
} func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response { //其他业务逻辑
} func main() { if err := shim.Start(new(SimpleChaincode)); err != nil { // TODO: logger error } }
可结合官方GitHub中的fabric-samples的智能合约chaincode_example02,此处不粘贴其代码
当智能合约写完后,就可以部署在fabric链上了
链码相关API
shim 包提供了如下几种类型的接口:
- 参数解析 API:调用链码时需要给被调用的目标函数/方法传递参数,该 API 提供解析这些参数的方法
- 账本状态数据操作 API:该 API 提供了对账本数据状态进行操作的方法,包括对状态数据的查询及事务处理等
- 交易信息获取 API:获取提交的交易信息的相关 API
- 对 PrivateData 操作的 API: Hyperledger Fabric 在 1.2.0 版本中新增的对私有数据操作的相关 API
- 其他 API:其他的 API,包括事件设置、调用其他链码操作
参数解析API
// 返回调用链码时指定提供的参数列表(以字符串数组形式返回)
GetStringArgs() []string
// 返回调用链码时在交易提案中指定提供的被调用的函数名称及函数的参数列表(以字符串数组形式返回)
GetFunctionAndParameters() (function string, params []string)
// 返回提交交易提案时提供的参数列表(以字节串数组形式返回)
GetArgsSlice() ([]byte, error)
// 返回调用链码时在交易提案中指定提供的被调用的函数名称及函数的参数列表(以字符串数组形式返回)
GetArgs() [][]byte
一般使用 GetFunctionAndParameters() 及 GetStringArgs() 。
账本数据状态操作API
// 查询账本,返回指定键对应的值 GetState(key string) ([]byte, error) // 尝试添加/更新账本中的一对键值 // 这一对键值会被添加到写集合中,等待 Committer 进一步确认,验证通过后才会真正写入到账本 PutState(key string, value []byte) error // 尝试删除账本中的一对键值 // 同样,对该对键值删除会添加到写集合中,等待 Committer 进一步确认,验证通过后才会真正写入到账本 DelState(key string) error // 查询指定范围的键值,startKey 和 endkey 分别指定开始(包括)和终止(不包括),当为空时默认是最大范围 // 返回结果是一个迭代器结构,可以按照字典序迭代每个键值对,最后需要调用 Close() 方法关闭 GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) // 返回指定键的所有历史值。该方法的使用需要节点配置中打开历史数据库特性(ledger.history.enableHistoryDatabase=true) GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error) // 给定一组属性(attributes),将这些属性组合起来构造返回一个复合键 // 例如:CreateComositeKey("name-age",[]string{"Alice", "12"}); CreateCompositeKey(objectType string, attributes []string) (string, error) // 将指定的复合键进行分割,拆分成构造复合键时所用的属性 SplitCompositeKey(compositeKey string) (string, []string, error) // 根据局部的复合键(前缀)返回所有匹配的键值,即与账本中的键进行前缀匹配 // 返回结果是一个迭代器结构,可以按照字典序迭代每个键值对,最后需要调用 Close() 方法关闭 GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error) // 对(支持富查询功能的)状态数据库进行富查询,返回结果是一个迭代器结构,目前只支持 CouchDB // 注意该方法不会被 Committer 重新执行进行验证,所以不能用于更新账本状态的交易中 GetQueryResult(query string) (StateQueryIteratorInterface, error)
注意: 通过 put 写入的数据状态不能立刻 get 到,因为 put 只是链码执行的模拟交易(防止重复提交攻击),并不会真正将状态保存到账本中,必须经过 Orderer 达成共识之后,将数据状态保存在区块中,然后保存在各 peer 节点的账本中。
交易信息相关API
// 返回交易提案中指定的交易 ID。 // 一般情况下,交易 ID 是客户端提交提案时由 Nonce 随机串和签名者身份信息哈希产生的数字摘要 GetTxID() string // 返回交易提案中指定的 Channel ID GetChannelID() string // 返回交易被创建时的客户端打上的的时间戳 // 这个时间戳是直接从交易 ChannnelHeader 中提取的,所以在所以背书节点处看到的值都相同 GetTxTimestamp() (*timestamp.Timestamp, error) // 返回交易的 binding 信息 // 交易的 binding 信息是将交提案的 nonse、Creator、epoch 等信息组合起来哈希得到数字摘要 GetBinding() ([]byte, error) // 返回该 stub 的 SignedProposal 结构,包括了跟交易提案相关的所有数据 GetSignedProposal() (*pb.SignedProposal, error) // 返回该交易提交者的身份信息(用户证书) // 从 SignedProposal 中的 SignatureHeader.Creator 提取 GetCreator() ([]byte, error) // 返回交易中带有的一些临时信息 // 从 ChaincodeProposalPayload.transient 提取,可以存放与应用相关的保密信息,该信息不会被写入到账本 GetTransient() (map[string][]byte, error)
对PrivateData操作的API
// 根据指定的 key,从指定的私有数据集中查询对应的私有数据 GetPrivateData(collection, key string) ([]byte, error) // 将指定的 key 与 value 保存到私有数据集中 PutPrivateData(collection string, key string, value []byte) error // 根据指定的 key 从私有数据集中删除相应的数据 DelPrivateData(collection, key string) error // 根据指定的开始与结束 key 查询范围(不包含结束key)内的私有数据 GetPrivateDataByRange(collection, startKey, endKey string) (StateQueryIteratorInterface, error) // 根据给定的部分组合键的集合,查询给定的私有状态 GetPrivateDataByPartialCompositeKey(collection, objectType string, keys []string) (StateQueryIteratorInterface, error) // 根据指定的查询字符串执行富查询 (只支持支持富查询的 CouchDB) GetPrivateDataQueryResult(collection, query string) (StateQueryIteratorInterface, error)
智能合约单元测试
(参考https://www.cnblogs.com/skzxc/p/12150476.html)
单元测试 (UT) 可以提高调试的效率和我们代码的质量。fabric中提供了一个MockStub类用于单元测试。
单元测试不需要启动任何网络节点,通过我们的测试文件就可以在本地对链码中的接口进行调用测试。
其原理就是在MockStub类中维护一个 map[string][]byte 来模拟 key-val 的状态数据库,链码调用的PutState() 和 GetState() 其实是作用于内存中的map。
MockStub主要提供两个函数来模拟背书节点对链码的调用:MockInit()和MockInvoke(),分别调用Init和Invoke接口。接收的参数均为类型为string的uuid(随便设置即可),以及一个二维byte数组(用于测试的提供参数)。
单元测试要求:
1.需要导入testing包;
2.单元测试文件以_test.go结尾;
3.测试用例的函数必须以Test开头,可选的后缀名必须以大写字母开头!
单元测试的实例
测试examlpe_cc.go智能合约,其测试用例如下:
package main import ( "fmt" "github.com/hyperledger/fabric/core/chaincode/shim" "testing" ) func TestFunc(t *testing.T) { cc := new(SimpleChaincode) // 创建Chaincode对象 stub := shim.NewMockStub("sacc", cc) // 创建MockStub对象 // 调用Init接口,将a的值设为90 stub.MockInit("1", [][]byte{[]byte("init"), []byte("a"),[]byte("100"),[]byte("b"),[]byte("200")}) // 调用query接口查询a的值 res := stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("a")}) fmt.Println("The value of a is ", string(res.Payload)) // 调用query接口查询a的值 res = stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("b")}) fmt.Println("The value of b is ", string(res.Payload)) // 转账 res = stub.MockInvoke("1", [][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("50")}) // 再次查询a的值 res = stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("a")}) fmt.Println("The new value of a is ", string(res.Payload)) }
测试结果如下:
ex02 Init [a 100 b 200] Aval = 100, Bval = 200 ex02 Invoke Query Response:{"Name":"a","Amount":"100"} The value of a is 100 ex02 Invoke Query Response:{"Name":"b","Amount":"200"} The value of b is 200 ex02 Invoke Aval = 50, Bval = 250 ex02 Invoke Query Response:{"Name":"a","Amount":"50"} The new value of a is 50 PASS
ok go-demo 0.314s