智能合约 #
1.什么是链码 #
链码是程序,用Go,Node.js,Java其中一种语言编写的,提供分布式账本的状态处理逻辑。链码运行在Peer的独立进程中,负责初始化账本,管理账本状态。
链码能够独立运行在具有安全特性的受保护的Docker容器中,以gRPC协议与相应的Peer节点进行通信,并操作(初始化或管理)分布式账本中的数据。
链码通常用来处理网络成员同意的逻辑事务,所以它也被称为“智能合约”。可以调用链码更新或者查询交易。如果有合适的权限,两码可以调用另一个链码,无论是否在一个channel中,获取账本状态。
注意如果被调用的链码和链码处于不同的channel中,只有读权限。也就是说被调用链码只有读功能,不参与后续事务的验证和检查。
在hyperledger fabric中链码一般分为系统链码和用户链码。
(1)系统链码
系统链码负责fabric节点自身的处理逻辑,包括系统配置、背书、校验等工作。hypgeledger fabric 系统链码仅支持go语言,在peer节点启动时会自动完成注册和部署。
- 配置系统链码
- 生命周期系统链码
- 查询系统链码
- 背书管理系统链码
- 验证系统链码
(2)用户链码
开发人员编写的基于区块链分布式账本状态的业务处理逻辑代码运行在链码容器中。通过hyperledger fabric 提供的接口与账本状态进行交互。
生命周期 #
- install:安装在指定的Peer节点中。
- instantiate:进行实例化 //过时了
- upgrade:链码升级
- package:对链码进行打包
- singnpackage:对打包的文件进行签名
链码安装在一个节点中还是安装在多个节点中?有什么区别?
在实际生产环境中,必须在应用通道上每一个要运行链码的背书节点上安装链码,其他未安装链码的节点不能执行链码,但仍可以验证交易并提交到账本中。
链码执行查询与执行事务的流程相同吗?
不同,执行查询操作,则客户端接收到背书的交易提案响应后不会再将交易请求提交给Orderer节点。
背书策略具体指的是什么?
背书策略是一种在实例化链码时指定由当前通道中的那些成员节点进行背书签名的策略。
如果在实例化链码时没有指定背书策略,那么会有节点进行背书吗?
会,默认的背书策略时MSP标识DEFAULT成员的签名
CORE_PEER_ADDRESS=peer:7052中的7052端口指的是什么,为什么不是7051?
7052是用于指定链码的专用监听地址及端口号,而7051是peer节点监听的网络端口。
2.初始整理 #
首先创建一个存放链码的目录,我们使用以下命令在GOPATH
下创建一个目录
cd $GOPATH
mkdir chaincode
cd chaincode
接着使用以下命令初始化这个项目并创建一个go文件
go mod init chaincode
touch sacc.go
链代码的包名的必须是main
package main
必须要引入的包shim
和peer
,用于客户端与Fabric框架通信
import (
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-protos-go/peer"
)
自定义一个结构体, 基于这个结构体实现一些接口函数
/*
每个ChainCode都需要定义一个结构体,结构体的名字可以是任意符合Golang命名规范的字符串。
*/
// 自定义结构体名为: chainCodeStudy
type TestStudy struct {
}
/*
Chaincode结构体是ChainCode的主体结构。ChainCode结构体需要实现Fabric提供的接口:
"github.com/hyperledger/fabric/protos/peer",其中必须实现下面两个方法:
*/
// 系统初始化
func (t *TestStudy) Init(stub shim.ChaincodeStubInterface) pb.Response {};
//Init:在链码实例化或升级时被调用,完成初始化数据的工作
// 数据写入
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response{};
//Invoke:在更新或查询提案事务中的分类账本数据状态时被调用,因此响应调用或查询的业务实现逻辑都需要在此函数中编写实现。
链码 API 查询
https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim
shim包为链码提供了用来访问/操作数据状态、事务上下文和调用其他链代码的相关API。shim包提供了链码与账本交互的中间层。
链码通过shim.ChaincodeStub提供的相应函数来读取和修改账本的状态。
peer包提供了链码执行后的响应信息。链码被调用执行之后通过peer包中的Response来封装执行结果的响应信息。
3.初始化链码 #
接下来实现Init函数
// Init is called during chaincode instantiation to initialize any data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {
}
注意:链码升级也会调用Init
函数,当我们升级现有链码时,务必要确保Init
是否需要修改。可以提供一个空的Init
函数如果没有需要迁移的数据或者初始化的部分。
下一步,我们使用ChaincodeStubInterface.GetStringArgs来找到参数并检验。在这里我们需要一个键值对
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")
}
}
我们已经验证了参数,接下来将数据保存到账本中。key-value的形式向 ChaincodeStubInterface.PutState 中传值。如果进展顺利,返回一个peer.Response
对象来表明初始化成功
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")
}
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)
}
Init方法是系统初始化方法。当执行命令
peer chaincode instantiate
实例化chaincode时候会调用该方法,同时命令中-c
选项后面内容会作为参数传入Init
方法中。以下面的chaincode实例化命令为例:
$ 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!!!"))
};
这个函数可以不写内容
实际应用中:
func (this *CableChainCode) Init(stub shim.ChaincodeStubInterface) peer.Response {
fmt.Println(" ==== Cable_trace Init ====")
fmt.Println("000000000000000000")
return shim.Success(nil)
}
4.调用链码 #
Invoke方法的主要作用是写入数据,比如发起交易等。在执行命令
peer chaincode invoke
的时候系统会调用该方法,同时会把命令中-c
后面的参数传入Invoke
方法中,以下面的Invoke命令为例:
$ 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!!!"))
};
首先,增加Invoke
函数
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
}
在Init中,我们需要从ChaincodeStubInterface获取准确的参数。Invoke的参数会成为链码中函数的名字。这里我们只定义两个内部的函数set和get,用来设置资产和查询资产。我们使用ChaincodeStubInterface.GetFunctionAndParameters来获取函数名和函数的参数。
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
fn, args := stub.GetFunctionAndParameters()
}
接下来我们需要验证函数是set或get,并调用这些内部函数,返回一个合适的值(shim.Success
或shim.Error,会被序列化成gRPC数据
)
func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
// Extract the function and args from the transaction proposal
fn, args := stub.GetFunctionAndParameters()
var result string
var err error
if fn == "set" {
result, err = set(stub, args)
} else {
result, err = get(stub, args)
}
if err != nil {
return shim.Error(err.Error())
}
return shim.Success([]byte(result))
}
5.实现链码内的函数 #
如前文所述,我们需要定义两个被链码调用的函数。注意我们之前提到的,管理账本装态需要ChaincodeStubInterface.PutState和ChaincodeStubInterface.GetState
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
}
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
}
6.完整代码 #
最后需要增加main函数,用来调用 shim.Start函数。完整代码展示
package main
import (
"fmt"
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-protos-go/peer"
)
type SimpleAsset struct {
}
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")
}
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)
}
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 {
result, err = get(stub, args)
}
if err != nil {
return shim.Error(err.Error())
}
return shim.Success([]byte(result))
}
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
}
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
}
func main() {
if err := shim.Start(new(SimpleAsset)); err != nil {
fmt.Printf("Error starting SimpleAsset chaincode: %s", err)
}
}
智能合约API #
1.chaincodestubinterface
接口中常用方法:
#
在Init
和Invoke
方法中,都有一个stub
参数,通过这个参数可以做很多操作,例如读取数据、写入数据、查看提案等。
接口在ChaincodeStubInterface中定义。
1.GetFunctionAndParameters() (string, []string)
传入参数通过stub.GetFunctionAndParameters()
获取,得到的是一个数组,记录了所有传入参数。
返回调用链码时在交易提案中指定提供的被调用函数名称及其参数列表。
示例:
func (t *Test) Init(stub shim.ChaincodeStubInterface) pb.Response {
function, args := stub.GetFunctionAndParameters()
..
if len(args) != 4 {
return shim.Error("Incorrect number of arguments. Expecting 4")
}
// Initialize the chaincode
A = args[0]
...
}
2.PutState(key string, value []byte) error
使用stub.PutState()
方法以key-value
的方式将数据写入账本。
示例:
err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
if err != nil {
return shim.Error(err.Error())
}
3. GetState(key string) ([]byte, error)
使用stub.GetState()
方法查询区块。
示例:
Avalbytes, err := stub.GetState(A)
if err != nil {
jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}"
return shim.Error(jsonResp)
}
4.DelState(key string) error
使用stub.DelState()
方法从状态库中删除指定的状态变量键。
示例:
// 删除A键 所对应值的数据
err := stub.DelState("A")
if err != nil{
fmt.Println(err)
}
5.GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)
stub.GetStateByRange()
方法返回一个账本状态键的迭代器,可用来 遍历在起始键和结束键之间的所有状态键,返回结果按词典顺序排列。
示例:
// 更新状态数据库
func (t *Test)set(stub shim.ChaincodeStubInterface, args []string) pb.Response {
// 一般情况下是先序列化然后存入状态数据库,为了简便,我们直接存进去
stub.PutState("1", []byte("cat"))
stub.PutState("2", []byte("boy"))
stub.PutState("3", []byte("girl"))
stub.PutState("4", []byte("child"))
stub.PutState("5", []byte("odog"))
return shim.Success(nil)
}
// 获取指定范围状态数据
func (t *Test)get(stub shim.ChaincodeStubInterface, args []string) pb.Response {
if len(args) != 2 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
A := args[0]
B := args[1]
keysIter, err := stub.GetStateByRange(A, B)
if err != nil{
return shim.Error(err.Error())
}
// 初始化
rsp := make(map[string]string)
for keysIter.HasNext(){
response, interErr := keysIter.Next()
if interErr != nil{
return shim.Error(interErr.Error())
}
// 赋值
rsp[response.Key] = string(response.Value)
// 打印
fmt.Println(response.Key, string(response.Value))
}
// 将获取的数据序列化
jsonRsp, err := json.Marshal(rsp)
if err != nil{
return shim.Error(err.Error())
}
return shim.Success(jsonRsp)
}
6. GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)
stub.GetHistoryForKey()
方法返回指定状态键的值历史修改记录。返回的记录包括交易的编号、修改的值、当前key的有没有被删除,交易发生的时间戳。时间戳取自交易提议头。
**注:**该方法需要通过peer节点配置中的如下选项开启:
core.ledger.history.enableHistoryDatabase = true
示例:
func (t *Test)history(stub shim.ChaincodeStubInterface, args []string) pb.Response
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
A := args[0]
keyInter, err := stub.GetHistoryForKey(A)
if err != nil{
return shim.Error(err.Error())
}
for keyInter.HasNext(){
response, interErr := keyInter.Next()
if interErr != nil{
return shim.Error(interErr.Error())
}
txid := response.TxId // 交易编号
txvalue := response.Value // 修改的值
txstatus := response.IsDelete // 当前值有没有被删除
txtimestamp := response.Timestamp // 交易发生的时间戳
tm := time.Unix(txtimestamp.Seconds, 0)
timeString := tm.Format("2006-01-02 03:04:05 PM") // 转换为标准时间格式
fmt.Println(txid, string(txvalue), txstatus, timeString)
}
return shim.Success(nil)
}
7. CreateCompositeKey(objectType string, attributes []string) (string, error)
创建一个复合键
// 给定一组属性,将这些属性组合起来构造一个复合键
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: testkeygo1go2go3go4go5go6
fmt.Println(ckey)
return shim.Success([]byte(ckey))
}
8. SplitCompositeKey(compositeKey string) (string, []string, error)
对指定的复合键进行分割
// 根据局部的复合键返回所有的匹配的键值
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 []string
for 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, key)
value2 := compositeKeyParts[0]
value3 := compositeKeyParts[1]
// print: find value v1:testkey, v2:go1, v3:go2
fmt.Printf("find value v1:%s, v2:%s, V3:%s\n", value1, value2, value3)
}
return shim.Success("success")
}
2.其他方法 #
1.func Success(payload []byte) pb.Response
使用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!!!"))
};
2.func Error(msg string) pb.Response
使用shim.Error()
将失败结果返回调用者。
// Error方法负责将错误信息返回给调用ChainCode的客户端, Error方法的定义和调用如下
// 方法定义
func Error(msg string) pb.Response;
// 示例代码
func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
return shim.Error("operation fail!!!")
};xxxxxxxxxx // Error方法负责将错误信息返回给调用ChainCode的客户端, Error方法的定义和调用如下// 方法定义func Error(msg string) pb.Response;// 示例代码func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response { return shim.Error("operation fail!!!")};func Error(msg string) pb.Response { return pb.Response{ Status: ERROR, Message: msg, }}
3.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!!!"))
};
-
交易管理相关的方法
-
// 获取当前客户端发送的交易时间戳 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("2018-11-11 23:23:32")))) }
-
调用其他chaincode的方法
// 调用另一个链码中的Invoke方法 func InvokeChaincode(chaincodeName string,args [][]byte,channel string) pb.Response // 示例代码 func (t *TestStudy) Invoke(stub shim.ChaincodeStubInterface) pb.Response { // 设置参数, a向b转转11 trans:=[][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("11")} // 调用chaincode response := stub.InvokeChaincode("mycc", trans, "mychannel") // 判断是否操作成功了 // 课查询: https://godoc.org/github.com/hyperledger/fabric/protos/peer#Response if 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)) }