HyperLedger Fabric ChainCode開發——shim.ChaincodeStubInterface用法

深藍前幾篇博客講了Fabric的環境搭建,在環境搭建好後,咱們就能夠進行Fabric的開發工做了。Fabric的開發主要分紅2部分,ChainCode鏈上代碼開發和基於SDK的Application開發。咱們這裏先講ChainCode的開發。Fabric的鏈上代碼支持Java或者Go語言進行開發,由於Fabric自己是Go開發的,因此深藍建議仍是用Go進行ChainCode的開發。git

ChainCode的Go代碼須要定義一個SimpleChaincode這樣一個struct,而後在該struct上定義Init和Invoke兩個函數,而後還要定義一個main函數,做爲ChainCode的啓動入口。如下是ChainCode的模板:github

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,而後插入一個學生數據,對於的代碼應該是這樣的:json

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進行前綴匹配的查詢,也就是說,咱們雖然是部分複合鍵的查詢,可是不容許拿後面部分的複合鍵進行匹配,必須是前面部分。spa

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-----code

咱們常見的需求是在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:\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)
}

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:\n%s\n", 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】你們一塊兒討論。
相關文章
相關標籤/搜索