前幾章已經分別把三台虛擬機環境和配置文件准備好了,在啟動fabric網絡之前我們要准備好寫好的chaincode。chaincode的開發一般是使用GO或者JAVA,而我選擇的是GO語言。先分析一下官方最典型的一個chaincode--fabcar,然后着重介紹一下shim.ChaincodeSubInterface,最后在貼上我自己的chaincode。
一.chaincode主要框架結構
1.1 引入了4個程序庫,用於格式化、處理字節、讀取和寫入JSON,以及字符串操作。2個Hyperledger Fabric 特定的智能合同庫。shim包提供了一些 API,以便chaincode與底層區塊鏈網絡交互來訪問狀態變量、交易上下文、調用方證書和屬性,並調用其他chaincode和執行其他操作。
import ( "bytes" "encoding/json" "fmt" "strconv" "github.com/hyperledger/fabric/core/chaincode/shim" sc "github.com/hyperledger/fabric/protos/peer" )
1.2 定義SmartContract結構體,然后在該struct上定義Init和Invoke兩個函數
type SmartContract struct {
}
1.3 定義具有四個變量的汽車結構體,結構體標簽被編碼/json庫使用。
type Car struct {
Make string `json:"make"`
Model string `json:"model"`
Colour string `json:"colour"`
Owner string `json:"owner"`
}
1.4 當智能合同“fabcar”由區塊鏈網絡實例化時,Init方法被調用。在Go中通過給函數標明所屬類型,來給該類型定義方法,下面的 s *SmartContract即表示給SmartContract聲明了一個方法。在Init和Invoke的時候,都會傳入參數stub shim.ChaincodeStubInterface,這個參數提供的接口為我們編寫ChainCode的業務邏輯提供了大量的實用方法。假設一切順利,將返回一個表示初始化已經成功的sc.Response對象。
func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
return shim.Success(nil)
}
1.5 應用程序請求運行智能合約fabcar后,Invoke方法被調用。在Invoke的時候,由傳入的參數來決定我們具體調用了哪個方法,所以需要用GetFunctionAndParameters解析調用的時候傳入的參數。
func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
// 檢索請求的智能合約的函數和參數,GetFunctionAndParameters() (string, []string)將字符串數組的參數分為兩部分,數組第一個字是Function,剩下的都是Parameter
function, args := APIstub.GetFunctionAndParameters()
// 找到合適的處理函數以便與賬本進行交互
if function == "queryCar" {
return s.queryCar(APIstub, args)
} else if function == "initLedger" {
return s.initLedger(APIstub)
} else if function == "createCar" {
return s.createCar(APIstub, args)
} else if function == "queryAllCars" {
return s.queryAllCars(APIstub)
} else if function == "changeCarOwner" {
return s.changeCarOwner(APIstub, args)
}
return shim.Error("Invalid Smart Contract function name.")
}
1.6 主函數僅與單元測試模式相關,任何GO程序的起點都是main函數,在這里只是為了完整性。
func main() {
// 創建了新的智能合同,並向對等節點注冊它
err := shim.Start(new(SmartContract))
if err != nil {
fmt.Printf("Error creating new Smart Contract: %s", err)
}
}
二.對State DB (狀態數據庫)的增刪改查
2.1 查詢某輛汽車
通過GetState(key string) ([]byte, error)查詢數據。因為我們是Key Value數據庫,所以根據Key來對數據庫進行查詢,是一件很常見,很高效的操作,返回的數據是byte數組。
func (s *SmartContract) queryCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
carAsBytes, _ := APIstub.GetState(args[0])
return shim.Success(carAsBytes)
}
2.2 初始化賬本
通過PutState(key string, value []byte) error 增改數據,對於State DB來說,增加和修改數據是統一的操作,因為State DB是一個Key Value數據庫,如果我們指定的Key在數據庫中已經存在,那么就是修改操作,如果Key不存在,那么就是插入操作。對於實際的系統來說,我們的Key可能是單據編號,或者系統分配的自增ID+實體類型作為前綴,而Value則是一個對象經過JSON序列號后的字符串。
func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response {
cars := []Car{
Car{Make: "Toyota", Model: "Prius", Colour: "blue", Owner: "Tomoko"},
Car{Make: "Ford", Model: "Mustang", Colour: "red", Owner: "Brad"},
Car{Make: "Hyundai", Model: "Tucson", Colour: "green", Owner: "Jin Soo"},
Car{Make: "Volkswagen", Model: "Passat", Colour: "yellow", Owner: "Max"},
Car{Make: "Tesla", Model: "S", Colour: "black", Owner: "Adriana"},
Car{Make: "Peugeot", Model: "205", Colour: "purple", Owner: "Michel"},
Car{Make: "Chery", Model: "S22L", Colour: "white", Owner: "Aarav"},
Car{Make: "Fiat", Model: "Punto", Colour: "violet", Owner: "Pari"},
Car{Make: "Tata", Model: "Nano", Colour: "indigo", Owner: "Valeria"},
Car{Make: "Holden", Model: "Barina", Colour: "brown", Owner: "Shotaro"},
}
i := 0
for i < len(cars) {
fmt.Println("i is ", i)
carAsBytes, _ := json.Marshal(cars[i])
APIstub.PutState("CAR"+strconv.Itoa(i), carAsBytes)
fmt.Println("Added", cars[i])
i = i + 1
}
return shim.Success(nil)
}
2.3 創建汽車
把對象轉換為JSON的方法(函數)為 json.Marshal(),也就是說,這個函數接收任意類型的數據 v,並轉換為字節數組類型,返回值就是我們想要的JSON數據和一個錯誤代碼。當轉換成功的時候,這個錯誤代碼為nil。
func (s *SmartContract) createCar(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 5 {
return shim.Error("Incorrect number of arguments. Expecting 5")
}
var car = Car{Make: args[1], Model: args[2], Colour: args[3], Owner: args[4]}
carAsBytes, _ := json.Marshal(car)
APIstub.PutState(args[0], carAsBytes)
return shim.Success(nil)
}
2.4 查詢所有汽車
Key區間查詢GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) 提供了對某個區間的Key進行查詢的接口,適用於任何State DB。由於返回的是一個StateQueryIteratorInterface(迭代器)接口,我們需要通過這個接口再做一個for循環,才能讀取返回的信息,所有我們可以獨立出一個方法,專門將該接口返回的數據以string的byte數組形式返回。
func (s *SmartContract) queryAllCars(APIstub shim.ChaincodeStubInterface) sc.Response {
startKey := "CAR0"
endKey := "CAR999"
resultsIterator, err := APIstub.GetStateByRange(startKey, endKey)
if err != nil {
return shim.Error(err.Error())
}
//defer關鍵字用來標記最后執行的Go語句,一般用在資源釋放、關閉連接等操作,會在函數關閉前調用。多個defer的定義與執行類似於棧的操作:先進后出,最先定義的最后執行。
defer resultsIterator.Close()
// buffer 是一個包含查詢結果的JSON數組,bytes.buffer是一個緩沖byte類型的緩沖器存放着都是byte,這樣直接定義一個 Buffer 變量,而不用初始化。
var buffer bytes.Buffer
buffer.WriteString("["]
bArrayMemberAlreadyWritten := false
//迭代器的兩個方法,hasNext:沒有指針下移操作,只是判斷是否存在下一個元素。next:指針下移,返回該指針所指向的元素。
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
// 在數組成員前加一個逗號
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")
buffer.WriteString(", \"Record\":")
// Record 是一個JSON對象,所以我們按原樣寫
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
fmt.Printf("- queryAllCars:\n%s\n", buffer.String())
return shim.Success(buffer.Bytes())
}
2.5 改變汽車擁有人
Unmarshal是用於反序列化json的函數根據data將數據反序列化到傳入的對象中。
func (s *SmartContract) changeCarOwner(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 2 {
return shim.Error("Incorrect number of arguments. Expecting 2")
}
carAsBytes, _ := APIstub.GetState(args[0])
car := Car{}
json.Unmarshal(carAsBytes, &car)
car.Owner = args[1]
carAsBytes, _ = json.Marshal(car)
APIstub.PutState(args[0], carAsBytes)
return shim.Success(nil)
}
以上就是官方鏈代碼fabcar的分析
三.鏈碼開發API
作為chaincode中的利器shim.ChaincodeSubInterface提供了一系列API供開發者在編寫鏈碼時靈活選擇使用。
這些API可分為四類:賬本狀態交互API、交易信息相關API、參數讀取API、其他API,下面分別介紹。
3.1賬本狀態交互API
chaincode需要將一些數據記錄在分布式賬本中。需要記錄的數據稱為狀態state,以鍵值對(key-value)的形式存儲。賬本狀態API可以對賬本狀態進行操作,十分重要。方法的調用會更新交易提案的讀寫集和,在committer進行驗證時會在此執行,跟賬本狀態進行比對,這類API的大致功能如下:

