fabric链码开发中API的作用不言而喻。为了理解如何使用这些API,本文将从fabric源码给出的几个例子着手,意图讲清楚这些API的用法。

fabric链码API

​ fabric的链码必须实现以下Chaincode 接口:

1
2
3
4
type Chaincode interface {
Init(stub ChaincodeStubInterface) pb.Response
Invoke(stub ChaincodeStubInterface) pb.Response
}

​ 可以看到,此接口的两个方法都以ChaincodeStubInterface为参数。事实上,stub为链码提供了丰富的API,功能包括对账本进行操作,读取交易参数,调用其他链码等。

​ 实际上,shim.ChaincodeStubInterface提供了一系列API,可以分为四类: 账本状态交互API、交易信息相关API、参数读取API、其他API。

账本状态交互API

​ 众所周知,fabric记录的数据,我们称之为状态(state),这些纸以键值对(key-value)的形式存储。而账本状态交互API可以对账本状态进行操作,相应的,方法会更新交易提案的读写集。

API 方法格式 说明
GetState GetState(key string) ([]byte, error) 负责查询账本,返回指定键对应的值
PutState PutState(key string, value []byte) error 尝试在账本中添加或更新一对键值。 这一对键值会被添 加到写集合中,等待 Committer进一步的验证,验证通 过后会真正写入到账本
GetStateByRange GetStateByRange(startKey, endKey string) (StateQuerylteratorlnterface,error) 查询指定范围内的键值, startKey、 endKey 分别 指定起始(包括)和终止(不包括),当为空时默认是最大范围 。 返回结果是一个迭代器 StateQuerylteratorlnterface结构, 可以按照字典序迭代每个键值对, 最后需调用 Close()方 法关闭
GetStateByPartialCompositeKey GetStateByPartialCompositeKey (objectType string, keys []string) (StateQuerylteratorlnterface, error) 根据局部的复合键(前缀)返回所有匹配的键值。 返回结 果也是一个迭代器StateQuerylteratorlnterface结构,可以按照字典序迭代每个键值对,最后需调用 C lose()方法关闭
GetHistoryForKey GetHistoryForKey(key string) (History Querylteratorlnterface, error) 返回某个键的所有历史值 。 需要在节点配置中打开历史数据库特性 (Iedger.history.enableHistoryDatabase= true)
GetQueryResult GetQueryResult(query string) (State Querylteratorlnterface, error) 对(支持富查询功能的)状态数据库进行富查询( rich query)。 返回结果为迭代器结构 StateQueryIteratorInterface。 注意该方法不会被 Committer重新执行进行验证,因此, 不应该用于更新账本状态的交易中 。 目前仅有 CouchDB 类型的状态数据库支持富查询

交易信息相关API

此类API可以获取到与交易自身相关的数据。用户对链码的调用会产生一系列的交易提案,这些API支持查询当前交易提案的一些属性。

API 方法格式 说明
GetTxID GetTxID() string 该方法返回交易提案中指定的交易 ID。一般情况下, 交易 ID 是在客户端生成提案时候产生的数字摘要,由 Nonce 随机串和签名者身份信息,一起进行 SHA256 哈希运行生成
GetTxTimestamp GetTxTimestamp() (*timestamp. Timestamp, error) 返回交易被创建时的客户端打上的时间戳 。 这个时间戳 是 直接从交易 ChannelHeader中提取的,所以在所有背书节点 (endorsers)处看到的值都相同
GetBinding GetBinding() ([]byte, eπor) 返回交易的 binding信息。
注意:交易的 binding 信息是将交易提案的 nonse、 Creator、 epoch 等信息组合起来,再进行晗希得到的数字摘要
GetSignecjProposal GetSignedProposal()(*pb.SignedProposal, eηor) 返回该 stub的 SignedProposal结构,包括了跟交易提案相 关的所有数据
GetCreator GetCreator() ([]byte, eπor) 返回该交易的提交者的身份信息,从 signedProposal 中的 SignatureHeader.Creator 提取
GetTransient GetTransient()(map[string][]byte, error) 返回交易中带有的 一 些临时信息,从 ChaincodeProposalPayload.transient 域提取,可以存放一 些应用相关的保密信息,这些信息不会被写到账本中

参数读取API

