Fabric的鏈碼開發(fā)調(diào)試比較繁瑣。在不使用鏈碼開發(fā)模式的情況下,鏈碼不能在本地測試,必須部署到docker,install和instantiate后,Peer節(jié)點(diǎn)會(huì)在新的容器中啟動(dòng)鏈碼。但只能通過docker logs查看鏈碼日志,通過打印日志的方式進(jìn)行鏈碼調(diào)試。如果對(duì)鏈碼進(jìn)行了修改,需要重新開始上述流程。
為了簡化Fabric鏈碼開發(fā)的調(diào)試過程,F(xiàn)abric引入了鏈碼開發(fā)模式。通常情況下,鏈碼由Peer節(jié)點(diǎn)啟動(dòng)和維護(hù),但在鏈碼開發(fā)模式下,鏈碼由用戶構(gòu)建和啟動(dòng)。鏈碼開發(fā)模式用于鏈碼開發(fā)階段中鏈碼的編碼、構(gòu)建、運(yùn)行、調(diào)試等鏈碼生命周期階段的快速轉(zhuǎn)換。
使用鏈碼開發(fā)模式,啟動(dòng)Peer節(jié)點(diǎn)仍然需要安裝、初始化鏈碼,但只需要執(zhí)行一次,并且鏈碼可以運(yùn)行在本地(比如直接在IDE啟動(dòng)),可以使用IDE的調(diào)試功能。如果對(duì)鏈碼進(jìn)行了修改,直接在IDE中編譯運(yùn)行就能在Peer節(jié)點(diǎn)看到修改后的鏈碼。
要使用鏈碼開發(fā)模式,首先修改運(yùn)行Peer節(jié)點(diǎn)容器的啟動(dòng)命令,添加--peer-chaincodedev參數(shù),例如在docker-compose.yaml中:command: peer node start --peer-chaincodedev=true
指定宿主機(jī)端口與Peer節(jié)點(diǎn)容器端口的映射:
成都創(chuàng)新互聯(lián)主營陸川網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,App定制開發(fā),陸川h5小程序制作搭建,陸川網(wǎng)站營銷推廣歡迎陸川等地區(qū)企業(yè)咨詢
ports:
- 7052:7052
宿主機(jī)端口是在本地啟動(dòng)鏈碼連接Peer節(jié)點(diǎn)時(shí)使用的端口。Fabric 1.1版本使用7052端口,如果是Fabric 1.0版本,使用7051端口,可以通過修改core.yaml文件修改默認(rèn)端口。
進(jìn)入cli容器,安裝、實(shí)例化鏈碼:
peer chaincode install -n 鏈碼名 -v 1 -p xxx.com/xxxapp
peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n 鏈碼名 -v 版本號(hào) -c '{"Args":[""]}' -P "OR ('Org1MSP.member','Org2MSP.member')"
背書策略使用-P參數(shù)指定。
如果在IDE中直接運(yùn)行鏈碼,需要先配置兩個(gè)環(huán)境變量:CORE_PEER_ADDRESS=127.0.0.1:7052 CORE_CHAINCODE_ID_NAME=鏈碼名:版本號(hào)
fabric-sample項(xiàng)目提供了Fabric開發(fā)的多個(gè)實(shí)例,其中一個(gè)提供了鏈碼開發(fā)Fabric網(wǎng)絡(luò)環(huán)境,即chaincode-docker-devmode實(shí)例。
fabric-sample項(xiàng)目地址如下:
https://github.com/hyperledger/fabric-samples
進(jìn)入chaincode-docker-devmode目錄:cd fabric-samples/chaincode-docker-devmode
啟動(dòng)Fabric鏈碼開發(fā)網(wǎng)絡(luò)環(huán)境:docker-compose -f docker-compose-simple.yaml up -d
docker-compose-simple.yaml文件在chaincode容器中指定了鏈碼代碼注入目錄為./../chaincode,用于指定開發(fā)者的開發(fā)目錄。
進(jìn)入鏈碼容器:docker exec -it chaincode bash
此時(shí)進(jìn)入鏈碼容器的工作目錄,工作目錄中存放了開發(fā)者開發(fā)的鏈碼。
編譯鏈碼:
cd [鏈碼目錄]
go build -o [可執(zhí)行文件]
部署鏈碼CORE_PEER_ADDRESS=peer:[端口號(hào)] CORE_CHAINCODE_ID_NAME=[鏈碼實(shí)例]:0 ./[可執(zhí)行文件]
退出鏈碼容器:exit
進(jìn)入客戶端cli容器:docker exec -it cli bash
安裝鏈碼
cd ..
peer chaincode install -p [鏈碼可執(zhí)行文件的所在目錄路徑] -n [鏈碼實(shí)例] -v [版本號(hào)]
實(shí)例化鏈碼peer chaincode instantiate -n [鏈碼實(shí)例] -v [版本號(hào)] -c '{"Args":["函數(shù)","參數(shù)","參數(shù)"]}' -C [通道]
調(diào)用鏈碼peer chaincode invoke -n [鏈碼實(shí)例] -c '{"Args":["函數(shù)", "參數(shù)", "參數(shù)"]}' -C [通道]
如果要測試新開發(fā)的鏈碼,需要將新開發(fā)的鏈碼目錄添加到chaincode子目錄下,并重新啟動(dòng)chaincode-docker-devmode網(wǎng)絡(luò)。
每個(gè)鏈碼程序都必須實(shí)現(xiàn)鏈碼接口?,接口中的方法會(huì)在響應(yīng)傳來的交易時(shí)被調(diào)用。Init方法會(huì)在鏈碼接收到instantiate(實(shí)例化)或者upgrade(升級(jí))交易時(shí)被調(diào)用,執(zhí)行必要的初始化操作,包括初始化應(yīng)用的狀態(tài);Invoke方法會(huì)在響應(yīng)調(diào)用交易時(shí)被調(diào)用以執(zhí)行交易。
鏈碼在開發(fā)過程中需要實(shí)現(xiàn)鏈碼接口,交易的類型決定了哪個(gè)接口函數(shù)將會(huì)被調(diào)用,如instantiate和upgrade類型會(huì)調(diào)用鏈碼的Init接口,而invoke類型的交易則調(diào)用鏈碼的Invoke接口。鏈碼的接口定義如下:
type Chaincode interface {
Init(stub ChaincodeStubInterface) pb.Response
Invoke(stub ChaincodeStubInterface) pb.Response
}
shim.ChaincodeStubInterface接口用于訪問及修改賬本,并實(shí)現(xiàn)鏈碼之間的互相調(diào)用,為編寫鏈碼的業(yè)務(wù)邏輯提供了大量實(shí)用的方法。
鏈碼的必要結(jié)構(gòu)如下:
package main
//引入必要的包
import(
"github.com/hyperledger/fabric/core/chaincode/shim"
pb"github.com/hyperledger/fabric/protos/peer"
)
//聲明一個(gè)結(jié)構(gòu)體
type SimpleChaincode struct {}
//為結(jié)構(gòu)體添加Init方法
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) pb.Response{
//在該方法中實(shí)現(xiàn)鏈碼初始化或升級(jí)時(shí)的處理邏輯
//編寫時(shí)可靈活使用stub中的API
}
//為結(jié)構(gòu)體添加Invoke方法
func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response{
//在該方法中實(shí)現(xiàn)鏈碼運(yùn)行中被調(diào)用或查詢時(shí)的處理邏輯
//編寫時(shí)可靈活使用stub中的API
}
//主函數(shù),需要調(diào)用shim.Start( )方法
func main() {
err:=shim.Start(new(SimpleChaincode))
if err != nil {
fmt.Printf("Error starting Simple chaincode: %s", err)
}
}
使用Go語言開發(fā)鏈碼需要定義一個(gè)struct,然后在struct上定義Init和Invoke兩個(gè)函數(shù),定義main函數(shù)作為鏈碼的啟動(dòng)入口。
Init和Invoke都有傳入?yún)?shù)stub shim.ChaincodeStubInterface,為編寫鏈碼的業(yè)務(wù)邏輯提供大量實(shí)用方法。
GetArgs() [][]byte
以byte數(shù)組的數(shù)組的形式獲得傳入的參數(shù)列表GetStringArgs() []string
以字符串?dāng)?shù)組的形式獲得傳入的參數(shù)列表GetFunctionAndParameters() (string, []string)
將字符串?dāng)?shù)組的參數(shù)分為兩部分,數(shù)組第一個(gè)字是Function,剩下的都是ParameterGetArgsSlice() ([]byte, error)
以byte切片的形式獲得參數(shù)列表function, args := stub.GetFunctionAndParameters()
鏈碼開發(fā)的核心業(yè)務(wù)邏輯就是對(duì)State Database的增刪改查。PutState(key string, value []byte) error
State DB是一個(gè)Key Value數(shù)據(jù)庫,增加和修改數(shù)據(jù)是統(tǒng)一的操作,如果指定的Key在數(shù)據(jù)庫中已經(jīng)存在,那么是修改操作,如果Key不存在,那么是插入操作。Key是一個(gè)字符串,Value是一個(gè)對(duì)象經(jīng)過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序列號(hào)
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
根據(jù)Key刪除State DB的數(shù)據(jù)。如果根據(jù)Key找不到對(duì)應(yīng)的數(shù)據(jù),刪除失敗。
err= stub.DelState(key)
if err != nil {
return shim.Error("Failed to delete Student from DB, key is: "+key)
}
GetState(key string) ([]byte, error)
根據(jù)Key來對(duì)數(shù)據(jù)庫進(jìn)行查詢,返回byte數(shù)組數(shù)據(jù),需要轉(zhuǎn)換為string,然后再Json反序列化,可以得到對(duì)象。
不能在一個(gè)鏈碼的函數(shù)中PutState后馬上GetState,因?yàn)檫€沒有完成,還沒有提交到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)
根據(jù)某個(gè)對(duì)象生成復(fù)合鍵,需要指定對(duì)象的類型,復(fù)合鍵涉及的屬性。
type ChooseCourse struct {
CourseNumber string //開課編號(hào)
StudentId int //學(xué)生ID
Confirm bool //是否確認(rèn)
}
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)
根據(jù)復(fù)合鍵拆分得到對(duì)象類型,屬性字符串?dāng)?shù)組
objType,attrArray,_:= stub.SplitCompositeKey(key1)
fmt.Println("Object:"+objType+" ,Attributes:"+strings.Join(attrArray,"|"))
GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error)
對(duì)Key進(jìn)行前綴匹配的查詢,不允許使用后面部分的復(fù)合鍵進(jìn)行匹配。
GetCreator() ([]byte, error)?
獲得調(diào)用本鏈碼的客戶端的用戶證書。
通過獲得當(dāng)前用戶的用戶證書,可以將用戶證書的字符串轉(zhuǎn)換為Certificate對(duì)象,然后通過Subject獲得當(dāng)前用戶的名字。
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)
提供了對(duì)某個(gè)區(qū)間的Key進(jìn)行查詢的接口,適用于任何State DB,返回一個(gè)StateQueryIteratorInterface接口。需要通過返回接口再做一個(gè)for循環(huán),讀取返回的信息。
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)
對(duì)同一個(gè)數(shù)據(jù)(Key相同)的更改,會(huì)記錄到區(qū)塊鏈中,可以通過GetHistoryForKey方法獲得對(duì)象在區(qū)塊鏈中記錄的更改歷史,包括是在哪個(gè)TxId,修改的數(shù)據(jù),修改的時(shí)間戳,以及是否是刪除等。
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
在本鏈碼中調(diào)用其它通道上已經(jīng)部署好的鏈碼。
channel:通道名稱
chaincodeName:鏈碼實(shí)例名稱
args:調(diào)用的方法、參數(shù)的數(shù)組組合
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)
從客戶端發(fā)現(xiàn)背書節(jié)點(diǎn)的Transaction或者Query都是一個(gè)提案,GetSignedProposal獲得當(dāng)前的提案對(duì)象包括客戶端對(duì)提案的簽名。提案的內(nèi)容包括提案Header,Payload和Extension。
(2)獲得Transient對(duì)象GetTransient() (map[string][]byte, error)
返回提案對(duì)象的Payload的屬性ChaincodeProposalPayload.TransientMap
(3)獲得交易時(shí)間戳GetTxTimestamp() (*timestamp.Timestamp, error)
返回提案對(duì)象的proposal.Header.ChannelHeader.Timestamp
(4)獲得Binding對(duì)象GetBinding() ([]byte, error)
返回提案對(duì)象的proposal.Header中SignatureHeader.Nonce,SignatureHeader.Creator和ChannelHeader.Epoch的組合。
SetEvent(name string, payload []byte) error
當(dāng)鏈碼提交完畢,會(huì)通過事件的方式通知客戶端,通知的內(nèi)容可以通過SetEvent設(shè)置。事件設(shè)置完畢后,需要在客戶端也做相應(yīng)的修改。
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"
)
// 簡單鏈碼實(shí)現(xiàn)
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)
// 寫入狀態(tài)到賬本
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" {
// 轉(zhuǎn)賬,將X金額從賬戶A轉(zhuǎn)賬到賬戶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\"")
}
// 轉(zhuǎn)賬交易,將X金額從賬戶A轉(zhuǎn)賬到賬戶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]
// 從賬本讀取狀態(tài)
// 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))
// 執(zhí)行交易
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)
// 將狀態(tài)寫回賬本
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)
}
}