zoukankan      html  css  js  c++  java
  • HyperLedger Fabric ChainCode开发——shim.ChaincodeStubInterface用法

    深蓝前几篇博客讲了Fabric的环境搭建,在环境搭建好后,我们就可以进行Fabric的开发工作了。Fabric的开发主要分成2部分,ChainCode链上代码开发和基于SDK的Application开发。我们这里先讲ChainCode的开发。Fabric的链上代码支持Java或者Go语言进行开发,因为Fabric本身是Go开发的,所以深蓝建议还是用Go进行ChainCode的开发。

    ChainCode的Go代码需要定义一个SimpleChaincode这样一个struct,然后在该struct上定义Init和Invoke两个函数,然后还要定义一个main函数,作为ChainCode的启动入口。以下是ChainCode的模板:

    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)
    }
    
    
    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")
    }
    func (t *SimpleChaincode) test1(stub shim.ChaincodeStubInterface, args []string) pb.Response{
       return shim.Success([]byte("Called test1"))
    }
    这里我们可以看到,在Init和Invoke的时候,都会传入参数stub shim.ChaincodeStubInterface,这个参数提供的接口为我们编写ChainCode的业务逻辑提供了大量实用的方法。下面一一讲解:

    1.获得调用的参数

    前面给出的ChainCode的模板中,我们已经可以看到,在Invoke的时候,由传入的参数来决定我们具体调用了哪个方法,所以需要先使用GetFunctionAndParameters解析调用的时候传入的参数。除了这个方法以外,接口还提供了另外几个方法,不过其本质都是一样的。
    • GetArgs() [][]byte 以byte数组的数组的形式获得传入的参数列表
    • GetStringArgs() []string 以字符串数组的形式获得传入的参数列表
    • GetFunctionAndParameters() (string, []string) 将字符串数组的参数分为两部分,数组第一个字是Function,剩下的都是Parameter
    • GetArgsSlice() ([]byte, error) 以byte切片的形式获得参数列表

    2. 增删改查State DB

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

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

    对于State DB来说,增加和修改数据是统一的操作,因为State DB是一个Key Value数据库,如果我们指定的Key在数据库中已经存在,那么就是修改操作,如果Key不存在,那么就是插入操作。对于实际的系统来说,我们的Key可能是单据编号,或者系统分配的自增ID+实体类型作为前缀,而Value则是一个对象经过JSON序列号后的字符串。比如说我们定义一个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!"))
    }

    2.2 删除数据DelState(key string) error

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

    err= stub.DelState(key)
    if err != nil {
       return shim.Error("Failed to delete Student from DB, key is: "+key)
    }

    2.3 查询数据GetState(key string) ([]byte, error)

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

    【注意:不能在一个ChainCode函数中PutState后又马上GetState,这个时候GetState是没有最新值的,因为在这时Transaction并没有完成,还没有提交到StateDB里面】

    3. 复合键的处理

    3.1 生成复合键CreateCompositeKey(objectType string, attributes []string) (string, error)

    前面在进行数据库的增删改查的时候,都需要用到Key,而我们使用的是我们自己定义的Key格式:{StructName}:{Id},这是有单主键Id还比较简单,如果我们有多个列做联合主键怎么办?实际上,ChainCode也为我们提供了生成Key的方法CreateCompositeKey,通过这个方法,我们可以将联合主键涉及到的属性都传进去,并声明了对象的类型即可。
    以选课表为例,里面包含了以下属性:
    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)
    【注:其实Fabric就是用U+0000来把各个字段分割开的,因为这个字符太特殊,所以很适合做分割】

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

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

    objType,attrArray,_:= stub.SplitCompositeKey(key1)
    fmt.Println("Object:"+objType+" ,Attributes:"+strings.Join(attrArray,"|"))

    3.3 部分复合键的查询GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)

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

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

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

    -----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获得当前用户的名字。

    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))
    }

    5.高级查询

    前面提到的GetState只是最基本的根据Key查询值的操作,但是对于很多时候,我们需要查询返回的是一个集合,比如我要知道某个区间的Key对于所有对象,或者我们需要对Value对象内部的属性进行查询。

    5.1 Key区间查询GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error)

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

    5.2 富查询GetQueryResult(query string) (StateQueryIteratorInterface, error)

    这是一个“富查询”,是对Value的内容进行查询,如果是LevelDB,那么是不支持,只有CouchDB时才能用这个方法。
    关于传入的query这个字符串,其实是CouchDB所使用的Mango查询,我们可以在官方博客了解到一些信息:https://blog.couchdb.org/2016/08/03/feature-mango-query/ 其基本语法可以在https://github.com/cloudant/mango 这里看到。
    比如我们仍然以前面的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)
    }

    5.3历史数据查询GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)

    对同一个数据(也就是Key相同)的更改,会记录到区块链中,我们可以通过GetHistoryForKey方法获得这个对象在区块链中记录的更改历史,包括是在哪个TxId,修改的数据,修改的时间戳,以及是否是删除等。比如之前的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:
    %s
    ", buffer.String())
       return buffer.Bytes(), nil
    }

    5.4部分复合键查询GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)

    这个我在前面3.3已经说过了,只是因为那个函数即是复合键的,也是高级查询的,所以我在这里给这个函数留了一个位置。

    6.调用另外的链上代码 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))
    }
    这里需要注意,我们使用的是example02的链上代码的实例名mycc,而不是代码的名字example02.
    
    

    7.获得提案对象Proposal属性

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

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

    7.2获得Transient对象 GetTransient() (map[string][]byte, error)

    Transient是在提案中Payload对象中的一个属性,也就是ChaincodeProposalPayload.TransientMap

    7.3获得交易时间戳GetTxTimestamp() (*timestamp.Timestamp, error)

    交易时间戳也是在提案对象中获取的,提案对象的Header部分,也就是proposal.Header.ChannelHeader.Timestamp

    7.4 获得Binding对象 GetBinding() ([]byte, error)

    这个Binding对象也是从提案对象中提取并组合出来的,其中包含proposal.Header中的SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch。关于Proposal对象确实很8复杂,我目前了解的并不对,接下来得详细研究。

    8.事件设置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)
    }
    事件设置完毕后,需要在客户端也做相应的修改。由于我现在还没有做Application的开发,所以了解的还不够。以后也需要写一篇博客探讨这个话题。
    最后,大家如果想进一步探讨Fabric或者使用中遇到什么问题可以加入QQ群【494085548】大家一起讨论。
  • 相关阅读:
    Roce ofed 环境搭建与测试
    Ubuntu 1804 搭建NFS服务器
    Redhat 8.0.0 安装与网络配置
    Centos 8.1 安装与网络配置
    SUSE 15.1 系统安装
    VSpare ESXi 7.0 基本使用(模板、iso、SRIOV)
    VSpare ESXi 7.0 服务器安装
    open SUSE leap 15.1 安装图解
    KVM虚拟机网卡连接网桥
    GitHub Action一键部署配置,值得拥有
  • 原文地址:https://www.cnblogs.com/studyzy/p/7360733.html
Copyright © 2011-2022 走看看