1. 程式人生 > >HyperLeger Fabric開發(七)——HyperLeger Fabric鏈碼開發

HyperLeger Fabric開發(七)——HyperLeger Fabric鏈碼開發

HyperLeger Fabric開發(七)——HyperLeger Fabric鏈碼開發

一、鏈碼開發模式

1、鏈碼開發模式簡介

Fabric的鏈碼開發除錯比較繁瑣。在不使用鏈碼開發模式的情況下,鏈碼不能在本地測試,必須部署到docker,install和instantiate後,Peer節點會在新的容器中啟動鏈碼。但只能通過docker logs檢視鏈碼日誌,通過列印日誌的方式進行鏈碼除錯。如果對鏈碼進行了修改,需要重新開始上述流程。
為了簡化Fabric鏈碼開發的除錯過程,Fabric引入了鏈碼開發模式。通常情況下,鏈碼由Peer節點啟動和維護,但在鏈碼開發模式下,鏈碼由使用者構建和啟動。鏈碼開發模式用於鏈碼開發階段中鏈碼的編碼、構建、執行、除錯等鏈碼生命週期階段的快速轉換。
使用鏈碼開發模式,啟動Peer節點仍然需要安裝、初始化鏈碼,但只需要執行一次,並且鏈碼可以執行在本地(比如直接在IDE啟動),可以使用IDE的除錯功能。如果對鏈碼進行了膝蓋,直接在IDE中編譯執行就能在Peer節點看到修改後的鏈碼。
要使用鏈碼開發模式,首先修改執行Peer節點容器的啟動命令,新增--peer-chaincodedev引數,例如在docker-compose.yaml中:
command: peer node start --peer-chaincodedev=true


指定宿主機埠與Peer節點容器埠的對映:

ports: 
    - 7052:7052

宿主機埠是在本地啟動鏈碼連線Peer節點時使用的埠。Fabric 1.1版本使用7052埠,如果是Fabric 1.0版本,使用7051埠,可以通過修改core.yaml檔案修改預設埠。
進入cli容器,安裝、例項化鏈碼:

peer chaincode install -n 鏈碼名 -v 1 -p xxx.com/xxxapp
peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n 鏈碼名 -v 版本號 -c '{"Args":[""]}' -P "OR ('Org1MSP.member','Org2MSP.member')"

背書策略使用-P引數指定。
如果在IDE中直接執行鏈碼,需要先配置兩個環境變數:
CORE_PEER_ADDRESS=127.0.0.1:7052 CORE_CHAINCODE_ID_NAME=鏈碼名:版本號

2、鏈碼開發環境部署

fabric-sample專案提供了Fabric開發的多個例項,其中一個提供了鏈碼開發Fabric網路環境,即chaincode-docker-devmode例項。
fabric-sample專案地址如下:
https://github.com/hyperledger/fabric-samples
進入chaincode-docker-devmode目錄:
cd fabric-samples/chaincode-docker-devmode


啟動Fabric鏈碼開發網路環境:
docker-compose -f docker-compose-simple.yaml up -d
docker-compose-simple.yaml檔案在chaincode容器中指定了鏈碼程式碼注入目錄為./../chaincode,用於指定開發者的開發目錄。

3、編譯鏈碼

進入鏈碼容器:
docker exec -it chaincode bash
此時進入鏈碼容器的工作目錄,工作目錄中存放了開發者開發的鏈碼。
編譯鏈碼:

cd [鏈碼目錄]
go build -o [可執行檔案]

部署鏈碼
CORE_PEER_ADDRESS=peer:[埠號] CORE_CHAINCODE_ID_NAME=[鏈碼例項]:0 ./[可執行檔案]
退出鏈碼容器:
exit

4、測試鏈碼

進入客戶端cli容器:
docker exec -it cli bash
安裝鏈碼

cd ..
peer chaincode install -p [鏈碼可執行檔案的所在目錄路徑] -n [鏈碼例項] -v [版本號]

例項化鏈碼
peer chaincode instantiate -n [鏈碼例項] -v [版本號] -c '{"Args":["函式","引數","引數"]}' -C [通道]
呼叫鏈碼
peer chaincode invoke -n [鏈碼例項] -c '{"Args":["函式", "引數", "引數"]}' -C [通道]

5、測試新鏈碼

如果要測試新開發的鏈碼,需要將新開發的鏈碼目錄新增到chaincode子目錄下,並重新啟動chaincode-docker-devmode網路。

二、鏈碼的結構

1、鏈碼API

每個鏈碼程式都必須實現鏈碼介面 ,介面中的方法會在響應傳來的交易時被呼叫。Init方法會在鏈碼接收到instantiate(例項化)或者upgrade(升級)交易時被呼叫,執行必要的初始化操作,包括初始化應用的狀態;Invoke方法會在響應呼叫交易時被呼叫以執行交易。
鏈碼在開發過程中需要實現鏈碼介面,交易的型別決定了哪個介面函式將會被呼叫,如instantiate和upgrade型別會呼叫鏈碼的Init介面,而invoke型別的交易則呼叫鏈碼的Invoke介面。鏈碼的介面定義如下:

