链码的编写
前言: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 {
}
func (s*SimpleChaincode)Init(stub shim.ChaincodeStubInterface)pb.Response {
return shim.Success(nil)
}
func (s *SimpleChaincode)Invoke(stub shim.ChaincodeStubInterface)pb.Response {
return shim.Success(nil)
}
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)
PutState(key string, value []byte) error
DelState(key string) error
GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)
GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)
CreateCompositeKey(objectType string, attributes []string) (string, error)
SplitCompositeKey(compositeKey string) (string, []string, error)
GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)
GetQueryResult(query string) (StateQueryIteratorInterface, error)
注意: 通过 put 写入的数据状态不能立刻 get 到,因为 put 只是链码执行的模拟交易(防止重复提交攻击),并不会真正将状态保存到账本中,必须经过 Orderer 达成共识之后,将数据状态保存在区块中,然后保存在各 peer 节点的账本中。
2.3 交易信息相关 API[了解即可]
GetTxID() string
GetChannelID() string
GetTxTimestamp() (*timestamp.Timestamp, error)
GetBinding() ([]byte, error)
GetSignedProposal() (*pb.SignedProposal, error)
GetCreator() ([]byte, error)
GetTransient() (map[string][]byte, error)
GetTransient()在私有数据的编写会用到
2.4 对 PrivateData [私有数据]操作的 API
2.5 其他 API
SetEvent(name string, payload []byte) error
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 {
fmt.Println("ex02 Init")
_, args := stub.GetFunctionAndParameters()
var A, B string
var Aval, Bval int
var err error
if len(args) != 4 {
return shim.Error("Incorrect number of arguments. Expecting 4")
}
A = args[0]
Aval, err = strconv.Atoi(args[1])
if err != nil {
return shim.Error("Expecting integer value for asset holding")
}
B = args[2]
Bval, err = strconv.Atoi(args[3])
if err != nil {
return shim.Error("Expecting integer value for asset holding")
}
fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)
err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
if err != nil {
return shim.Error(err.Error())
}
err = stub.PutState(B, []byte(strconv.Itoa(Bval)))
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(nil)
}
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("ex02 Invoke")
function, args := stub.GetFunctionAndParameters()
if function == "invoke" {
return t.invoke(stub, args)
} else if function == "delete" {
return t.delete(stub, args)
} else if function == "query" {
return t.query(stub, args)
}
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
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]
B = args[1]
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))
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, 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)
err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
if err != nil {
return shim.Error(err.Error())
}
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"
)
func TestSmartContract_Init(t *testing.T) {
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)
}
}
func TestSimpleChaincode_Invoke(t *testing.T) {
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
3.打开一个新的终端,进入 cli 容器,安装并示例化链码
$ docker exec -it cli bash
4.查询账户 a 的余额,返回结果为 100
5.从账户 a 转账 10 给 b
6.再次查询账户 b 的余额,返回结果为 90
可以在 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
可以在 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
|