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節點容器端口的映射:node
ports: - 7052:7052
宿主機端口是在本地啓動鏈碼鏈接Peer節點時使用的端口。Fabric 1.1版本使用7052端口,若是是Fabric 1.0版本,使用7051端口,能夠經過修改core.yaml文件修改默認端口。
進入cli容器,安裝、實例化鏈碼:git
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=鏈碼名:版本號
github
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,用於指定開發者的開發目錄。docker
進入鏈碼容器:docker exec -it chaincode bash
此時進入鏈碼容器的工做目錄,工做目錄中存放了開發者開發的鏈碼。
編譯鏈碼:數據庫
cd [鏈碼目錄] go build -o [可執行文件]
部署鏈碼CORE_PEER_ADDRESS=peer:[端口號] CORE_CHAINCODE_ID_NAME=[鏈碼實例]:0 ./[可執行文件]
退出鏈碼容器:exit
json
進入客戶端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 [通道]
bash
若是要測試新開發的鏈碼,須要將新開發的鏈碼目錄添加到chaincode子目錄下,並從新啓動chaincode-docker-devmode網絡。網絡
每一個鏈碼程序都必須實現鏈碼接口 ,接口中的方法會在響應傳來的交易時被調用。Init方法會在鏈碼接收到instantiate(實例化)或者upgrade(升級)交易時被調用,執行必要的初始化操做,包括初始化應用的狀態;Invoke方法會在響應調用交易時被調用以執行交易。
鏈碼在開發過程當中須要實現鏈碼接口,交易的類型決定了哪一個接口函數將會被調用,如instantiate和upgrade類型會調用鏈碼的Init接口,而invoke類型的交易則調用鏈碼的Invoke接口。鏈碼的接口定義以下:app
type Chaincode interface { Init(stub ChaincodeStubInterface) pb.Response Invoke(stub ChaincodeStubInterface) pb.Response }
shim.ChaincodeStubInterface接口用於訪問及修改帳本,並實現鏈碼之間的互相調用,爲編寫鏈碼的業務邏輯提供了大量實用的方法。
鏈碼的必要結構以下:
package main //引入必要的包 import( "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{ //在該方法中實現鏈碼初始化或升級時的處理邏輯 //編寫時可靈活使用stub中的API } //爲結構體添加Invoke方法 func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response{ //在該方法中實現鏈碼運行中被調用或查詢時的處理邏輯 //編寫時可靈活使用stub中的API } //主函數,須要調用shim.Start( )方法 func main() { err:=shim.Start(new(SimpleChaincode)) if err != nil { fmt.Printf("Error starting Simple chaincode: %s", err) } }
使用Go語言開發鏈碼須要定義一個struct,而後在struct上定義Init和Invoke兩個函數,定義main函數做爲鏈碼的啓動入口。
Init和Invoke都有傳入參數stub shim.ChaincodeStubInterface,爲編寫鏈碼的業務邏輯提供大量實用方法。
GetArgs() [][]byte
以byte數組的數組的形式得到傳入的參數列表 GetStringArgs() []string
以字符串數組的形式得到傳入的參數列表 GetFunctionAndParameters() (string, []string)
將字符串數組的參數分爲兩部分,數組第一個字是Function,剩下的都是Parameter GetArgsSlice() ([]byte, error)
以byte切片的形式得到參數列表function, args := stub.GetFunctionAndParameters()
鏈碼開發的核心業務邏輯就是對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)
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進行前綴匹配的查詢,不容許使用後面部分的複合鍵進行匹配。
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)) }
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 }
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)) }
(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的組合。
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) } }