type Chaincode interface {
   Init(stub ChaincodeStubInterface) pb.Response
   Invoke(stub ChaincodeStubInterface) pb.Response
}

shim.ChaincodeStubInterface介面用於訪問及修改賬本,並實現鏈碼之間的互相呼叫,為編寫鏈碼的業務邏輯提供了大量實用的方法。

2、鏈碼的基本結構

鏈碼的必要結構如下:

使用Go語言開發鏈碼需要定義一個struct,然後在struct上定義Init和Invoke兩個函式,定義main函式作為鏈碼的啟動入口。
Init和Invoke都有傳入引數stub shim.ChaincodeStubInterface,為編寫鏈碼的業務邏輯提供大量實用方法。

三、shim.ChaincodeStubInterface介面

1、獲得呼叫的引數

GetArgs() [][]byte
以byte陣列的陣列的形式獲得傳入的引數列表
GetStringArgs() []string
以字串陣列的形式獲得傳入的引數列表
GetFunctionAndParameters() (string, []string)
將字串陣列的引數分為兩部分,陣列第一個字是Function,剩下的都是Parameter
GetArgsSlice() ([]byte, error)
以byte切片的形式獲得引數列表
function, args := stub.GetFunctionAndParameters()

2、增刪改查State DB

鏈碼開發的核心業務邏輯就是對State Database的增刪改查。
PutState(key string, value []byte) error
State DB是一個Key Value資料庫,增加和修改資料是統一的操作,如果指定的Key在資料庫中已經存在,那麼是修改操作,如果Key不存在,那麼是插入操作。Key是一個字串,Value是一個物件經過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!"))
}

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

GetState(key string) ([]byte, error)
根據Key來對資料庫進行查詢,返回byte陣列資料,需要轉換為string,然後再Json反序列化,可以得到物件。
不能在一個鏈碼的函式中PutState後馬上GetState,因為還沒有完成,還沒有提交到StateDB裡。

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)

3、複合鍵處理

CreateCompositeKey(objectType string, attributes []string) (string, error)
根據某個物件生成複合鍵,需要指定物件的型別,複合鍵涉及的屬性。

type ChooseCourse struct {
   CourseNumber string //開課編號
   StudentId int //學生ID
   Confirm bool //是否確認
}
cc:=ChooseCourse{"CS101",123,true}
var key1,_= stub.CreateCompositeKey("ChooseCourse",[]string{cc.CourseNumber,strconv.Itoa(cc.StudentId)})
fmt.Println(key1)

SplitCompositeKey(compositeKey string) (string, []string, error)
根據複合鍵拆分得到物件型別,屬性字串陣列

objType,attrArray,_:= stub.SplitCompositeKey(key1)
fmt.Println("Object:"+objType+" ,Attributes:"+strings.Join(attrArray,"|"))
GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)

對Key進行字首匹配的查詢,不允許使用後面部分的複合鍵進行匹配。

4、獲取當前使用者證書

GetCreator() ([]byte, error) 
獲得呼叫本鏈碼的客戶端的使用者證書。
通過獲得當前使用者的使用者證書,可以將使用者證書的字串轉換為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、高階查詢

GetStateByRange(startKey, endKey string)** (StateQueryIteratorInterface, error)
提供了對某個區間的Key進行查詢的介面,適用於任何State DB,返回一個StateQueryIteratorInterface介面。需要通過返回介面再做一個for迴圈,讀取返回的資訊。

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

GetQueryResult(query string) (StateQueryIteratorInterface, error)
富查詢,CouchDB才能使用。

func (t *SimpleChaincode) testRichQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{
   name:="Lee"
   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)
}

GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error)
對同一個資料(Key相同)的更改,會記錄到區塊鏈中,可以通過GetHistoryForKey方法獲得物件在區塊鏈中記錄的更改歷史,包括是在哪個TxId,修改的資料,修改的時間戳,以及是否是刪除等。

func (t *SimpleChaincode) testHistoryQuery(stub shim.ChaincodeStubInterface, args []string) pb.Response{
   student1:=Student{1,"Lee"}
   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
}

6、呼叫鏈碼

InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response
在本鏈碼中呼叫其它通道上已經部署好的鏈碼。
channel:通道名稱
chaincodeName:鏈碼例項名稱
args:呼叫的方法、引數的陣列組合

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

7、獲取提案物件Proposal屬性

(1)獲得簽名的提案
GetSignedProposal() (*pb.SignedProposal, error)
從客戶端發現背書節點的Transaction或者Query都是一個提案,GetSignedProposal獲得當前的提案物件包括客戶端對提案的簽名。提案的內容包括提案Header,Payload和Extension。
(2)獲得Transient物件
GetTransient() (map[string][]byte, error)
返回提案物件的Payload的屬性ChaincodeProposalPayload.TransientMap
(3)獲得交易時間戳
GetTxTimestamp() (*timestamp.Timestamp, error)
返回提案物件的proposal.Header.ChannelHeader.Timestamp
(4)獲得Binding物件
GetBinding() ([]byte, error)
返回提案物件的proposal.Header中SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch的組合。

8、事件設定

SetEvent(name string, payload []byte) error
當鏈碼提交完畢,會通過事件的方式通知客戶端,通知的內容可以通過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)
}

四、鏈碼開發示例

package main

import (
   "fmt"
   "strconv"

   "github.com/hyperledger/fabric/core/chaincode/lib/cid"
   "github.com/hyperledger/fabric/core/chaincode/shim"
   pb "github.com/hyperledger/fabric/protos/peer"
)

// 簡單鏈碼實現
type SimpleChaincode struct {
}

// Init初始化鏈碼
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response {

   fmt.Println("abac Init")
   err := cid.AssertAttributeValue(stub, "abac.init", "true")
   if err != nil {
      return shim.Error(err.Error())
   }

   _, 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")
   }

   // 初始化鏈碼
   A = args[0]
   Aval, err = strconv.Atoi(args[1])
   if err != nil {
      return shim.Error("Expecting integer value for asset holding")
   }
   B = args[2]
   Bval, err = strconv.Atoi(args[3])
   if err != nil {
      return shim.Error("Expecting integer value for asset holding")
   }
   fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)

   // 寫入狀態到賬本
   err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
   if err != nil {
      return shim.Error(err.Error())
   }

   err = stub.PutState(B, []byte(strconv.Itoa(Bval)))
   if err != nil {
      return shim.Error(err.Error())
   }

   return shim.Success(nil)
}

func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
   fmt.Println("abac Invoke")
   function, args := stub.GetFunctionAndParameters()
   if function == "invoke" {
      // 轉賬,將X金額從賬戶A轉賬到賬戶B
      return t.invoke(stub, args)
   } else if function == "delete" {
      // 刪除賬戶
      return t.delete(stub, args)
   } else if function == "query" {
      return t.query(stub, args)
   }

   return shim.Error("Invalid invoke function name. Expecting \"invoke\" \"delete\" \"query\"")
}

// 轉賬交易,將X金額從賬戶A轉賬到賬戶B
func (t *SimpleChaincode) invoke(stub shim.ChaincodeStubInterface, args []string) pb.Response {
   var A, B string    // Entities
   var Aval, Bval int // Asset holdings
   var X int          // Transaction value
   var err error

   if len(args) != 3 {
      return shim.Error("Incorrect number of arguments. Expecting 3")
   }

   A = args[0]
   B = args[1]

   // 從賬本讀取狀態
   // TODO: will be nice to have a GetAllState call to ledger
   Avalbytes, err := stub.GetState(A)
   if err != nil {
      return shim.Error("Failed to get state")
   }
   if Avalbytes == nil {
      return shim.Error("Entity not found")
   }
   Aval, _ = strconv.Atoi(string(Avalbytes))

   Bvalbytes, err := stub.GetState(B)
   if err != nil {
      return shim.Error("Failed to get state")
   }
   if Bvalbytes == nil {
      return shim.Error("Entity not found")
   }
   Bval, _ = strconv.Atoi(string(Bvalbytes))

   // 執行交易
   X, err = strconv.Atoi(args[2])
   if err != nil {
      return shim.Error("Invalid transaction amount, expecting a integer value")
   }
   Aval = Aval - X
   Bval = Bval + X
   fmt.Printf("Aval = %d, Bval = %d\n", Aval, Bval)

   // 將狀態寫回賬本
   err = stub.PutState(A, []byte(strconv.Itoa(Aval)))
   if err != nil {
      return shim.Error(err.Error())
   }

   err = stub.PutState(B, []byte(strconv.Itoa(Bval)))
   if err != nil {
      return shim.Error(err.Error())
   }

   return shim.Success(nil)
}

// 刪除賬戶
func (t *SimpleChaincode) delete(stub shim.ChaincodeStubInterface, args []string) pb.Response {
   if len(args) != 1 {
      return shim.Error("Incorrect number of arguments. Expecting 1")
   }

   A := args[0]

   // Delete the key from the state in ledger
   err := stub.DelState(A)
   if err != nil {
      return shim.Error("Failed to delete state")
   }

   return shim.Success(nil)
}

// query callback representing the query of a chaincode
func (t *SimpleChaincode) query(stub shim.ChaincodeStubInterface, args []string) pb.Response {
   var A string // Entities
   var err error

   if len(args) != 1 {
      return shim.Error("Incorrect number of arguments. Expecting name of the person to query")
   }

   A = args[0]

   // Get the state from the ledger
   Avalbytes, err := stub.GetState(A)
   if err != nil {
      jsonResp := "{\"Error\":\"Failed to get state for " + A + "\"}"
      return shim.Error(jsonResp)
   }

   if Avalbytes == nil {
      jsonResp := "{\"Error\":\"Nil amount for " + A + "\"}"
      return shim.Error(jsonResp)
   }

   jsonResp := "{\"Name\":\"" + A + "\",\"Amount\":\"" + string(Avalbytes) + "\"}"
   fmt.Printf("Query Response:%s\n", jsonResp)
   return shim.Success(Avalbytes)

}

func main() {
   err := shim.Start(new(SimpleChaincode))
   if err != nil {
      fmt.Printf("Error starting Simple chaincode: %s", err)
   }
}