调用链码事支持传入若干参数,参数可通过API读取。

API 方法格式 说明
GetArgs GetArgs() [][]byte 提取调用链码时交易 Proposal 中指定的参数,以字节串 (ByteArray)数组形式返回。 可以在Init或Invoke方法中使用。 这些参数从 ChaincodeSpec结构中的 Input域直接提取
GetArgsSlice GetArgsSlice() ([]byte, error) 提取调用链码时交易 Proposal 中指定的参数,以字节串形式返回
GetFunctionAndParameters GetFunctionAndParameters()(string, []string) 这是链码开发者和用户约定俗成的习惯,即在 Init/Invoke方法中编写实现若干子函数,用户调用时以第一个参数作为函数名 , 链码中的代码根据函数名称可以仅执行对应的分支处理逻辑
GetStringArgs GetStringArgs() []string 提取调用链码时交易 Proposal 中指定的参数,以 字 符串 (String) 数组形式返回

其他API

上面是常用的API,同时还有一些辅助API

API 方法格式 说明
CreateCompositeKey CreateCompositeKey(objectType string, attributes []string) (string, error) 给定一组属性( attributes),该 API 将这些属性组合起来构造返回一个复合键 。 返回的复合键可以被 PutState 等方法使用 。 objectType 和 attributes 只允许合法的 utf8 字符串,并且不能包含 U+OOOO 和 U+10FFFF
SplitCompositeKey SplitCompositeKey(compositeKey string) (string, []string, error) 该方法与 CreateCompositeKey方法相对,给定一个复合键,将其拆分为构造复合键时所用的属性
InvokeChaincode InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response 调用另一个链码中的 Invoke方法,如果被调用链码在同一个通道内,则添加其读写集合信息到调用交易; 否则执行调用但不影响读写集合信息 。 如果 channel 为空 ,则默认为当前通道。 目前仅限于读操作,同时不会生成新的交易
SetEvent SetEvent(name string, payload []byte) error 设定当这个交易在 Committer处被认证通过, 写入到 区块时发送的事件( event)

应用开发案例

上述API并不能代表现在的所有API,但是是常用的API应该已经覆盖到了,所以接下来,将在案例中体验这些API

案例一: 转账

fabric1.4版本中自带一些完整的链码,本节将以Go语言的典型链码chaincode_example02进行学习。

此链码代码位置在examples/chaincode/go/examples02/chaincode.go

1. 链码结构

链码的基本结构如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package example02

//导入必要的包
import (
……
)
//声明链码结构体
type SimpleChaincode struct {
}
//实现Init方法
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {
……
}
//实现Invoke方法
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
……
}

具体的实现中,Invoke方法又存在不同的分支,如下所示:

方法 分支方法 功能 参数示例
Init 添加两个实体和初始余额 [“init”,”a”,”100”,”b”,”200”]
query 查询一个实体的余额 [“query”,”a”]
Invoke Invoke 一个实体向另一个实体转账 [“invoke”,”a”,”b”,”50”]
Delete 删除一个实体 [“delete”,”b”]

2. Init方法

Init方法中,首先通过stub的GetFunctionAndPaeameters()方法提取本次调用的交易中所指定的参数:

1
_, args := stub.GetFunctionAndParameters()

GetFunctionAndParameters()的返回值类型为:function string, params []string,其中第一个function为交易参数中的第一个参数,params为从第二个参数开始的所有参数。例如,当实例化链码时指定参数{“Args”:["init","a","100","b","200"]}。由于我们的案例不需要第一个参数,所以用下划线忽略。

接下来检查args的参数的数量,其中值得说的是,shim.Error(),此函数将创建并返回一个状态为ERROR的Response消息。

1
2
3
if len(args) != 4 {
return shim.Error("Incorrect number of arguments. Expecting 4")
}

接下来的代码:分别设置两个实体A,B,然后给相应的实体赋值基本余额数目。

1
2
3
4
5
6
7
8
9
10
11
// Initialize the chaincode
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")
}

接下来,是最为关键的部分,将必要的状态值记录到分布式账本中。stub的PutState()函数将尝试在账本中添加或更新一堆键值。(实际上,这些值是发起了相应的交易,一直得等到区块链网络中,到最后的peer节点验证确认之后,才会写入账本。但是一般情况下,正常操作可以认为这一步会成功)