3.2交易信息相關API
交易信息相關API可以獲取到與交易信息自身相關的數據。用戶對鏈碼的調用(初始化和升級時調用Init()方法,運行時調用Invoke()方法)過程中會產生交易提案。這些API支持查詢當前交易提案的一些屬性,具體信息如下:

3.3 參數讀取API
調用鏈碼時支持傳入若干參數,參數可通過API讀取。具體信息如下:

3.4其他API
除了上面一些API以外還有一些輔助API,如下:

四. 社區聯盟chaincode
4.1 業務功能需求
4.1.1 AddPost(Post)
本函數功能為存入一個帖子。
參數:
Post 為JSON 格式數據,包含帖子的的基本信息。須將字段名作為Key,字段的值作為 Value。
返回值:
-1:操作不成功
某個正整數:操作成功,返回值表示新帖的ID
4.1.2 UpdatePost(Post)
本函數功能為更改一個已經存在的帖子。
參數:
Post 為JSON 格式數據,包含帖子的的基本信息,其中帖子ID不可為空。須將字段名作為Key,字段的值作為 Value。
返回值:
-1:操作不成功
1:操作成功
4.1.3 RichQueryPost (Attribute, Operator, Value)
本函數功能為查找帖子。
參數:
Attribute 表示帖子的屬性名稱,須符合附件一中的字段名。
Operator 表示比較運算符的代碼。如下所示
0: =
1: >
2: >=
3: <
4: <=
5: between
6: like
Value 為屬性的值。若Operator 為0(等於),則本函數將返回對應屬性中等於指定Value的所有帖子;若Operator為1~4,則Value 視為單個值(單個數值或單個字符串);若Operator為5,則Value須為兩個值,中間以逗號分開;若Operator為6,則本函數將返回對應屬性中包含指定Value的所有帖子。
返回值:
-1:操作不成功
JSON格式字符串:操作成功,所有符合條件的帖子將組織為JSON格式的數組,數組中每個元素為一個帖子。
注:目前僅支持單屬性的查找。
4.1.4 GetPostNum(Attribute, Operator, Value)
本函數功能為查找返回某個條件的帖子的數量。
參數的含義與函數QueryPost相同。
返回值:
-1:操作不成功
某個正整數:操作成功,返回值符合查詢條件的帖子的數量
4.2 編寫chaincode遇到的問題
在實現有transaction功能的函數時,在函數里面寫一個返回值,並不能像查詢類型的函數一樣在用fabric SDK調用時得到對應的返回值。addpost功能需要返回新帖子的ID,但是我的帖子id正好就是鍵值對的數量,我通過fabric node SDK 返回了鍵值對的數量,也就是返回了新帖子的ID。
RichQueryPost和GetPostNum兩個函數都會用到富查詢。富查詢的語法可以參考couchdb官方文檔關於Selector語法部分的介紹:
http://docs.couchdb.org/en/2.0.0/api/database/find.html#find-selectors
https://github.com/cloudant/mango
但是模糊查詢功能暫時沒有實現。(下個版本會實現)
4.3 chaincode代碼
我們將自己編寫符合業務邏輯的chaincode放在peer0.org1和peer0.org2的/go/src/github.com/hyperledger/fabric/examples/chaincode/go/community
目錄下,之后啟動docker容器的時候會自動掛載chaincode
cd ~/go/src/github.com/hyperledger/fabric/examples/chaincode/go/
mkdir community
完整chaincode代碼如下
package main
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/hyperledger/fabric/core/chaincode/shim"
sc "github.com/hyperledger/fabric/protos/peer"
)
type SmartContract struct {
}
type Post struct {
Id string `json:"id"`
OriginalWebsite string `json:"originalwebsite"`
OriginalID string `json:"originalid"`
Title string `json:"title"`
Content string `json:"content"`
AuthorId string `json:"authorid"`
PublishTime string `json:"publishtime"`
UpdateTime string `json:"updatetime"`
Category string `json:"category"`
SourceId string `json:"sourceid"`
Labels string `json:"labels"`
Follower_num int `json:"follower_num"`
Browse_num int `json:"browse_num"`
Star_num int `json:"star_num"`
}
type PostLength struct {
Length int `json:"length"`
}
func (s *SmartContract) Init(APIstub shim.ChaincodeStubInterface) sc.Response {
return shim.Success(nil)
}
func (s *SmartContract) Invoke(APIstub shim.ChaincodeStubInterface) sc.Response {
function, args := APIstub.GetFunctionAndParameters()
if function == "queryPost" {
return s.queryPost(APIstub, args)
} else if function == "initLedger" {
return s.initLedger(APIstub)
} else if function == "addPost" {
return s.addPost(APIstub, args)
} else if function == "updatePost" {
return s.updatePost(APIstub, args)
} else if function == "richQueryPosts" {
return s.richQueryPosts(APIstub, args)
} else if function == "getPostNum" {
return s.getPostNum(APIstub, args)
}
return shim.Error("Invalid Smart Contract function name.")
}
func (s *SmartContract) queryPost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 1 {
return shim.Error("Incorrect number of arguments. Expecting 1")
}
postAsBytes, _ := APIstub.GetState(args[0])
return shim.Success(postAsBytes)
}
func (s *SmartContract) initLedger(APIstub shim.ChaincodeStubInterface) sc.Response {
posts := []Post{
Post{Id: "1", OriginalWebsite: "b", OriginalID: "c", Title: "d",Content:"e",AuthorId:"f",PublishTime:"g",UpdateTime:"h",Category:"i",SourceId:"j",Labels:"k",Follower_num:100,Browse_num:200,Star_num:300},
Post{Id: "2", OriginalWebsite: "bb", OriginalID: "bb", Title: "dd",Content:"ee",AuthorId:"ff",PublishTime:"gg",UpdateTime:"hh",Category:"ii",SourceId:"jj",Labels:"kk",Follower_num:400,Browse_num:500,Star_num:600},
}
length := PostLength{Length:len(posts)}
lengthAsBytes,_ := json.Marshal(length)
APIstub.PutState("POSTLENGTH",lengthAsBytes)
i := 0
for i < len(posts) {
fmt.Println("i is ", i)
postAsBytes, _ := json.Marshal(posts[i])
APIstub.PutState("POST"+strconv.Itoa(i), postAsBytes)
fmt.Println("Added", posts[i])
i = i + 1
}
return shim.Success(nil)
}
func (s *SmartContract) addPost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 13 {
return shim.Error("Incorrect number of arguments. Expecting 13")
}
args10,error := strconv.Atoi(args[10])
args11,error := strconv.Atoi(args[11])
args12,error := strconv.Atoi(args[12])
if error != nil{
fmt.Println("String conversion integer failed!")
}
lengthAsBytes, _ := APIstub.GetState("POSTLENGTH")
length := PostLength{}
json.Unmarshal(lengthAsBytes,&length)
newlength := length.Length+1
var post = Post{Id: strconv.Itoa(newlength), OriginalWebsite: args[0], OriginalID: args[1], Title: args[2],Content:args[3],AuthorId:args[4],PublishTime:args[5],UpdateTime:args[6],Category:args[7],SourceId:args[8],Labels:args[9],Follower_num:args10,Browse_num:args11,Star_num:args12}
postAsBytes, _ := json.Marshal(post)
APIstub.PutState("POST"+strconv.Itoa(newlength), postAsBytes)
length.Length = newlength
lengthAsBytes,_ = json.Marshal(length)
APIstub.PutState("POSTLENGTH",lengthAsBytes)
return shim.Success(lengthAsBytes)
}
func (s *SmartContract) updatePost(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 14 {
return shim.Error("Incorrect number of arguments. Expecting 14")
}
args11,error := strconv.Atoi(args[11])
args12,error := strconv.Atoi(args[12])
args13,error := strconv.Atoi(args[13])
if error != nil{
fmt.Println("String conversion integer failed!")
}
var post = Post{Id: args[0], OriginalWebsite: args[1], OriginalID: args[2], Title: args[3],Content:args[4],AuthorId:args[5],PublishTime:args[6],UpdateTime:args[7],Category:args[8],SourceId:args[9],Labels:args[10],Follower_num:args11,Browse_num:args12,Star_num:args13}
postAsBytes, _ := json.Marshal(post)
APIstub.PutState("POST"+args[0], postAsBytes)
return shim.Success(nil)
}
func (s *SmartContract) richQueryPosts(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 3 {
return shim.Error("Incorrect number of arguments. Expecting 3")
}
var queryString string
if args[1] == "0" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":\"%s\"}}", args[0],args[2])
} else if args[1] == "1" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gt\":%s}}}", args[0],args[2])
} else if args[1] == "2" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gte\":%s}}}", args[0],args[2])
} else if args[1] == "3" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lt\":%s}}}", args[0],args[2])
} else if args[1] == "4" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lte\":%s}}}", args[0],args[2])
} else if args[1] == "5" {
between := strings.Split(args[2], ",")
queryString = fmt.Sprintf("{\"selector\":{\"$and\":[{\"%s\":{\"$gte\":%s}},{\"%s\":{\"$lte\":%s}}]}}", args[0],between[0],args[0],between[1])
} else {
return shim.Error("Incorrect number of arguments. Expecting 0~5")
}
resultsIterator, err := APIstub.GetQueryResult(queryString)
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()
var buffer bytes.Buffer
buffer.WriteString("[")
bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return shim.Error(err.Error())
}
if bArrayMemberAlreadyWritten == true {
buffer.WriteString(",")
}
buffer.WriteString("{\"Key\":")
buffer.WriteString("\"")
buffer.WriteString(queryResponse.Key)
buffer.WriteString("\"")
buffer.WriteString(", \"Record\":")
buffer.WriteString(string(queryResponse.Value))
buffer.WriteString("}")
bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")
fmt.Printf("- richQueryPosts:\n%s\n", buffer.String())
return shim.Success(buffer.Bytes())
}
func (s *SmartContract) getPostNum(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {
if len(args) != 3 {
return shim.Error("Incorrect number of arguments. Expecting 3")
}
var queryString string
if args[1] == "0" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":\"%s\"}}", args[0],args[2])
} else if args[1] == "1" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gt\":%s}}}", args[0],args[2])
} else if args[1] == "2" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$gte\":%s}}}", args[0],args[2])
} else if args[1] == "3" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lt\":%s}}}", args[0],args[2])
} else if args[1] == "4" {
queryString = fmt.Sprintf("{\"selector\":{\"%s\":{\"$lte\":%s}}}", args[0],args[2])
} else if args[1] == "5" {
between := strings.Split(args[2], ",")
queryString = fmt.Sprintf("{\"selector\":{\"$and\":[{\"%s\":{\"$gte\":%s}},{\"%s\":{\"$lte\":%s}}]}}", args[0],between[0],args[0],between[1])
} else {
return shim.Error("Incorrect number of arguments. Expecting 0~5")
}
resultsIterator, err := APIstub.GetQueryResult(queryString)
if err != nil {
return shim.Error(err.Error())
}
defer resultsIterator.Close()
i := 0
for resultsIterator.HasNext() {
resultsIterator.Next()
i = i + 1
}
fmt.Printf("- getPostNum:\n%s\n", strconv.Itoa(i))
return shim.Success([]byte(strconv.Itoa(i)))
}
func main() {
err := shim.Start(new(SmartContract))
if err != nil {
fmt.Printf("Error creating new Smart Contract: %s", err)
}
}
