搭建基於hyperledger fabric的聯盟社區(四) --chaincode開發


前幾章已經分別把三台虛擬機環境和配置文件准備好了,在啟動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)
	}
}

  

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM