上篇文章中我們介紹了鏈碼的概念,本文中我們將介紹Fabric下鏈碼的編寫和測試。我們會通過一個簡單例子的方式來闡述鏈碼API的使用。
鏈碼API
每一個鏈碼程序都必須實現一個接口Chaincode Interface, 這些方法用來響應接受到的交易。特別的,當鏈碼接收到``Instantiate``和``upgrade``類型的交易時會調用``Init``方法,執行一些需要的初始化,包括應用狀態的初始化。當鏈碼接收到``Invoke``類型的交易時候會調用``Invoke``方法來處理交易提議。
鏈碼中調用的其他接口“shim” APIs,用來訪問和修改賬本,以及調用其他鏈碼操作。
在本文中,我們通過一個簡單的資產管理的鏈碼應用來展示這些APIs的使用。
簡單資產鏈碼
我們的應用是一個簡單的鏈碼,用來在賬本上創建資產(key-value健值對)。
選擇代碼目錄位置
如果沒有使用Go做過開發,應該首先確定系統中已經安裝和配置了Golang。然后為鏈碼應用程序創建一個目錄,我們使用如下的命令進行創建:
mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc
現在,我們創建源文件
touch sacc.go
編寫鏈碼
現在我們來編寫一個具體的鏈碼。每個鏈碼都實現了``Chaincode Interface``,主要是``Init``和``Invoke``函數。因此在編寫程序時,我們首先要導入shim接口,以及其他一些包,,比如:``peer protobuf``包。然后,我們添加一個struct ``SimpleAsset``作為鏈碼shim函數的接收器(這塊不懂,請補習go語言基本知識)。
代碼如下:
.. code:: go package main import ( "fmt" "github.com/hyperledger/fabric/core/chaincode/shim" "github.com/hyperledger/fabric/protos/peer" ) // SimpleAsset implements a simple chaincode to manage an asset type SimpleAsset struct { }
初始化鏈碼
接下來,我們要實現``Init``函數。
.. code:: go //Init在鏈碼初始化的時候調用,用來初始化一些數據 func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { }
注意:鏈碼升級也會調用這個函數。當進行鏈碼升級的時候,確保新的鏈碼對``Init``進行了合適的修改。特別的,如果沒有遷移或者升級不需要進行一些初始化,那么可以提供一個空的``Init``。
然后,我們通過`ChaincodeStubInterface.GetStringArgs`來獲取調用``Init``的參數列表。在我們的例子中,我們期望得到一個健值對。
代碼如下:
.. code:: go // Init is called during chaincode instantiation to initialize any // data. Note that chaincode upgrade also calls this function to reset // or to migrate data, so be careful to avoid a scenario where you // inadvertently clobber your ledger's data! func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { // 從交易提議中獲取參數列表 args := stub.GetStringArgs() if len(args) != 2 { return shim.Error("Incorrect arguments. Expecting a key and a value") } }
在代碼中我們簡單檢查了參數數量,然后我們將初始的狀態存入賬本中。我們調用``ChaincodeStubInterface.PutState``並傳入健值參數。假設程序執行正常,最后我們返回一個``peer.Response``來表明初始化成功。
.. code:: go // Init is called during chaincode instantiation to initialize any // data. Note that chaincode upgrade also calls this function to reset // or to migrate data, so be careful to avoid a scenario where you // inadvertently clobber your ledger's data! func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { // Get the args from the transaction proposal args := stub.GetStringArgs() if len(args) != 2 { return shim.Error("Incorrect arguments. Expecting a key and a value") } // Set up any variables or assets here by calling stub.PutState() // We store the key and the value on the ledger err := stub.PutState(args[0], []byte(args[1])) if err != nil { return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0])) } return shim.Success(nil) }
調用鏈碼
首先我們添加``Invoke``函數
.. code:: go // Invoke is called per transaction on the chaincode. Each transaction is // either a 'get' or a 'set' on the asset created by Init function. The 'set' // method may create a new asset by specifying a new key-value pair. func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response { }
像``Init``一樣,我們通過``ChaincodeStubInterface``來獲取參數。``Invoke`` 的函數參數是鏈碼應用程序調用時候的名字。在我們的例子中,我們的應用程序只有兩個函數: "set"和“get”,用來設置資產和資產狀態的查詢。
1、我們首先調用“ChaincodeStubInterface.GetFunctionAndParameters"來獲取函數名字和參數。
2、驗證函數參數是否為“set”或者“get”,調用鏈碼應用相關函數,返回成功或者失敗的響應(通過“shim.Sucess"或者”shim.Error"函數)。
.. code:: go // Invoke is called per transaction on the chaincode. Each transaction is // either a 'get' or a 'set' on the asset created by Init function. The Set // method may create a new asset by specifying a new key-value pair. func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response { // Extract the function and args from the transaction proposal fn, args := stub.GetFunctionAndParameters() var result string var err error if fn == "set" { result, err = set(stub, args) } else { result, err = get(stub, args) } if err != nil { return shim.Error(err.Error()) } // Return the result as success payload return shim.Success([]byte(result)) }
實現鏈碼應用
前面提到,我們的鏈碼應用程序實現了兩個函數,可以通過”Invoke“函數來進行調用。下面是實現方法,分別調用了”ChaincodeStubInterface“的“PutState”和“GetState”接口。
.. code:: go // Set stores the asset (both key and value) on the ledger. If the key exists, // it will override the value with the new one func set(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 2 { return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value") } err := stub.PutState(args[0], []byte(args[1])) if err != nil { return "", fmt.Errorf("Failed to set asset: %s", args[0]) } return args[1], nil } // Get returns the value of the specified asset key func get(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 1 { return "", fmt.Errorf("Incorrect arguments. Expecting a key") } value, err := stub.GetState(args[0]) if err != nil { return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err) } if value == nil { return "", fmt.Errorf("Asset not found: %s", args[0]) } return string(value), nil }
整個鏈碼程序(合並)
最后,我們添加“main”函數,調用了“shim.Start"函數。
.. code:: go package main import ( "fmt" "github.com/hyperledger/fabric/core/chaincode/shim" "github.com/hyperledger/fabric/protos/peer" ) // SimpleAsset implements a simple chaincode to manage an asset type SimpleAsset struct { } // Init is called during chaincode instantiation to initialize any // data. Note that chaincode upgrade also calls this function to reset // or to migrate data. func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { // Get the args from the transaction proposal args := stub.GetStringArgs() if len(args) != 2 { return shim.Error("Incorrect arguments. Expecting a key and a value") } // Set up any variables or assets here by calling stub.PutState() // We store the key and the value on the ledger err := stub.PutState(args[0], []byte(args[1])) if err != nil { return shim.Error(fmt.Sprintf("Failed to create asset: %s", args[0])) } return shim.Success(nil) } // Invoke is called per transaction on the chaincode. Each transaction is // either a 'get' or a 'set' on the asset created by Init function. The Set // method may create a new asset by specifying a new key-value pair. func (t *SimpleAsset) Invoke(stub shim.ChaincodeStubInterface) peer.Response { // Extract the function and args from the transaction proposal fn, args := stub.GetFunctionAndParameters() var result string var err error if fn == "set" { result, err = set(stub, args) } else { // assume 'get' even if fn is nil result, err = get(stub, args) } if err != nil { return shim.Error(err.Error()) } // Return the result as success payload return shim.Success([]byte(result)) } // Set stores the asset (both key and value) on the ledger. If the key exists, // it will override the value with the new one func set(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 2 { return "", fmt.Errorf("Incorrect arguments. Expecting a key and a value") } err := stub.PutState(args[0], []byte(args[1])) if err != nil { return "", fmt.Errorf("Failed to set asset: %s", args[0]) } return args[1], nil } // Get returns the value of the specified asset key func get(stub shim.ChaincodeStubInterface, args []string) (string, error) { if len(args) != 1 { return "", fmt.Errorf("Incorrect arguments. Expecting a key") } value, err := stub.GetState(args[0]) if err != nil { return "", fmt.Errorf("Failed to get asset: %s with error: %s", args[0], err) } if value == nil { return "", fmt.Errorf("Asset not found: %s", args[0]) } return string(value), nil } // main function starts up the chaincode in the container during instantiate func main() { if err := shim.Start(new(SimpleAsset)); err != nil { fmt.Printf("Error starting SimpleAsset chaincode: %s", err) } }
鏈碼編譯
現在讓我們編譯剛才編寫的鏈碼。
go get -u --tags nopkcs11 github.com/hyperledger/fabric/core/chaincode/shim go build --tags nopkcs11
假設編譯過程中沒有錯誤,接下來我們對鏈碼進行測試。
使用dev模式進行測試
正常情況下,鏈碼由節點來啟動和維護。然而在“dev“模式下,鏈碼由用戶來編譯和啟動,這種模式便於快速的編碼、編譯和調試鏈碼。
我們使用預先生成的orderer和channel artifacts來啟動”dev“模式下的測試網絡。之后,用戶可以快速的進行鏈碼的編譯和調用。
安裝超級賬本Fabric-samples
如何你還未安裝過samples,首先需要安裝。
git clone https://github.com/hyperledger/fabric-samples.git
進入fabric-samples的”chaincode-docker-devmode“目錄
.. code:: bash
cd chaincode-docker-devmode
下載docker鏡像
在”dev“模式下,我們需要4個docker鏡像。如果已經clone了”fabric-samples", 執行’download-platfrom-specific-binaries', 會下載需要的鏡像。下載成功后,執行’docker images‘,會顯示已經下載好的鏡像如下:
docker images REPOSITORY TAG IMAGE ID CREATED SIZE hyperledger/fabric-tools latest e09f38f8928d 4 hours ago 1.32 GB hyperledger/fabric-tools x86_64-1.0.0 e09f38f8928d 4 hours ago 1.32 GB hyperledger/fabric-orderer latest 0df93ba35a25 4 hours ago 179 MB hyperledger/fabric-orderer x86_64-1.0.0 0df93ba35a25 4 hours ago 179 MB hyperledger/fabric-peer latest 533aec3f5a01 4 hours ago 182 MB hyperledger/fabric-peer x86_64-1.0.0 533aec3f5a01 4 hours ago 182 MB hyperledger/fabric-ccenv latest 4b70698a71d3 4 hours ago 1.29 GB hyperledger/fabric-ccenv x86_64-1.0.0 4b70698a71d3 4 hours ago 1.29 GB
現在打開3個終端,切換到`chaincode-docker-devmode`。
終端1 - 啟動網絡
docker-compose -f docker-compose-simple.yaml up
執行上面命令后會啟動fabric網路,包括一個`SingleSampleMSPSolo`配置的orderer和一個`dev`模式的peer。同時還會啟動另外兩個容器 — 一個是鏈碼環境容器,另一個CLI用來跟鏈碼進行交互,同時內嵌了創建和加入通道的命令,因此我們可以立刻進行合約的調用操作。
終端2 — 編譯&啟動鏈碼
docker exec -it chaincode bash 執行后進入到鏈碼容器中, root@d2629980e76b:/opt/gopath/src/chaincode#
現在,編譯你的鏈碼: cd sacc go build 然后運行鏈碼: CORE_PEER_ADDRESS=peer:7051 CORE_CHAINCODE_ID_NAME=mycc:0 ./sacc
現在鏈碼在peer節點上啟動,鏈碼日志表明鏈碼已經成功注冊到peer上。注意,在這個階段鏈碼還沒有與通道進行關聯,由接下來的`instantiate`來完成。
終端3 — 使用鏈碼
盡管你使用的是`--peer-chaincodedev`模式,你仍然需要安裝鏈碼以便於生命周期系統鏈碼能夠通過正常的檢查,以后該模式下可能會移除這個檢查。
我們使用CLI容器來調用鏈碼。
docker exec -it cli bash
peer chaincode install -p chaincodedev/chaincode/sacc -n mycc -v 0
peer chaincode instantiate -n mycc -v 0 -c '{"Args":["a","10"]}' -C myc
現在調用`invoke`來將`a`的值改為`20`。
peer chaincode invoke -n mycc -c '{"Args":["set", "a", "20"]}' -C myc
最后,我們查詢`a`。 我們會看到a的值已經改為`20`
peer chaincode query -n mycc -c '{"Args":["query","a"]}' -C myc
測試新的鏈碼
我們默認僅加載了`sacc`。 然而,我們可以很容的測試其他的鏈碼,通過將鏈碼添加到`chaincode`子目錄下並重新啟動你的網絡。此時在鏈碼容器中就可以使用這些新添加的鏈碼。
