目前fabric开发主要分两大块,一是chaincode链上代码的开发,二是基于SDK的application开发;
本文主要是介绍fabric chaincode api,对api的功能和参数进行说明,同时针对每个主要api都给出了
使用例子。
Go语言开发Chaincode链码
Chaincode的go代码首先需要一定结构体struct,然后在struct结构体上定义两个函数Init和Invoke,最后定义main函数作为chaincode的入口,模板如下:
package main import ( "github.com/hyperledger/fabric/core/chaincode/shim" pb "github.com/hyperledger/fabric/protos/peer" "fmt" )
type SimpleChaincode struct { }
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response { _,args := stub.GetFunctionAndParameters() var A, B string //entities var Aval, Bval int // asset holdings if len(args) != 4 { return shim.Error("Incorrect number of arguments. Expecting 4") } // TODO… return shim.Success(nil) }
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response { function, args := stub.GetFunctionAndParameters() fmt.Println("invoke is running " + function) if function == "add" { //自定义函数名称 return t.add (stub, args) //定义调用的函数 } else if function == "delete"{ } return shim.Error("Received unknown function invocation") } func main() { err := shim.Start(new(SimpleChaincode)) if err != nil { fmt.Printf("Error starting Simple chaincode: %s", err) } } |
|
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response
Init
(初始化)方法会在chaincode接收到instantiate
(实例化)或者upgrade
(升级)交易时被调用,进而使得chaincode顺利执行必要的初始化操作,包括初始化应用的状态;
说明 |
名称 |
类型 |
函数名 |
Init |
自定义结构体 |
入参 |
stub |
shim.ChaincodeStubInterface |
返回值 |
Peer返回值 |
pb.Response |
func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response
Invoke
(调用)方法会在响应invoke
(调用)交易时被调用以执行交易。根据传入的参数不同判断调用不同的函数,完成不同的功能。
说明 |
名称 |
类型 |
函数名 |
invoke |
自定义结构体 |
入参 |
stub |
shim.ChaincodeStubInterface |
入参 |
args |
[]string |
返回值 |
Peer返回值 |
pb.Response |
4.3.1 GetArgs() [][]byte
以byte数组的数组的形式获得传入的参数列表
说明 |
名称 |
类型 |
函数名 |
GetArgs |
|
返回值 |
传入参数列表 |
[][]byte |
4.3.2 GetStringArgs() []string
以字符串数组的形式获得传入的参数列表
说明 |
名称 |
类型 |
函数名 |
GetStringArgs |
|
返回值 |
传入参数列表 |
[]string |
4.3.3 GetFunctionAndParameters() (string, []string)
将字符串数组的参数分为两部分,数组第一个字是Function,剩下的都是Parameter
说明 |
名称 |
类型 |
函数名 |
GetFunctionAndParameters |
|
返回值 |
Function name |
string |
返回值 |
参数列表 |
[]string |
4.3.4 GetArgsSlice() ([]byte, error)
以byte切片的形式获得参数列表
说明 |
名称 |
类型 |
函数名 |
GetArgsSlice |
|
返回值 |
参数列表 |
[]byte |
返回值 |
错误码 |
error |
4.4.1增改数据PutState(key string, value []byte) error
对于State DB来说,增加和修改数据是统一的操作,因为State DB是一个Key Value数据库,如果我们指定的Key在数据库中已经存在,那么就是修改操作,如果Key不存在,那么就是插入操作。对于实际的系统来说,我们的Key可能是单据编号,或者系统分配的自增ID+实体类型作为前缀,而Value则是一个对象经过JSON序列号后的字符串。
说明 |
名称 |
类型 |
函数名 |
PutState |
|
传入值 |
Key |
String |
传入值 |
Value |
[]byte |
返回值 |
Error |
Error |
比如说我们定义一个Student的Struct,然后插入一个学生数据,对于的代码应该是这样的:
type Student struct {
Id int
Name string
}
func (t *SimpleChaincode) testStateOp(stub shim.ChaincodeStubInterface, args []string) pb.Response{
student1:=Student{1,"Devin Zeng"}
key:="Student:"+strconv.Itoa(student1.Id)//Key格式为 Student:{Id}
studentJsonBytes, err := json.Marshal(student1)//Json序列号
if err != nil {
return shim.Error(err.Error())
}
err= stub.PutState(key,studentJsonBytes)
if(err!=nil){
return shim.Error(err.Error())
}
return shim.Success([]byte("Saved Student!"))
}
根据Key删除State DB的数据。如果根据Key找不到对于的数据,删除失败。4.4.2删除数据DelState(key string) error
说明 |
名称 |
类型 |
函数名 |
DelState |
|
传入值 |
Key |
String |
返回值 |
Error |
Error |
4.4.3查询数据GetState(key string) ([]byte, error)
根据Key来对数据库进行查询。返回的数据是byte数组,我们需要转换为string,然后再Json反序列化,可以得到我们想要的对象。
说明 |
名称 |
类型 |
函数名 |
GetState |
|
传入值 |
Key |
String |
返回值 |
Key对应的value值 |
[]byte |
返回值 |
Error |
Error |
例:
dbStudentBytes,err:= stub.GetState(key)
var dbStudent Student;
err=json.Unmarshal(dbStudentBytes,&dbStudent)//反序列化
if err != nil {
return shim.Error("{\"Error\":\"Failed to decode JSON of: " + string(dbStudentBytes)+ "\" to Student}")
}
fmt.Println("Read Student from DB, name:"+dbStudent.Name)
4.5.1 生成复合键
CreateCompositeKey(objectType string, attributes []string) (string, error)
在进行数据库的增删改查的时候,都需要用到Key,而我们使用的是我们自己定义的Key格式:{StructName}:{Id},这是有单主键Id还比较简单,如果我们有多个列做联合主键怎么办?实际上,ChainCode也为我们提供了生成Key的方法CreateCompositeKey,通过这个方法,我们可以将联合主键涉及到的属性都传进去,并声明了对象的类型即可。
说明 |
名称 |
类型 |
函数名 |
CreateCompositeKey |
|
传入值 |
目标类型objectType |
String |
传入值 |
用来创建key的属性值 |
[]string |
返回值 |
组合后的key值 |
string |
返回值 |
error |
error |
使用:
type ChooseCourse struct {
CourseNumber string //开课编号
StudentId int //学生ID
Confirm bool //是否确认
}
//其中CourseNumber+StudentId构成了这个对象的联合主键,我们要获得生成的复核主键,那么可写为:
cc:=ChooseCourse{"CS101",123,true}
var key1,_= stub.CreateCompositeKey("ChooseCourse",[]string{cc.CourseNumber,strconv.Itoa(cc.StudentId)})
fmt.Println(key1)
4.5.2 拆分复合键
SplitCompositeKey(compositeKey string) (string, []string, error)
当我们从数据库中获得了一个复合键的Key之后,怎么知道其具体是由哪些字段组成的呢。其实就是用U+0000把这个复合键再Split开,得到结果中第一个是objectType,剩下的就是复合键用到的列的值。
说明 |
名称 |
类型 |
函数名 |
SplitCompositeKey |
|
传入值 |
组合key:compositeKey |
String |
传入值 |
目标变量的类型 |
string |
返回值 |
对象类型 |
objectType |
返回值 |
组合key的组成成分 |
[]string |
返回值 |
error |
error |
使用:
type Car struct {
ID string `json:"ID"` // key
Color string `json:"Color"`
Price string `json:"Price"`
LaunchDate string `json:"LaunchDate"`
}
// get the color, Price and name from composite key
objectType, compositeKeyParts, err := stub.SplitCompositeKey(responseRange.Key)
if err != nil {
return shim.Error(err.Error())
}
returnedId := compositeKeyParts[0]
returnedColor := compositeKeyParts[1]
returnedPrice := compositeKeyParts[2]
4.5.3 部分复合键的查询
GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)
一种对Key进行前缀匹配的查询,也就是说,我们虽然是部分复合键的查询,但是不允许拿后面部分的复合键进行匹配,必须是前面部分。即,根据局部的复合键(前缀)返回所有匹配的键值。返回结果是一个迭代器。可以按照字典序迭代每个键值对,最后需调用Close()方法关闭。
说明 |
名称 |
类型 |
函数名 |
GetStateByPartialCompositeKey |
|
传入值 |
目标变量的类型 |
string |
传入值 |
Key |
[]string |
返回值 |
StateQueryIteratorInterface |
StateQueryIteratorInterface |
返回值 |
error |
error |
使用:
func (sc *SimpleChaincode) queryAllOwner(stub shim.ChaincodeStubInterface) pb.Response {
coloredMarbleResultsIterator, err := stub.GetStateByPartialCompositeKey(INDEX, []string{})
if err != nil {
return shim.Error(err.Error())
}
defer coloredMarbleResultsIterator.Close() //函数结束时执行
for coloredMarbleResultsIterator.HasNext() {
responseRange, err := coloredMarbleResultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
_, compositeKeyParts, err := stub.SplitCompositeKey(responseRange.Key)
if err != nil {
return shim.Error(err.Error())
}
// name := compositeKeyParts[0]
// hashvalue := compositeKeyParts[1]
owner := compositeKeyParts[2]
}
return shim.Success(buffer.Bytes())
}
4.6高级查询
4.6.1 Key区间查询
GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)
提供了对某个区间的Key进行查询的接口,适用于任何State DB。由于返回的是一个StateQueryIteratorInterface接口,我们需要通过这个接口再做一个for循环,才能读取返回的信息,所有我们可以独立出一个方法,专门将该接口返回的数据以string的byte数组形式返回。
说明 |
名称/说明 |
类型 |
函数名 |
GetStateByRange |
|
传入值 |
查询开始key:startKey |
string |
传入值 |
查询结束key:endKey |
string |
返回值 |
StateQueryIteratorInterface接口 |
StateQueryIteratorInterface |
返回值 |
error |
error |
转换方法:
func getListResult(resultsIterator shim.StateQueryIteratorInterface) ([]byte,error){
defer resultsIterator.Close()
// buffer is a JSON array containing QueryRecords
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("]")
fmt.Printf("queryResult:\n%s\n", buffer.String())
return buffer.Bytes(), nil
}
//假如我们要查询编号从1号到3号的所有学生,那么我们的查询代码可以这么写:
func (t *SimpleChaincode) testRangeQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{
resultsIterator,err:= stub.GetStateByRange("Student:1","Student:3")
if err!=nil{
return shim.Error("Query by Range failed")
}
students,err:=getListResult(resultsIterator)
if err!=nil{
return shim.Error("getListResult failed")
}
return shim.Success(students)
}
4.6.2 富查询
GetQueryResult(query string) (StateQueryIteratorInterface, error)
这是一个“富查询”,是对Value的内容进行查询,如果是LevelDB,那么是不支持,只有CouchDB时才能用这个方法。传入的query这个字符串,其实是CouchDB所使用的Mango查询。
说明 |
名称/说明 |
类型 |
函数名 |
GetStateByRange |
|
传入值 |
CouchDB查询语句query |
string |
返回值 |
StateQueryIteratorInterface接口 |
StateQueryIteratorInterface |
返回值 |
error |
error |
我们仍然以前面的Student为例,我们要按Name来进行查询,那么我们的代码可以写为:
func (t *SimpleChaincode) testRichQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{
name:="Devin Zeng"//这里按理来说应该是参数传入
queryString := fmt.Sprintf("{\"selector\":{\"Name\":\"%s\"}}", name)
resultsIterator,err:= stub.GetQueryResult(queryString)//必须是CouchDB才行
if err!=nil{
return shim.Error("Rich query failed")
}
students,err:=getListResult(resultsIterator)
if err!=nil{
return shim.Error("Rich query failed")
}
return shim.Success(students)
}
4.6.3 历史数据查询
GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)
对同一个数据(也就是Key相同)的更改,会记录到区块链中,我们可以通过GetHistoryForKey方法获得这个对象在区块链中记录的更改历史,包括是在哪个TxId,修改的数据,修改的时间戳,以及是否是删除等。
说明 |
名称/说明 |
类型 |
函数名 |
GetHistoryForKey |
|
传入值 |
查询key |
string |
返回值 |
StateQueryIteratorInterface接口 |
StateQueryIteratorInterface |
返回值 |
error |
error |
比如之前的Student:1这个对象,我们更改和删除过数据,现在要查询这个对象的更改记录,那么对应代码为:
func (t *SimpleChaincode) testHistoryQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{
student1:=Student{1,"Devin Zeng"}
key:="Student:"+strconv.Itoa(student1.Id)
it,err:= stub.GetHistoryForKey(key)
if err!=nil{
return shim.Error(err.Error())
}
var result,_= getHistoryListResult(it)
return shim.Success(result)
}
func getHistoryListResult(resultsIterator shim.HistoryQueryIteratorInterface) ([]byte,error){
defer resultsIterator.Close()
// buffer is a JSON array containing QueryRecords
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(",")
}
item,_:= json.Marshal( queryResponse)
buffer.Write(item)
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
fmt.Printf("queryResult:\n%s\n", buffer.String())
return buffer.Bytes(), nil
}
4.6.4 部分复合键查询
GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)
与2.5.3类似
说明 |
名称 |
类型 |
函数名 |
GetStateByPartialCompositeKey |
|
传入值 |
目标变量的类型 |
string |
传入值 |
Key |
[]string |
返回值 |
StateQueryIteratorInterface |
StateQueryIteratorInterface |
返回值 |
error |
error |
2.7 获得当前用户
GetCreator() ([]byte, error)
这个方法可以获得调用这个ChainCode的客户端的用户的证书,这里虽然返回的是byte数组,但是其实是一个字符串,内容格式如下:
-----BEGIN CERTIFICATE----- |
我们常见的需求是在ChainCode中获得当前用户的信息,方便进行权限管理。那么我们怎么获得当前用户呢?我们可以把这个证书的字符串转换为Certificate对象。一旦转换成这个对象,我们就可以通过Subject获得当前用户的名字
func (t *SimpleChaincode) testCertificate(stub shim.ChaincodeStubInterface, args []string) pb.Response{
creatorByte,_:= stub.GetCreator()
certStart := bytes.IndexAny(creatorByte, "-----BEGIN")
if certStart == -1 {
fmt.Errorf("No certificate found")
}
certText := creatorByte[certStart:]
bl, _ := pem.Decode(certText)
if bl == nil {
fmt.Errorf("Could not decode the PEM structure")
}
cert, err := x509.ParseCertificate(bl.Bytes)
if err != nil {
fmt.Errorf("ParseCertificate failed")
}
uname:=cert.Subject.CommonName
fmt.Println("Name:"+uname)
return shim.Success([]byte("Called testCertificate "+uname))
}
InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response
在自己的链上代码中调用别人已经部署好的链上代码。
在本地使用相同的交易上下文调用指定链码 的invoke()方法,在链码中调用链码不会产生新的交易消息。
如果被调用的链码在同一个通道,那么它只是简单地将被调用链码的 读写集添加到被调用交易中。
如果被调用的链码处于不同的通道,那么只会返回响应结果,在被调用 链码中的PutState调用不会影响账本的状态。
说明 |
名称 |
类型 |
函数名 |
InvokeChaincode |
|
传入值 |
chaincodeName |
string |
传入值 |
args |
[][]byte |
传入值 |
channel(要调用的链码所在的通道名称) |
string |
返回值 |
|
pb.Response |
比如官方提供的example02,要在代码中去实现a->b的转账,实现如下:
func (t *SimpleChaincode) testInvokeChainCode(stub shim.ChaincodeStubInterface, args []string) pb.Response{
trans:=[][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("11")}
response:= stub.InvokeChaincode("mycc",trans,"mychannel")
fmt.Println(response.Message)
return shim.Success([]byte( response.Message))
}
4.8.1 获得签名的提案
GetSignedProposal() (*pb.SignedProposal, error)
从客户端发现背书节点的Transaction或者Query都是一个提案,GetSignedProposal获得当前的提案对象包括客户端对这个提案的签名。提案的内容如果直接打印出来就像是乱码,其内包含了提案Header,Payload和Extension,里面更包含了复杂的结构,这里不讲,以后可以写一篇博客专门研究提案对象。
4.8.2获得Transient对象
GetTransient() (map[string][]byte, error)
Transient是在提案中Payload对象中的一个属性,也就是ChaincodeProposalPayload.TransientMap
4.8.3获得交易时间戳
GetTxTimestamp() (*timestamp.Timestamp, error)
交易时间戳也是在提案对象中获取的,提案对象的Header部分,也就是proposal.Header.ChannelHeader.Timestamp
4.8.4 获得Binding对象
GetBinding() ([]byte, error)
这个Binding对象也是从提案对象中提取并组合出来的,其中包含proposal.Header中的SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch。
2.9 事件设置
SetEvent(name string, payload []byte) error
当ChainCode提交完毕,会通过Event的方式通知Client。而通知的内容可以通过SetEvent设置。
func (t *SimpleChaincode) testEvent(stub shim.ChaincodeStubInterface, args []string) pb.Response{
tosend := "Event send data is here!"
err := stub.SetEvent("evtsender", []byte(tosend))
if err != nil {
return shim.Error(err.Error())
}
return shim.Success(nil)
}
4.9.1 设置指定键的背书策略
GetStateValidationParameter(key string) ([]byte, error)
设置key的背书策略。
2.9.2 获取指定键的背书策略
GetStateValidationParameter(key string) ([]byte, error)
获取key的背书策略