-
智能合約是什么?
chaincode(鏈碼)稱為智能合約,是一段程序,由Go、node.js、或JAVA編寫,來實現一些預定義接口,鏈碼運行在一個和背書節點分開的獨立進程中,通過應用程序提交的交易來初始化和管理賬本狀態。
智能合約一般處理網絡中的成員一致同意的商業邏輯,所以它類似於“智能合約”。鏈碼可以在提案交易中被調用用來升級或者查詢賬本。賦予適當的權限,鏈碼就可以調用其他鏈碼訪問它的狀態,不管是在同一個通道還是不同的通道。注意,如果被調用鏈碼和調用鏈碼在不同的通道,就只能執行只讀查詢。就是說,調用不同通道的鏈碼只能進行“查詢”,在提交的子語句中不能參與狀態的合法性檢查。
智能合約是一種旨在以信息化方式傳播、驗證或執行合同的計算機協議。智能合約允許在沒有第三方的情況下進行可信交易。這些交易可追蹤且不可逆轉,Fabric為智能合約提供了一個可信的執行環境。
-
智能合約的編寫
每一個chaincode 程序都必須實現 Chaincode 接口,其中2個接口是每個程序必不可少的,Init 和Invoke接口(如下)。接口詳情詳見:https://pkg.go.dev/github.com/hyperledger/fabric-chaincode-go/shim#Chaincode
type Chaincode interface { // Init is called during Instantiate transaction after the chaincode container // has been established for the first time, allowing the chaincode to // initialize its internal data Init(stub ChaincodeStubInterface) pb.Response // Invoke is called to update or query the ledger in a proposal transaction. // Updated state variables are not committed to the ledger until the // transaction is committed. Invoke(stub ChaincodeStubInterface) pb.Response }
所以一個最小的合約實例結構如下:
package main import ( "github.com/hyperledger/fabric/core/chaincode/shim" pb "github.com/hyperledger/fabric/protos/peer" ) type SimpleChaincode struct {}
func (t *SimpleChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response { //初始化邏輯
} func (t *SimpleChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response { //其他業務邏輯
} func main() { if err := shim.Start(new(SimpleChaincode)); err != nil { // TODO: logger error } }
可結合官方GitHub中的fabric-samples的智能合約chaincode_example02,此處不粘貼其代碼
當智能合約寫完后,就可以部署在fabric鏈上了
鏈碼相關API
shim 包提供了如下幾種類型的接口:
- 參數解析 API:調用鏈碼時需要給被調用的目標函數/方法傳遞參數,該 API 提供解析這些參數的方法
- 賬本狀態數據操作 API:該 API 提供了對賬本數據狀態進行操作的方法,包括對狀態數據的查詢及事務處理等
- 交易信息獲取 API:獲取提交的交易信息的相關 API
- 對 PrivateData 操作的 API: Hyperledger Fabric 在 1.2.0 版本中新增的對私有數據操作的相關 API
- 其他 API:其他的 API,包括事件設置、調用其他鏈碼操作
參數解析API
// 返回調用鏈碼時指定提供的參數列表(以字符串數組形式返回)
GetStringArgs() []string
// 返回調用鏈碼時在交易提案中指定提供的被調用的函數名稱及函數的參數列表(以字符串數組形式返回)
GetFunctionAndParameters() (function string, params []string)
// 返回提交交易提案時提供的參數列表(以字節串數組形式返回)
GetArgsSlice() ([]byte, error)
// 返回調用鏈碼時在交易提案中指定提供的被調用的函數名稱及函數的參數列表(以字符串數組形式返回)
GetArgs() [][]byte
一般使用 GetFunctionAndParameters() 及 GetStringArgs() 。
賬本數據狀態操作API
// 查詢賬本,返回指定鍵對應的值 GetState(key string) ([]byte, error) // 嘗試添加/更新賬本中的一對鍵值 // 這一對鍵值會被添加到寫集合中,等待 Committer 進一步確認,驗證通過后才會真正寫入到賬本 PutState(key string, value []byte) error // 嘗試刪除賬本中的一對鍵值 // 同樣,對該對鍵值刪除會添加到寫集合中,等待 Committer 進一步確認,驗證通過后才會真正寫入到賬本 DelState(key string) error // 查詢指定范圍的鍵值,startKey 和 endkey 分別指定開始(包括)和終止(不包括),當為空時默認是最大范圍 // 返回結果是一個迭代器結構,可以按照字典序迭代每個鍵值對,最后需要調用 Close() 方法關閉 GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) // 返回指定鍵的所有歷史值。該方法的使用需要節點配置中打開歷史數據庫特性(ledger.history.enableHistoryDatabase=true) GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error) // 給定一組屬性(attributes),將這些屬性組合起來構造返回一個復合鍵 // 例如:CreateComositeKey("name-age",[]string{"Alice", "12"}); CreateCompositeKey(objectType string, attributes []string) (string, error) // 將指定的復合鍵進行分割,拆分成構造復合鍵時所用的屬性 SplitCompositeKey(compositeKey string) (string, []string, error) // 根據局部的復合鍵(前綴)返回所有匹配的鍵值,即與賬本中的鍵進行前綴匹配 // 返回結果是一個迭代器結構,可以按照字典序迭代每個鍵值對,最后需要調用 Close() 方法關閉 GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error) // 對(支持富查詢功能的)狀態數據庫進行富查詢,返回結果是一個迭代器結構,目前只支持 CouchDB // 注意該方法不會被 Committer 重新執行進行驗證,所以不能用於更新賬本狀態的交易中 GetQueryResult(query string) (StateQueryIteratorInterface, error)
注意: 通過 put 寫入的數據狀態不能立刻 get 到,因為 put 只是鏈碼執行的模擬交易(防止重復提交攻擊),並不會真正將狀態保存到賬本中,必須經過 Orderer 達成共識之后,將數據狀態保存在區塊中,然后保存在各 peer 節點的賬本中。
交易信息相關API
// 返回交易提案中指定的交易 ID。 // 一般情況下,交易 ID 是客戶端提交提案時由 Nonce 隨機串和簽名者身份信息哈希產生的數字摘要 GetTxID() string // 返回交易提案中指定的 Channel ID GetChannelID() string // 返回交易被創建時的客戶端打上的的時間戳 // 這個時間戳是直接從交易 ChannnelHeader 中提取的,所以在所以背書節點處看到的值都相同 GetTxTimestamp() (*timestamp.Timestamp, error) // 返回交易的 binding 信息 // 交易的 binding 信息是將交提案的 nonse、Creator、epoch 等信息組合起來哈希得到數字摘要 GetBinding() ([]byte, error) // 返回該 stub 的 SignedProposal 結構,包括了跟交易提案相關的所有數據 GetSignedProposal() (*pb.SignedProposal, error) // 返回該交易提交者的身份信息(用戶證書) // 從 SignedProposal 中的 SignatureHeader.Creator 提取 GetCreator() ([]byte, error) // 返回交易中帶有的一些臨時信息 // 從 ChaincodeProposalPayload.transient 提取,可以存放與應用相關的保密信息,該信息不會被寫入到賬本 GetTransient() (map[string][]byte, error)
對PrivateData操作的API
// 根據指定的 key,從指定的私有數據集中查詢對應的私有數據 GetPrivateData(collection, key string) ([]byte, error) // 將指定的 key 與 value 保存到私有數據集中 PutPrivateData(collection string, key string, value []byte) error // 根據指定的 key 從私有數據集中刪除相應的數據 DelPrivateData(collection, key string) error // 根據指定的開始與結束 key 查詢范圍(不包含結束key)內的私有數據 GetPrivateDataByRange(collection, startKey, endKey string) (StateQueryIteratorInterface, error) // 根據給定的部分組合鍵的集合,查詢給定的私有狀態 GetPrivateDataByPartialCompositeKey(collection, objectType string, keys []string) (StateQueryIteratorInterface, error) // 根據指定的查詢字符串執行富查詢 (只支持支持富查詢的 CouchDB) GetPrivateDataQueryResult(collection, query string) (StateQueryIteratorInterface, error)
智能合約單元測試
(參考https://www.cnblogs.com/skzxc/p/12150476.html)
單元測試 (UT) 可以提高調試的效率和我們代碼的質量。fabric中提供了一個MockStub類用於單元測試。
單元測試不需要啟動任何網絡節點,通過我們的測試文件就可以在本地對鏈碼中的接口進行調用測試。
其原理就是在MockStub類中維護一個 map[string][]byte 來模擬 key-val 的狀態數據庫,鏈碼調用的PutState() 和 GetState() 其實是作用於內存中的map。
MockStub主要提供兩個函數來模擬背書節點對鏈碼的調用:MockInit()和MockInvoke(),分別調用Init和Invoke接口。接收的參數均為類型為string的uuid(隨便設置即可),以及一個二維byte數組(用於測試的提供參數)。
單元測試要求:
1.需要導入testing包;
2.單元測試文件以_test.go結尾;
3.測試用例的函數必須以Test開頭,可選的后綴名必須以大寫字母開頭!
單元測試的實例
測試examlpe_cc.go智能合約,其測試用例如下:
package main import ( "fmt" "github.com/hyperledger/fabric/core/chaincode/shim" "testing" ) func TestFunc(t *testing.T) { cc := new(SimpleChaincode) // 創建Chaincode對象 stub := shim.NewMockStub("sacc", cc) // 創建MockStub對象 // 調用Init接口,將a的值設為90 stub.MockInit("1", [][]byte{[]byte("init"), []byte("a"),[]byte("100"),[]byte("b"),[]byte("200")}) // 調用query接口查詢a的值 res := stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("a")}) fmt.Println("The value of a is ", string(res.Payload)) // 調用query接口查詢a的值 res = stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("b")}) fmt.Println("The value of b is ", string(res.Payload)) // 轉賬 res = stub.MockInvoke("1", [][]byte{[]byte("invoke"),[]byte("a"),[]byte("b"),[]byte("50")}) // 再次查詢a的值 res = stub.MockInvoke("1", [][]byte{[]byte("query"), []byte("a")}) fmt.Println("The new value of a is ", string(res.Payload)) }
測試結果如下:
ex02 Init [a 100 b 200] Aval = 100, Bval = 200 ex02 Invoke Query Response:{"Name":"a","Amount":"100"} The value of a is 100 ex02 Invoke Query Response:{"Name":"b","Amount":"200"} The value of b is 200 ex02 Invoke Aval = 50, Bval = 250 ex02 Invoke Query Response:{"Name":"a","Amount":"50"} The new value of a is 50 PASS
ok go-demo 0.314s