1
2
3
4
5
6
7
8
9
10
// Write the state to the ledger
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())
}

到方法的最后,通过shim.Success(nil)创建并返回状态为OK的Response消息。

3.Invoke方法

该方法中,同样使用GetFunctionAndParameters()方法提取本次调用的交易中所指定的参数,然后根据function的值的不同,执行不同的分支处理逻辑。

此案例中,实现对三种分支逻辑的处理: invoke,query,delete,由代码可知:

1
2
3
4
5
6
7
8
9
10
if function ** "invoke" {
// Make payment of X units from A to B
return t.invoke(stub, args)
} else if function ** "delete" {
// Deletes an entity from its state
return t.delete(stub, args)
} else if function ** "query" {
// the old "Query" is now implemtned in invoke
return t.query(stub, args)
}

方法又写了三个方法来对相应的代码逻辑进行处理。如果所有指定的方法不在这三种之中,将通过shim.Error()方法返回一个状态为ERROR的Response。

1. query方法

query方法实现了查询一个实体的余额。

该方法需要传入一个参数,即实体的名称。如,参数指定为{“Args”:[“query”,”a”]},表示查询实体a的余额。

通过代码可以看到,首先通过GetState()函数获取相应的实体的状态。通过查看源码知,此函数的原型如下,可以看到,函数接收键值,并返回相应的字节数组。

1
GetState(key string) ([]byte, error)

query函数如果查询到余额,则返回shim.Success(Avalbytes),其中Avalbytes为上述函数的返回值。返回的内容为状态为OK的Response消息,并将余额Avalbytes写入Response的Payload字段中。(其实也就是发起了一个交易,交易的字段中必须有Payload)。

2. invoke方法

此方法主要实现两个实体之间进行转账。方法需要传入三个参数,分别为付款方,收款方,以及转账数额。例如: {“Args”:[“invoke”,”a”,”b”,”50”]}.

方法同样用到GetState()来获取双方的余额,然后进行转账操作,之后将新的状态使用PutState()写入账本。

3.delete 方法

此方法类似于上面的两个方法,最为关键的是,调用了DelState()方法,该方法的原型如下:

1
func (stub *ChaincodeStub) DelState(key string) error {}

此方法将从账本中删去相应的记录,但是不要忘了这是在区块链里,该方法将会产生相应的交易,之后将写入区块链。

案例二: 资产权属管理

利用区块链进行资产权进行管理是很普遍的应用场景。

本节将以大理石的权属管理为例,来理解如何在链码中定义一种资产,并围绕这种资产提供创建,查询,转移所有权等操作。

1.链码结构

链码的基本结构和案例一一样,故这里不做赘述。

此案例中,声明了一个struct如下:

1
2
3
4
5
6
7
type marble struct {
ObjectType string `json:"docType"` //docType is used to distinguish the various types of objects in state database
Name string `json:"name"` //the fieldtags are needed to keep case from bouncing around
Color string `json:"color"`
Size int `json:"size"`
Owner string `json:"owner"`
}

此结构相应的字段如其英文名称所述的意思。值得一提的是,ObjectType字段,用于将结构体序列化为特定格式(如,json)时,该字段的键的名称。

2. Invoke方法

由于此方法的分支方法较多,所以列表说明,如下:

分支方法 功能 参数示例
initMarble 创建一个大理石信息并写入账本 [“initMarble”,”marblel1”,”blue”,”35”,”tom”]
transferMarble 更改一个大理石的拥有者 [“transferMarble”,”marble2”,”jerry”]
transferMarblesBasedOnColor 更改指定颜色的所有大理石的拥有者 [“transferMarblesBasedOnColor”,”blue”,”jerry”]
delete 删除一个大理石的信息 [“delete”,”marblel1”]
readMarble 从账本中读取一个大理石信息 [“readMarble”,”marblel1”]
queryMarblesByOwner 返回指定拥有者拥有的所有大理 石的信息 [“queryMarblesByOwner”,”tom”]
queryMarbles 富查询( rich query)大理石 信息 [“queryMarbles”,”\\”selector\\”:{\\”owner\\”:\\”tom\\”}]
getHistoryForMarble 返回一个大理石的所有历史信息 [”getHistoryForMarble”,”marble1” ]
getMarblesByRange 返回所有名称在指定字典序范围内的大理石的信息 [”getMarblesByRange”,”marblel”,” marble3 ” ]
getMarblesByRangeWithPagination 分页之后,返回指定页的所有名称在指定字典序范围内的大理石的信息,最后一个参数为每次调用之后所返回的值 [“getMarblesByRangeWithPagination”,”marblel”,” marble3 ”,1,””]
queryMarblesWithPagination 富查询( rich query)大理石 信息,并分页返回相关信息 [“queryMarbles”,”\\”selector\\”:{\\”owner\\”:\\”tom\\”},”1”,””]

