Fabric 智能合约API简单理解

在基于github.com/hyperledger/fabric/core/chaincode 或者 hyperledger/fabric-contract-api-go库进行简单的开发后,进一步分析一下Fabric chaincode 的 处理流程和一些常用API解释。

Fabric 原生构建流程

首先我们可以看到,github.com/hyperledger/fabric/core/chaincode是源于Fabric本身的代码,因为Fabric基于Go开发,所以方便调用自身的代码构建。

以一个案例分析:

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
34
35
36
37
38
package main

import (
"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
"fmt"
)

// 定义一个合约结构
type SimpleChaincode struct {
}

// 主函数
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 {
return shim.Success(nil)
}

// 反射执行chain code的方法
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
function, args := stub.GetFunctionAndParameters()
fmt.Println("invoke is running " + function)
if function == "test1" { //自定义函数名称
return t.test1(stub, args) //定义调用的函数
}
return shim.Error("Received unknown function invocation")
}

// 一个自定义chain code 合约方法
func (t *SimpleChaincode) test1(stub shim.ChaincodeStubInterface, args []string) pb.Response{
return shim.Success([]byte("Called test1"))
}

这里我们可以看到,在InitInvoke的时候,都会传入参数stub shim.ChaincodeStubInterface,这个参数提供的接口为我们编写ChainCode的业务逻辑提供了大量实用的方法

下面仔细介绍一下这个shim.ChaincodeStubInterface接口

  • 获得调用的参数

    Invoke的时候,由传入的参数来决定我们具体调用了哪个方法,所以需要先使用GetFunctionAndParameters解析调用的时候传入的方法名和参数

    • GetArgs() [][]byte以byte数组的数组的形式获得传入的参数列表
    • GetStringArgs() []string 以字符串数组的形式获得传入的参数列表
    • GetFunctionAndParameters() (string, []string) 将字符串数组的参数分为两部分,数组第一个字是Function,剩下的都是Parameter
    • GetArgsSlice() ([]byte, error) 以byte切片的形式获得参数列表

操作区块链数据

