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

深藍發表於2017-08-14

深藍前幾篇部落格講了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:\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】大家一起討論。

相關文章