1、本篇背景
前面已经对Hyperledger Fabric的链码ChainCode开发作了详细的介绍,其中导入了链码的shim包。
shim包在链码开发中很重要,其提供了一些API,便于链码和底层区块链网络交互来访问状态变量、交易上下文、调用方证书和属性,并调用其他链码和执行其他操作。
本篇主要对链码中shim包的开发API,最后以官方提供的"fabcar"为案例讲解。
官方shim相关API文档如下:
https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim
先来回顾下链码开发介绍中简单的"资产"管理链码的源代码:
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
)
// SimpleAsset实现简单的链式代码来管理资产
type SimpleAsset struct {
}
// 在链码初始化过程中调用Init来初始化任何数据。
// 请注意,链码升级也会调用此函数来重置或迁移数据。
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
// 从交易提案中获取参数
args := stub.GetStringArgs()
if len(args) != 2 {
return shim.Error("Incorrect arguments. Expecting a key and a value")
}
// 在这里,通过调用stub.PutState()来设置任何变量或者资产
// 我们在账本中存储key和value
err := stub.PutState(args[0], []byte(args[1]))
if err != nil {
return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0]))
}
return shim.Success(nil)
}
// 在链码每个事务中,Invoke会被调用。
// 每个事务都是由Init函数创建的资产,要么是'get'要么是'set'。
// Set方法可以通过指定新的键值对来创建新的资产。
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
// 从交易提案中提取函数和参数
fn, args := stub.GetFunctionAndParameters()
var result string
var err error
if fn == "set" {
result, err = set(stub, args)
} else { // fn为nil时,假设为get
result, err = get(stub, args)
}
if err != nil {
return shim.Error(err.Error())
}
// 将结果作为成功负载返回
return shim.Success([]byte(result))
}
// 设置在账本上存储的资产(包括key和value)
// 如果key存在,它将覆盖新值
func set(stub shim.ChaincodeStubInterface, args []string) (string, error) {
if len(args) != 2 {
return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value")
}
err := stub.PutState(args[0], []byte(args[1]))
if err != nil {
return "", fmt.Errorf("Failed to set asset: %s", args[0])
}
return args[1], nil
}
// 获取返回指定资产key的value值
func get(stub shim.ChaincodeStubInterface, args []string) (string, error) {
if len(args) != 1 {
return "", fmt.Errorf("Incorrect arguments. Expecting a key")
}
value, err := stub.GetState(args[0])
if err != nil {
return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err)
}
if value == nil {
return "", fmt.Errorf("Asset not found: %s", args[0])
}
return string(value), nil
}
// main函数在实例化时启动容器中的链码
func main() {
if err := shim.Start(new(SimpleAsset)); err != nil {
fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
}
}
如上所述,链码的Go代码实现需要定义一个struct(如其中的"SimpleAsset"),然后在该struct中实现Init和Invoke两个函数,最后定义一个main函数作为链码启动的入口。
之前介绍过每个链码程序都必须实现 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
}
Init和Invoke函数中都传入了一个参数,即"stub shim.ChaincodeStubInterface",这个参数提供的接口为编写链码的业务逻辑提供了一系列实用的API,以下开始讲解这些API。
2、Hyperledger Fabric数据存储系统介绍
介绍shim包链码API之前,先了解下Hyperledger Fabric的数据存储系统。
在Hyperledger Fabric中,存在三种类型的数据存储。
- 基于文件系统的区块链数据,跟比特币很像,比特币也是文件形式存储;
- 存储了交易提案的读写集和,里面以键值对形式存储链码操作业务数据的
State Database
(也叫World State
,中文名为世界状态),支持默认的LevelDB
和用户可选的CouchDB
) - 是对历史数据和区块链索引的数据库区块链式文件系统,用的是
LevelDB
。
![](https://img.haomeiwen.com/i5521305/f261a3d315172133.png)
(以上摘自"深蓝"的博客)
所以,Hyperledger Fabric常常会涉及到State DB,也就是常说的世界状态,后面也会涉及到相关内容(如账本状态State)。
3、shim包中的链码开发API
shim包中的链码开发API可分为四类:账本状态交互API、交易信息相关API、读取参数API和其他API。前面的简单"资产"管理链码也涉及到其中相关API,可以对照着来理解。
3.1 账本状态交互API
链码需要将一些数据记录在分布式账本中。需要记录的数据称为状态State,以键值对(key-value)的形式存储。其中key为字符串,value为二进制字节数组。账本状态API可以对账本状态进行操作,十分重要。方法的调用会更新交易提案的读写集和,在committer进行验证时会在此执行,跟账本状态进行比对,这类API的大致功能如下:
![](https://img.haomeiwen.com/i5521305/fb5b0277142f31dc.png)
3.2 交易信息相关API
交易信息相关API可以获取到与交易信息自身相关的数据。用户对链码的调用(初始化和升级时调用Init()方法,运行时调用Invoke()方法)过程中会产生交易提案。这些API支持查询当前交易提案的一些属性,具体信息如下:
![](https://img.haomeiwen.com/i5521305/3b8434b25e596fac.png)
3.3 参数读取API
调用链码时支持传入若干参数,参数可通过API读取。具体信息如下:
![](https://img.haomeiwen.com/i5521305/31b3fedf9cff59f3.png)
3.4 其他API
除了上面一些API以外还有一些辅助API,如下:
![](https://img.haomeiwen.com/i5521305/d39c05f4032c4c9e.png)
4、链码API案例详解
以下以官方提供的"fabcar"为案例讲解链码结构以及API。
4.1 链码结构解读
![](https://img.haomeiwen.com/i5521305/e9835214b8b79445.png)
以下是其中部分源代码,对英文注释翻译成中文,并新增了一些注释说明:
// 表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
package main
/*
* Go语言中导入包的语法为import("xxx"),import的作用是导入其他包
* 4个实用程序库,用于格式化,处理字节,读取和写入JSON以及字符串操作
* 2个特定的Hyperledger结构特定库,用于智能合同
*/
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"github.com/hyperledger/fabric/core/chaincode/shim"
sc "github.com/hyperledger/fabric/protos/peer"
)
// 定义智能合约结构
type SmartContract struct {
}
// 定义汽车结构,具有4个变量属性,结构标签由编码/json库使用
type Car struct {
Make string `json:"make"`
Model string `json:"model"`
Colour string `json:"colour"`
Owner string `json:"owner"`
}
/*
* 当智能合约“fabcar”由区块链网络实例化时,Init方法被调用
* 最佳做法是在单独的函数中进行任何Ledger初始化 - 请参见initLedger()
* (s *SmartContract)表示给SmartContract声明了一个方法
* Init和Invoke函数中都传入了一个参数,即"stub shim.ChaincodeStubInterface"
* 这个参数提供的接口为编写链码的业务逻辑提供了一系列实用的API
* 假设一切顺利,将返回一个表示初始化已经成功的sc.Response对象。
*/
func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
return shim.Success(nil)
}
/*
* Invoke方法由于运行智能合约“fabcar”的应用程序请求而被调用
* 调用应用程序还指定了具有参数的特定智能合约函数
* 通过使用GetFunctionAndParameters()方法来解析调用链码时传入的参数,从而判断需要调用的方法以及传入调用方法的参数
*/
func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
// 检索请求的智能合约函数和参数
// 方法完整描述为:func (stub *ChaincodeStub) GetFunctionAndParameters() (function string, params []string)
// 对调用链码时传入的字符串数组形式的参数作处理,也就是拆分为两部分
// 数组第一个参数为function,第二个到最后一个参数为args
function, args := APIstub.GetFunctionAndParameters()
// 找到适当的处理函数以便与账本进行交互
if function == "queryCar" {
return s.queryCar(APIstub, args)
} else if function == "initLedger" {
return s.initLedger(APIstub)
} else if function == "createCar" {
return s.createCar(APIstub, args)
} else if function == "queryAllCars" {
return s.queryAllCars(APIstub)
} else if function == "changeCarOwner" {
return s.changeCarOwner(APIstub, args)
}
return shim.Error("Invalid Smart Contract function name.")
}
// 考虑到内容有点多,后续的讲解会补充对应的内容,暂时省略了以下函数的实现
func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { ... }
func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response { ... }
func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { ... }
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response { ... }
func (s *SmartContract) changeCarOwner(APIstub shim.ChaincodeStubInterface, args []string) sc.Response { ... }
// 主要功能仅适用于单元测试模式,这里只包括完整性。
// Go语言的入口是main函数
func main() {
// 创建一个新的智能合约
err := shim.Start(new(SmartContract))
if err != nil {
fmt.Printf("Error creating new Smart Contract: %s", err)
}
}
5、对State DB的增删查改
State DB
:State Database
,状态数据库(也叫 World State
,中文名为世界状态)。
5.1 查询汽车
通过 GetState(key string) ([]byte, error)
来查询数据。因为我们是Key-Value数据库,所以根据Key来对数据库进行查询,是一件很常见,很高效的操作,返回的数据是byte数组。
func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
// 在Go语言中,下划线"_"是占位符,表示不需要返回这个值,直接忽略
carAsBytes, _ := APIstub.GetState(args[0])
return shim.Success(carAsBytes)
}
5.2 初始化账本
通过 PutState(key string, value []byte) error
增改数据,对于State DB来说,增加和修改数据是统一的操作,因为State DB是一个Key-Value数据库,如果我们指定的Key在数据库中已经存在,那么就是修改操作,如果Key不存在,那么就是插入操作。对于实际的系统来说,我们的Key可能是单据编号,或者系统分配的自增ID+实体类型作为前缀,而Value则是一个对象经过JSON序列号后的字符串。
func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response {
cars := []Car{
Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
}
i := 0
for i < len(cars) {
fmt.Println("i is ", i)
carAsBytes, _ := json.Marshal(cars[i])
APIstub.PutState("CAR"+strconv.Itoa(i), carAsBytes)
fmt.Println("Added", cars[i])
i = i + 1
}
return shim.Success(nil)
}
5.3 创建汽车
把对象转换为JSON的函数为 json.Marshal()
,也就是说,这个函数接收任意类型的数据,并转换为字节数组类型,返回值就是我们想要的JSON数据和一个错误码。下划线"_"表示不需要返回这个值,直接忽略错误码。
func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
// 传入参数的个数必须为5
if len(args) != 5 {
return shim.Error("Incorrect number of arguments. Expecting 5")
}
// 第一个参数作为Key,其他参数作为构造函数的参数
var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]}
carAsBytes, _ := json.Marshal(car)
APIstub.PutState(args[0], carAsBytes)
return shim.Success(nil)
}
5.4 查询所有汽车
Key区间查询 GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)
提供了对某个区间的Key进行查询的接口,适用于任何State DB。由于返回的是一个StateQueryIteratorInterface(迭代器)接口,我们需要通过这个接口再做一个for循环,才能读取返回的信息,所有我们可以独立出一个方法,专门将该接口返回的数据以string的byte数组形式返回。
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {
startKey := "CAR0"
endKey := "CAR999"
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
if err != nil {
return shim.Error(err.Error())
}
// defer关键字用来标记最后执行的Go语句,一般用在资源释放、关闭连接等操作,会在函数关闭前调用。
// 多个defer的定义与执行类似于栈的操作:先进后出,最先定义的最后执行。
defer resultsIterator.Close()
// buffer 是一个包含查询结果的JSON数组,bytes.buffer是一个缓冲byte类型的缓冲器存放着都是byte,这样直接定义一个 Buffer 变量,而不用初始化。
var buffer bytes.Buffer
buffer.WriteString("[")
bArrayMemberAlreadyWritten := false
// 迭代器的两个方法,hasNext:没有指针下移操作,只是判断是否存在下一个元素。next:指针下移,返回该指针所指向的元素。
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
// 在数组成员之前添加逗号,为第一个数组成员禁用它
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")
buffer.WriteString(", \"Record\":")
// Record是一个JSON对象,所以我们按原样写入
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
fmt.Printf("- queryAllCars:\n%s\n", buffer.String())
return shim.Success(buffer.Bytes())
}
5.5 改变汽车拥有者
Unmarshal是用于反序列化json的函数根据data将数据反序列化到传入的对象中。
func (s *SmartContract) changeCarOwner(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}
carAsBytes, _ := APIstub.GetState(args[0])
car := Car{}
json.Unmarshal(carAsBytes, &car)
car.Owner = args[1]
carAsBytes, _ = json.Marshal(car)
APIstub.PutState(args[0], carAsBytes)
return shim.Success(nil)
}
6、开发环境编译运行链码前提
请安装Hyperledger Fabric Samples和Docker镜像,如果已经安装好并配置好Docker环境,可以继续后面的操作。
进入到 fabric-samples
下的 chaincode-docker-devmode
目录下。
cd chaincode-docker-devmode
现在打开三个终端并进入到您的 chaincode-docker-devmode
目录下。
7、开发环境编译和测试链码
7.1 终端1 - 开启网络
docker-compose -f docker-compose-simple.yaml up
以上命令使用 SingleSampleMSPSole
配置启动 orderer并且以"开发模式"启动节点peer。它还启动了两个额外的容器,一个用于链码环境,一个用于与链码交互的CLI。创建和加入通道的命令被嵌入到CLI容器中, 因此我们可以立即跳转到链码的调用。
如果报错了,试着清除Docker容器,依次执行以下命令
// 删除所有活跃的容器
docker rm -f $(docker ps -aq)
// 清理网络缓存
docker network prune
![](https://img.haomeiwen.com/i5521305/b73b2e45986e0782.png)
7.2 终端2 - 编译和运行链码
docker exec -it chaincode bash
您应该看到类似以下内容:
root@d2629980e76b:/opt/gopath/src/chaincode#
然后,编译您的链码:
cd fabcar/go
go build -o fabcar
运行您的链码:
CORE_PEER_ADDRESS=peer:7051 CORE_CHAINCODE_ID_NAME=mycc:0 ./fabcar
链码随着peer节点启动并且在peer节点成功注册后链码日志会开始显示。请注意,在此阶段,链码与任何通道都没有关联。这个会在后续步骤中使用实例化命令完成。
![](https://img.haomeiwen.com/i5521305/328fc06fa28c3736.png)
7.3 终端3 - 使用链码
首先,启动利用Docker CLI容器:
docker exec -it cli bash
1、安装链码:
peer chaincode install -p chaincodedev/chaincode/fabcar/go -n mycc -v 0
#####最终输出如下(省略了很多内容)#####
2018-06-13 10:49:10.910 UTC [chaincodeCmd] install -> DEBU 00f Installed remotely response:<status:200 payload:"OK" >
2、实例化链码:
peer chaincode instantiate -n mycc -v 0 -c '{"Args":[]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 10:51:18.760 UTC [msp/identity] Sign -> DEBU 0a4 Sign: digest: 189AC2DE682A98749B8C76FE672DC5A325A46B6A3FB1AE2569E424A7D3566A35
2018-06-13 10:51:18.765 UTC [main] main -> INFO 0a5 Exiting.....
3、初始化账本:
peer chaincode invoke -n mycc -c '{"Args":["initLedger"]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 10:52:20.701 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0a6 Chaincode invoke successful. result: status:200
4、查询所有汽车:
peer chaincode invoke -n mycc -c '{"Args":["queryAllCars"]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 10:55:47.505 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0a6 Chaincode invoke successful. result: status:200 payload:"[{\"Key\":\"CAR0\", \"Record\":{\"make\":\"Toyota\",\"model\":\"Prius\",\"colour\":\"blue\",\"owner\":\"Tomoko\"}},{\"Key\":\"CAR1\", \"Record\":{\"make\":\"Ford\",\"model\":\"Mustang\",\"colour\":\"red\",\"owner\":\"Brad\"}},{\"Key\":\"CAR2\", \"Record\":{\"make\":\"Hyundai\",\"model\":\"Tucson\",\"colour\":\"green\",\"owner\":\"Jin Soo\"}},{\"Key\":\"CAR3\", \"Record\":{\"make\":\"Volkswagen\",\"model\":\"Passat\",\"colour\":\"yellow\",\"owner\":\"Max\"}},{\"Key\":\"CAR4\", \"Record\":{\"make\":\"Tesla\",\"model\":\"S\",\"colour\":\"black\",\"owner\":\"Adriana\"}},{\"Key\":\"CAR5\", \"Record\":{\"make\":\"Peugeot\",\"model\":\"205\",\"colour\":\"purple\",\"owner\":\"Michel\"}},{\"Key\":\"CAR6\", \"Record\":{\"make\":\"Chery\",\"model\":\"S22L\",\"colour\":\"white\",\"owner\":\"Aarav\"}},{\"Key\":\"CAR7\", \"Record\":{\"make\":\"Fiat\",\"model\":\"Punto\",\"colour\":\"violet\",\"owner\":\"Pari\"}},{\"Key\":\"CAR8\", \"Record\":{\"make\":\"Tata\",\"model\":\"Nano\",\"colour\":\"indigo\",\"owner\":\"Valeria\"}},{\"Key\":\"CAR9\", \"Record\":{\"make\":\"Holden\",\"model\":\"Barina\",\"colour\":\"brown\",\"owner\":\"Shotaro\"}}]"
![](https://img.haomeiwen.com/i5521305/7df2bf9e32f82d79.png)
5、查询某辆汽车:
peer chaincode invoke -n mycc -c '{"Args":["queryCar","CAR9"]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 10:59:20.621 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0a6 Chaincode invoke successful. result: status:200 payload:"{\"make\":\"Holden\",\"model\":\"Barina\",\"colour\":\"brown\",\"owner\":\"Shotaro\"}"
6、创建一辆汽车:
peer chaincode invoke -n mycc -c '{"Args":["createCar","CAR10","Tesla","Model S","red","Wenzil"]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 11:00:06.422 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0a6 Chaincode invoke successful. result: status:200
7、查询创建的汽车:
peer chaincode invoke -n mycc -c '{"Args":["queryCar","CAR10"]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 11:00:56.791 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0a6 Chaincode invoke successful. result: status:200 payload:"{\"make\":\"Tesla\",\"model\":\"Model S\",\"colour\":\"red\",\"owner\":\"Wenzil\"}"
8、改变汽车拥有者:
peer chaincode invoke -n mycc -c '{"Args":["changeCarOwner","CAR10","Elon Musk"]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 11:02:15.317 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> DEBU 0a5 ESCC invoke result: version:1 response:<status:200 message:"OK" > payload:"......"
2018-06-13 11:02:15.318 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0a6 Chaincode invoke successful. result: status:200
9、查询改变拥有者后的汽车:
peer chaincode invoke -n mycc -c '{"Args":["queryCar","CAR10"]}' -C myc
#####最终输出如下(省略了很多内容)#####
2018-06-13 11:03:35.509 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 0a6 Chaincode invoke successful. result: status:200 payload:"{\"make\":\"Tesla\",\"model\":\"Model S\",\"colour\":\"red\",\"owner\":\"Elon Musk\"}"
从链码API的介绍到链码的结构讲解,再到在开发环境编译、安装以及调用链码等一系列流程算是走完了。
文章大部分内容来自如下博客,稍微作了下整理,添加了 编译和运行链码
相关内容,如有侵权联系我删除文章:
搭建基于hyperledger fabric的联盟社区(四) --chaincode开发
PS:刚入坑的小白,很多不懂,还请各位大佬多赐教,谢谢!
网友评论