对于ChainCode来说,核心的操作就是对State Database的增删改查,对此Fabric接口提供了3个对State DB的操作方法。

  • 增改数据PutState(key string, value []byte) error

    这个也很好理解,根据Key删除State DB的数据。如果根据Key找不到对于的数据,删除失败。

    1
    2
    3
    4
    err= stub.DelState(key)
    if err != nil {
    return shim.Error("Failed to delete Student from DB, key is: "+key)
    }
  • 删除数据DelState(key string) error

    这个也很好理解,根据Key删除State DB的数据。如果根据Key找不到对于的数据,删除失败。

    1
    2
    3
    4
    err= stub.DelState(key)
    if err != nil {
    return shim.Error("Failed to delete Student from DB, key is: "+key)
    }
  • 查询数据GetState(key string) ([]byte, error)

    因为我们是Key Value数据库,所以根据Key来对数据库进行查询,是一件很常见,很高效的操作。返回的数据是byte数组,我们需要转换为string,然后再Json反序列化,可以得到我们想要的对象。

    1
    2
    3
    4
    5
    6
    7
    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)
  • 符合键操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    前面在进行数据库的增删改查的时候,都需要用到Key,而我们使用的是我们自己定义的Key格式:{StructName}:{Id},这是有单主键Id还比较简单,如果我们有多个列做联合主键怎么办?实际上,ChainCode也为我们提供了生成Key的方法CreateCompositeKey,通过这个方法,我们可以将联合主键涉及到的属性都传进去,并声明了对象的类型即可。

    以选课表为例,里面包含了以下属性:

    type ChooseCourse struct {
    CourseNumber string //开课编号
    StudentId int //学生ID
    Confirm bool //是否确认
    }
    • 生成复合键CreateCompositeKey(objectType string, attributes []string) (string, error)

      1
      2
      3
      4
      5
      # 其中CourseNumber+StudentId构成了这个对象的联合主键,我们要获得生成的复核主键,那么可写为:

      cc:=ChooseCourse{"CS101",123,true}
      var key1,_= stub.CreateCompositeKey("ChooseCourse",[]string{cc.CourseNumber,strconv.Itoa(cc.StudentId)})
      fmt.Println(key1)

      【注:其实Fabric就是用U+0000来把各个字段分割开的,因为这个字符太特殊,所以很适合做分割】

    • 拆分复合键SplitCompositeKey(compositeKey string) (string, []string, error)

      既然有组合那么就有拆分,当我们从数据库中获得了一个复合键的Key之后,怎么知道其具体是由哪些字段组成的呢。其实就是用U+0000把这个复合键再Split开,得到结果中第一个是objectType,剩下的就是复合键用到的列的值。

      1
      2
      objType,attrArray,_:= stub.SplitCompositeKey(key1)
      fmt.Println("Object:"+objType+" ,Attributes:"+strings.Join(attrArray,"|"))
    • 部分复合键的查询GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)

      这里其实是一种对Key进行前缀匹配的查询,也就是说,我们虽然是部分复合键的查询,但是不允许拿后面部分的复合键进行匹配,必须是前面部分。

    • 获得当前用户GetCreator() ([]byte, error)

      这个方法可以获得调用这个ChainCode的客户端的用户的证书,这里虽然返回的是byte数组,但是其实是一个字符串,内容格式如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      -----BEGIN CERTIFICATE-----
      MIICGjCCAcCgAwIBAgIRAMVe0+QZL+67Q+R2RmqsD90wCgYIKoZIzj0EAwIwczEL
      MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBG
      cmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMTE2Nh
      Lm9yZzEuZXhhbXBsZS5jb20wHhcNMTcwODEyMTYyNTU1WhcNMjcwODEwMTYyNTU1
      WjBbMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMN
      U2FuIEZyYW5jaXNjbzEfMB0GA1UEAwwWVXNlcjFAb3JnMS5leGFtcGxlLmNvbTBZ
      MBMGByqGSM49AgEGCCqGSM49AwEHA0IABN7WqfFwWWKynl9SI87byp0SZO6QU1hT
      JRatYysXX5MJJRzvvVsSTsUzQh5jmgwkPbFcvk/x4W8lj5d2Tohff+WjTTBLMA4G
      A1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMCsGA1UdIwQkMCKAIO2os1zK9BKe
      Lb4P8lZOFU+3c0S5+jHnEILFWx2gNoLkMAoGCCqGSM49BAMCA0gAMEUCIQDAIDHK
      gPZsgZjzNTkJgglZ7VgJLVFOuHgKWT9GbzhwBgIgE2YWoDpG0HuhB66UzlA+6QzJ
      +jvM0tOVZuWyUIVmwBM=
      -----END CERTIFICATE-----

      我们常见的需求是在ChainCode中获得当前用户的信息,方便进行权限管理。那么我们怎么获得当前用户呢?

      我们可以把这个证书的字符串转换为Certificate对象。一旦转换成这个对象,我们就可以通过Subject获得当前用户的名字。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      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))
      }
  • Key区间查询GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)

    提供了对某个区间的Key进行查询的接口,适用于任何State DB。由于返回的是一个StateQueryIteratorInterface接口,我们需要通过这个接口再做一个for循环,才能读取返回的信息,所有我们可以独立出一个方法,专门将该接口返回的数据以string的byte数组形式返回。这是我们的转换方法:

    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
    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
    }
  • 其他需要CouchDB时才能用这个方法 此处不多介绍了。

  • 调用其他的智能合约

    这个比较好理解,就是在我们的链上代码中调用别人已经部署好的链上代码。

    1
    2
    3
    4
    5
    6
    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))
    }
  • 获得提案对象Proposal属性

    • 获得签名的提案GetSignedProposal() (*pb.SignedProposal, error)

      1
      从客户端发现背书节点的Transaction或者Query都是一个提案,GetSignedProposal获得当前的提案对象包括客户端对这个提案的签名。提案的内容如果直接打印出来感觉就像是乱码,其内包含了提案Header,Payload和Extension,里面更包含了复杂的结构,这里不讲,以后可以写一篇博客专门研究提案对象。
    • 获得Transient对象 GetTransient() (map[string][]byte, error)

      1
      Transient是在提案中Payload对象中的一个属性,也就是ChaincodeProposalPayload.TransientMap
    • 获得交易时间戳GetTxTimestamp() (*timestamp.Timestamp, error)

      1
      交易时间戳也是在提案对象中获取的,提案对象的Header部分,也就是proposal.Header.ChannelHeader.Timestamp
    • 获得Binding对象 GetBinding() ([]byte, error)

      1
      这个Binding对象也是从提案对象中提取并组合出来的,其中包含proposal.Header中的SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch。关于Proposal对象确实很8复杂,我目前了解的并不对,接下来得详细研究。
  • 事件设置SetEvent(name string, payload []byte) error

    当ChainCode提交完毕,会通过Event的方式通知Client。而通知的内容可以通过SetEvent设置。

    1
    2
    3
    4
    5
    6
    7
    8
    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)
    }

    事件设置完毕后,需要在客户端也做相应的修改。

fabric-contract-api-go 对 原生代码的封装

大同小异与原生代码库的使用方法,也是通过 ctx.GetStub()获得 shim.ChaincodeStubInterface来实现对区块链数据的操作。

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
package main

import (
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// 定义一个合约结构
type SimpleChaincode struct {
contractapi.Contract
}

// 主函数
func main() {

// 注册链码
chaincode, err := contractapi.NewChaincode(new(SmartContract))
if err != nil {
fmt.Printf("Error create fabcar chaincode: %s", err.Error())
return
}

if err := chaincode.Start(); err != nil {
fmt.Printf("Error starting fabcar chaincode: %s", err.Error())
}
}


// 一个自定义chain code 合约方法
func (s *SimpleChaincode) test1(ctx contractapi.TransactionContextInterface,key string) ([]byte, error){
res, err := ctx.GetStub().GetState(key)
return res,err
}

不过使用API接口实现了一些方法的封装,如:invokeinit等方法。开发者可以更加专注于应用逻辑的书写,使用ctx.GetStub()实现对数据的操作(类似对数据库的操作)。

总结:

Fabric网络默认使用了GRPC调用智能合约,使用了类似反射的机制实现在SDK端调用如:

contract.SubmitTransaction("test1", "args")方法时就会向Peer节点发送一个交易请求,请求内容是调用test1方法,参数是args。

由于智能合约代码实现了Contract的方法,且被发布到Peer节点上,所以最后合约代码会由Fabric来进行调用执行。

参考:

HyperLedger Fabric ChainCode开发——shim.ChaincodeStubInterface用法 - 深蓝 - 博客园 (cnblogs.com)