链码的编写
前言:fabric链码的编写较简单,在熟悉了基本结构和相关API之后就可上手编写,但是要多多练习,提高编写链码的速度和正确度。
学习步骤:
1.熟悉链码的基本结构
2.熟练链码相关API
3.练习,练习,练习
参考链接:https://www.cnblogs.com/zongmin/p/11874792.html#_label0_1
1.链码的基本结构
链码的启动必须通过调用shim包中的Start函数,传递一个类型为Chaincode的参数,该参数是一个接口类型,有两个重要的函数Init和Invoke函数,(即每个链码都需实现chaincode接口)
Start函数
- Init
在链码实例化或者升级时被调用,完成初始化数据的工作
在此方法中实现链码的初始化或升级时的处理逻辑
- Invoke
更新或查询账本中的数据状态时被调用,需要在此方法中实现响应调用或查询的业务逻辑
在此方法中实现链码运行中被调用或查询时的处理逻辑
在实际开发中,我们可以自行定义一个结构体,实现chaincode接口,重写chaincode接口的两个方法,并将两个方法指定为自定义结构体的成员方法
链码基本结构如下所示:
package main
import (
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)
//自定义结构体
type SimpleChaincode struct {
}
//为结构体添加Init方法
func (s*SimpleChaincode)Init(stub shim.ChaincodeStubInterface)pb.Response {
return shim.Success(nil)
}
//为结构体添加Invoke方法
func (s *SimpleChaincode)Invoke(stub shim.ChaincodeStubInterface)pb.Response {
return shim.Success(nil)
}
//调用Start()函数
func main() {
shim.Start(new(SimpleChaincode))
}
- shim: 用来访问/操作数据状态、事务上下文和调用其他链代码的 API, 链码通过 shim.ChaincodeStub 提供的方法来读取和修改账本的状态
- peer: 提供了链码执行后的响应信息的 API,peer.Response 封装了响应信息
2.链码常用API
shim 包提供了如下几种类型的接口:
- **参数解析 API:**调用链码时需要给被调用的目标函数/方法传递参数,该 API 提供解析这些参数的方法
- **账本状态数据操作 API:**该 API 提供了对账本数据状态进行操作的方法,包括对状态数据的查询及事务处理等
- **交易信息获取 API:**获取提交的交易信息的相关 API
- 对 PrivateData 操作的 API: Hyperledger Fabric 在 1.2.0 版本中新增的对私有数据操作的相关 API
- **其他 API:**其他的 API,包括事件设置、调用其他链码操作
2.1 参数解析 API
// 返回调用链码时指定提供的参数列表(以字符串数组形式返回)
GetStringArgs() []string
// 返回调用链码时在交易提案中指定提供的被调用的函数名称及函数的参数列表(以字符串数组形式返回)
GetFunctionAndParameters() (function string, params []string)
// 返回提交交易提案时提供的参数列表(以字节串数组形式返回)
GetArgsSlice() ([]byte, error)
// 返回调用链码时在交易提案中指定提供的被调用的函数名称及函数的参数列表(以字符串数组形式返回)
GetArgs() [][]byte
一般使用 GetFunctionAndParameters() 及 GetStringArgs() 。
2.2 账本数据状态操作 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 节点的账本中。
2.3 交易信息相关 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)
GetTransient()在私有数据的编写会用到
2.4 对 PrivateData [私有数据]操作的 API
2.5 其他 API
// 设定当这个交易在 Committer 处被认证通过,写入到区块时发送的事件(event),一般由 Client 监听
SetEvent(name string, payload []byte) error
// 调用另外一个链码的 Invoke 方法
// 如果被调用链码在同一个通道内,则添加其读写集合信息到调用交易;否则执行调用但不影响读写集合信息
// 如果 channel 为空,则默认为当前通道。目前仅限读操作,同时不会生成新的交易
InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response
3.链码开发示例
此链码为fabric-sample/chaincode中的实例链码,可在fabric-sample中查看详情
[要自己跟着手敲一遍!不能光看,多练习!,先自己缕一遍思路,先回想一下链码的基本结构,以及API]
package main
import (
"fmt"
"strconv"
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)
type SimpleChaincode struct {
}
// 初始化数据状态,实例化/升级链码时被自动调用
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
// println 函数的输出信息会出现在链码容器的日志中
fmt.Println("ex02 Init")
// 获取用户传递给调用链码的所需参数
_, args := stub.GetFunctionAndParameters()
var A, B string // 两个账户
var Aval, Bval int // 两个账户的余额
var err error
// 检查合法性, 检查参数数量是否为 4 个, 如果不是, 则返回错误信息
if len(args) != 4 {
return shim.Error("Incorrect number of arguments. Expecting 4")
}
A = args[0] // 账户 A 用户名
Aval, err = strconv.Atoi(args[1]) // 账户 A 余额
if err != nil {
return shim.Error("Expecting integer value for asset holding")
}
B = args[2] // 账户 B 用户名
Bval, err = strconv.Atoi(args[3]) // 账户 B 余额
if err != nil {
return shim.Error("Expecting integer value for asset holding")
}
fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)
// 将账户 A 的状态写入账本中
err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
if err != nil {
return shim.Error(err.Error())
}
// 将账户 B 的状态写入账本中
err = stub.PutState(B, []byte(strconv.Itoa(Bval)))
if err != nil {
return shim.Error(err.Error())
}
// 一切成功,返回 nil(shim.Success)
return shim.Success(nil)
}
// 对账本数据进行操作时(query, invoke)被自动调用
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("ex02 Invoke")
// 获取用户传递给调用链码的函数名称及参数
function, args := stub.GetFunctionAndParameters()
// 对获取到的函数名称进行判断
if function == "invoke" {
// 调用 invoke 函数实现转账操作
return t.invoke(stub, args)
} else if function == "delete" {
// 调用 delete 函数实现账户注销
return t.delete(stub, args)
} else if function == "query" {
// 调用 query 实现账户查询操作
return t.query(stub, args)
}
// 传递的函数名出错,返回 shim.Error()
return shim.Error("Invalid invoke function name. Expecting \"invoke\" \"delete\" \"query\"")
}
// 账户间转钱
func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var A, B string // 账户 A 和 B
var Aval, Bval int // 账户余额
var X int // 转账金额
var err error
if len(args) != 3 {
return shim.Error("Incorrect number of arguments. Expecting 3")
}
A = args[0] // 账户 A 用户名
B = args[1] // 账户 B 用户名
// 从账本中获取 A 的余额
Avalbytes, err := stub.GetState(A)
if err != nil {
return shim.Error("Failed to get state")
}
if Avalbytes == nil {
return shim.Error("Entity not found")
}
Aval, _ = strconv.Atoi(string(Avalbytes))
// 从账本中获取 B 的余额
Bvalbytes, err := stub.GetState(B)
if err != nil {
return shim.Error("Failed to get state")
}
if Bvalbytes == nil {
return shim.Error("Entity not found")
}
Bval, _ = strconv.Atoi(string(Bvalbytes))
// X 为 转账金额
X, err = strconv.Atoi(args[2])
if err != nil {
return shim.Error("Invalid transaction amount, expecting a integer value")
}
// 转账
Aval = Aval - X
Bval = Bval + X
fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)
// 更新转账后账本中 A 余额
err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
if err != nil {
return shim.Error(err.Error())
}
// 更新转账后账本中 B 余额
err = stub.PutState(B, []byte(strconv.Itoa(Bval)))
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(nil)
}
// 账户注销
func (t *SimpleChaincode) delete(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
A := args[0] // 账户用户名
// 从账本中删除该账户状态
err := stub.DelState(A)
if err != nil {
return shim.Error("Failed to delete state")
}
return shim.Success(nil)
}
// 账户查询
func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var A string
var err error
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting name of the person to query")
}
A = args[0] // 账户用户名
// 从账本中获取该账户余额
Avalbytes, err := stub.GetState(A)
if err != nil {
jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}"
return shim.Error(jsonResp)
}
if Avalbytes == nil {
jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}"
return shim.Error(jsonResp)
}
jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}"
fmt.Printf("Query Response:%s\n", jsonResp)
// 返回转账金额
return shim.Success(Avalbytes)
}
func main() {
err := shim.Start(new(SimpleChaincode))
if err != nil {
fmt.Printf("Error starting Simple chaincode: %s", err)
}
}
4.链码测试
好耶,走到这里,你应该已经学会了链码的基本编写,但是我们编写的链码是需要部署在fabric网络之中的,不能直接运行,在开发过程中可选择单元测试或在fabric中设置dev模式【开发者模式】,将链码部署在网络中,进行测试。
单元测试:无需将链码部署在网络中,操作简单但是不太方便,无法进行富查询【富查询需要在有fabric网络的情况下】
dev模式【建议测试方式】:在fabric网络中的测试,需要将链码部署在fabric网络中。测试较方便,可以进行富查询。但是相较于单元测试,会有些难度。
建议学习方式:
在学习链码的初级阶段,先使用单元测试【对fabric还不是很了解】(如果对fabric已经掌握的不错了,环境搭建方面没什么问题了,可直接使用dev模式进行测试),在之后的学习过程中,使用dev。
单元测试:
参考链接:https://www.cnblogs.com/skzxc/p/12150476.html
fabric中提供了一个MockStub类用于单元测试。
单元测试不需要启动任何网络节点,通过我们的测试文件就可以在本地对链码中的接口进行调用测试。其原理就是在MockStub类中维护一个 map 来模拟 ke,key-val 的状态数据库,链码调用的PutState() 和 GetState() 其实是作用于内存中的map。
MockStub主要提供两个函数来模拟背书节点对链码的调用:MockInit()和MockInvoke(),分别调用Init和Invoke接口。接收的参数均为类型为string的uuid(随便设置即可),以及一个二维byte数组(用于测试的提供参数)。二维byte数组的第一个参数为方法名。例如参数为[][]byte{[]byte(“query”),[]byte(“A”)},使用stub.GetFunctionAndParameters方法返回的第一个参数是string类型的方法名,第二个参数为字符串切片类型,在此例中,则方法名为query,参数为[]string{“A”}
单元测试要求:
1.测试文件要以xx_test.go方式命名(即要以文件命名要以_test结尾)
2.测试方法名要以Testxxxx的方式命名(即方法名要以Test开头)
3.要导入testing包
单元测试示例:
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"testing"
)
//测试init方法
func TestSmartContract_Init(t *testing.T) {
//调用NewMockStub方法返回一个MockStub类,来模拟背书节点对链码的调用
//接收参数为一个string类型的name,随便写无需注意,和一个chaincode接口类型(即你要测试的链码)
stub:=shim.NewMockStub("1",new(SimpleChaincode))
//调用MockInit()方法测试初始化,返回一个response,
//response.Payload中包含链码执行成功响应的数据,response.Message包含链码执行失败返回的错误信息
//接收的参数均为类型为string的uuid(随便设置即可),以及一个二维byte数组(用于测试的提供参数)
re:=stub.MockInit("1",[][]byte{[]byte(""),[]byte("A"),[]byte("100"),[]byte("B"),[]byte("100")})
if len(re.Message)!=0 {
fmt.Println(re.Message)
}
}
//测试query方法
func TestSimpleChaincode_Invoke(t *testing.T) {
//每一个测试方法都是一个单独的模块,不能在上面的方法中测试了初始化,而对于这次的测试query方法就不 进行初始化,两个方法是没有关联的
//如果直接调用MockInvoke()取执行query操作,是无法查询到数据的
//需要先执行初始化方法(即对数据的添加),然后在执行query操作(仅针对于本例操作讲解,在往后链码的 编写中,若无需进行初始化操作,无需执行初始化
//但是要测试一个查询或者删除,要在用一个方法内,先执行新增的操作,然后再去进行查询或删除操作)
stub:=shim.NewMockStub("1",new(SimpleChaincode))
re:=stub.MockInit("1",[][]byte{[]byte(""),[]byte("A"),[]byte("100"),[]byte("B"),[]byte("100")})
if len(re.Message)!=0 {
fmt.Println(re.Message)
}
re2:=stub.MockInvoke("2",[][]byte{[]byte("query"),[]byte("A")})
if len(re2.Message)!=0 {
fmt.Println(re.Message)
}
fmt.Println(re2.Payload)
}
测试结果:
dev测试
【要在搭建好fabric网络环境的基础下】
该链码位于fabric-sample/chaincode/chaincode_example02中,可启动dev网络进行测试
1.启动网络
$ cd ./fabric-samples/chaincode-docker-devmode/
#启动网络
$ docker-compose -f docker-compose-simple.yaml up -d
2.进入链码容器,对链码进行编译
$ docker exec -it chaincode bash
# cd chaincode_example02/go/
# go build
# CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=test:0 ./go
3.打开一个新的终端,进入 cli 容器,安装并示例化链码
$ docker exec -it cli bash
# peer chaincode install -p chaincodedev/chaincode/chaincode_example02/go -n test -v 0
# peer chaincode instantiate -n test -v 0 -c '{"Args":["init","a", "100", "b","200"]}' -C myc
4.查询账户 a 的余额,返回结果为 100
# peer chaincode query -n test -c '{"Args":["query","a"]}' -C myc
5.从账户 a 转账 10 给 b
# peer chaincode invoke -n test -c '{"Args":["invoke","a","b","10"]}' -C myc
6.再次查询账户 b 的余额,返回结果为 90
# peer chaincode query -n test -c '{"Args":["query","a"]}' -C myc
可以在 chaincode 容器中查看到运行的日志:
ex02 Init
Aval = 100, Bval = 200
ex02 Invoke
Query Response:{"Name":"a","Amount":"100"}
ex02 Invoke
Aval = 90, Bval = 210
ex02 Invoke
Query Response:{"Name":"a","Amount":"90"}
7.关闭网络
$ docker-compose -f docker-compose-simple.yaml down
**5.从账户 a 转账 10 给 b**
```shell
# peer chaincode invoke -n test -c '{"Args":["invoke","a","b","10"]}' -C myc
6.再次查询账户 b 的余额,返回结果为 90
# peer chaincode query -n test -c '{"Args":["query","a"]}' -C myc
可以在 chaincode 容器中查看到运行的日志:
ex02 Init
Aval = 100, Bval = 200
ex02 Invoke
Query Response:{"Name":"a","Amount":"100"}
ex02 Invoke
Aval = 90, Bval = 210
ex02 Invoke
Query Response:{"Name":"a","Amount":"90"}
7.关闭网络
$ docker-compose -f docker-compose-simple.yaml down
关于dev更多详情及部署参考上方链接或文件中的链码开发dev模式.docx