深藍前幾篇博客講了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】大家一起討論。
文章列表