由于方法数目比较多,所以接下来将对重要的函数进行介绍:

1. initMarble方法

方法将根据输入参数创建一个大理石,并写入账本。方法接收四个参数,如 案例二.2.表 所示。

方法首先读入参数,并判断相应参数的合法性,之后用这些参数进行查重。如果已经存在同样名称的大理石,则返回error的Response。

之后将创建marble类型的变量,并用json.Marshal()方法将自定义marble类型序列化为json对象。之后,将序列化之后的json字节数组写入账本。

之后用函数CreateCompositeKey创建复合key,因为并不需要再存一遍marble,所以,就赋值为0x00,然后存入账本。

创建复合key的代码如下:

1
2
3
4
5
6
7
8
9
indexName := "color~name"
colorNameIndexKey, err := stub.CreateCompositeKey(indexName, []string{marble.Color, marble.Name})
if err != nil {
return shim.Error(err.Error())
}
// Save index entry to state. Only the key name is needed, no need to store a duplicate copy of the marble.
// Note - passing a 'nil' value will effectively delete the key from state, therefore we pass null character as value
value := []byte{0x00}
stub.PutState(colorNameIndexKey, value)

这里创建复合键的意义时将一部分属性也构造为索引的一部分,使得针对某些属性做查询的时,可以直接根据索引返回查询结果,而不需要提取完整的信息来作对比。

函数CreateCompositeKey的函数原型:

1
func createCompositeKey(objectType string, attributes []string) (string, error) {}

此方法实际上会将objectType和attrbutes中的每个stirng串联起来,中间用U+0000分割,同时在开头加上\x00,标明该键为复合键。

2. readMarble

根据大理石的名称,从账本中查询并返回大理石的信息。方法接收一个参数,即大理石的名称。如果找到就返回,否则返回error的json字符串

3. delete

方法根据提供的大理石的名称,删除账本中的大理石信息。方法接收1个参数,即为大理石名称。在删除该键的同时,还需要删除所对应的颜色与名称的复合键。

4. getMarblesByRange

方法中调用了API stub.GetStateByRange(startKey,endKey),该函数返回结果时一个迭代器StateQueryIteratorInterface结构。方法的原型如下:

1
func (stub *ChaincodeStub) GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) {}

对于迭代器StateQueryIteratorInterface的使用方法可参考如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func constructQueryResponseFromIterator(resultsIterator shim.StateQueryIteratorInterface) (*bytes.Buffer, error) {
// buffer is a JSON array containing QueryResults
var buffer bytes.Buffer
buffer.WriteString("[")

bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
// Add a comma before array members, suppress it for the first array member
if bArrayMemberAlreadyWritten ** true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")

buffer.WriteString(", \"Record\":")
// Record is a JSON object, so we write as-is
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

return &buffer, nil
}

5. transferMarblesBasedOnColor

该方法将指定颜色的大理石的所有权转移到新的用户名下。

此方法主要用到了复合键的查询。stub.GetStateByPartialCompositeKey方法可以返回所有满足条件的键值对。其结果也是一个迭代器StateQueryInteratorInterface结构. 事实上,这里指定了前缀为当时设定的objectType("color~name")加上attributes的第一个string(color的值)。实际上,GetStateByPartialCompositeKey在实现上是以复合键前缀为起始,前缀加utf8.MaxRune为终止,通过调用范围查找返回所有匹配的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Query the color~name index by color
// This will execute a key range query on all keys starting with 'color'
coloredMarbleResultsIterator, err := stub.GetStateByPartialCompositeKey("color~name", []string{color})
if err != nil {
return shim.Error(err.Error())
}
defer coloredMarbleResultsIterator.Close()
// Iterate through result set and for each marble found, transfer to newOwner
var i int
for i = 0; coloredMarbleResultsIterator.HasNext(); i++ {
// Note that we don't get the value (2nd return variable), we'll just get the marble name from the composite key
responseRange, err := coloredMarbleResultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}

