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