// get the color and name from color~name composite key
objectType, compositeKeyParts, err := stub.SplitCompositeKey(responseRange.Key)
if err != nil {
return shim.Error(err.Error())
}
returnedColor := compositeKeyParts[0]
returnedMarbleName := compositeKeyParts[1]
fmt.Printf("- found a marble from index:%s color:%s name:%s\n", objectType, returnedColor, returnedMarbleName)

// Now call the transfer function for the found marble.
// Re-use the same function that is used to transfer individual marbles
response := t.transferMarble(stub, []string{returnedMarbleName, newOwner})
// if the transfer failed break out of loop and return error
if response.Status != shim.OK {
return shim.Error("Transfer failed: " + response.Message)
}
}

值得一提的是,上面的程序段里,使用stub.SplitCompositeKey()方法拆分了复合键,得到构造复合键时所用的各个attributes,即大理石颜色和名称。得到名称后,通过内部调用transferMarble()方法更新拥有者。

6. queryMarbles方法

该方法通过使用支持副查询的数据库(如CouchDB)作为状态数据库,则可以进行规则更为复杂的富查询。

方法里使用了stub.GetQueryResult,传入的参数为富查询指令字符串。

此方法所产生的交易,不会在最后阶段被校验,故而,这里最好只进行查询。

案例三: 调用其他链码

在同一个区块链上可以部署多个链码,多个链码之间可以互相调用。这种方式有助于将智能合约的工作模块化,并为应用开发带来更多的灵活性。

本节将通过链码passthru来进行学习链码相互调用。该链码的功能可以形容为同一个区块链中其他的“网关“,其对外暴露的Invoke接口功能可以使用户指定想要调用的其他的链码的ID,方法,参数。通过该”网关“链码传递给指定链码,获得调用结果后返回给用户。

该链码比较短,所以这里分析一下核心的Invoke方法,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//helper
func (p *PassthruChaincode) iq(stub shim.ChaincodeStubInterface, function string, args []string) pb.Response {
if function ** "" {
return shim.Error("Chaincode ID not provided")
}
chaincodeID := function

return stub.InvokeChaincode(chaincodeID, toChaincodeArgs(args...), "")
}

// Invoke passes through the invoke call
func (p *PassthruChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
function, args := stub.GetFunctionAndParameters()
return p.iq(stub, function, args)
}

其中最核心的是,调用其他链码需要使用stub.InvokeChaincode()。该方法用于调用另一个链码中的Invoke方法。值得注意的是,该方法目前仅限于读操作,同时不会生成新的交易。如果第三个参数为空,则默认为空当前通道,否则为指定的通道。

案例四: 发送事件

Fabric应用程序除了通过主动查询来获取当前已确认的状态,还可以通过订阅并监听事件(event)来获取交易执行信息,用于进行交易或者审计。

本节通过学习examples/chaincode/go/eventsender/eventsender.go,来学习使用方法。

发送事件需要使用stub.SetEvent方法。方法的格式为:

1
SetEvent(name string, payload []byte) error

其中,name表示事件的名称,payload为事件内容。

通过该方法,可以设定当这个交易在Committer处被认证通过,写入到区块时所发送的事件。

示例链码的invoke分支被调用的时候,会将记录子啊账本中的递增序列和Invoke传入的参数串联起来作为事件内容,以eventsender为事件名称,调用stub.SetEvent方法。

应用开发着可以使用SDK中封装的方法监听链码发出的事件,然后在此基础上作出相应的逻辑处理。当然还可以使用fabric提供的block -listener工具来监听。

block-listener工具位于examples/events/block-listener,展示了如何利用事件客户端来从网络中获取事件信息。具体实现参考源码,这里不做赘述。

最后更新: 2023年02月22日 00:00

原始链接: /2020/06/28/fabric链码API及相关开发实例/

× 请我吃糖~
打赏二维码