链码(ChainCode)是一个用GO语言编写的实现了规定接口的程序。事实上,它也支持其他语言,比如JAVA。链码(ChainCode)运行在一个安全的Docker容器中,该容器与其他的程序相隔离。链码(ChainCode)通过应用程序提交的事务来初始化和管理账本状态(the ledger state).
链码(chainCode)通常处理网络成员同意的业务逻辑,所以它类似于“智能合约”(smart contract)。由链码(ChainCode)创建的分类状态仅限于该链码,不能由其他链码(ChainCode)直接访问。考虑到适当的许可,一个链码(ChainCode)可以调用另一个链码(ChainCode)来访问其在相同网络中的状态。
在下面的章节中,我们将以程序开发人员的眼光来探索链码(ChainCode)。我们将简单的介绍一个链码(ChainCode)的应用程序示例,并介绍链码(ChainCode)Shim API的每种方法的用途。
每一个链码(ChainCode)程序都必须实现链码(ChainCode)接口,其方法被调用以响应接收到的事务。当链码(ChainCode)接收到一个instantiate或者upgrade事务时执行Init方法进行必要的初始化,包括初始化应用程序的状态。调用Invoke方法来处理处理应用程序提交的事务。
链码(ChainCode)的shim API中的另一个接口是ChaincodeStubInterface, 用于访问和修改分类账本和进行链码之间的调用。
在本教程中,我们将通过实现一个简单的“资产”管理的链码来演示如何使用这些API。
我们的程序是一个简单的在账本中创建资产(键值对)。
如果你还未使用过Go语言进行编码,你也许需要去确定你是否安装了Go编程语言并且你的系统做过正确的配置。
现在你需要在你的$GOPATH/src/下创建一个子目录去存储你的链码程序。
为了是事情简单,我们提供了一句简单的指令:
mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc
我们创建一个源文件:
touch sacc.go
首先让我们准备编码。像每一个链码一样,它实现了链码的Init和Invoke函数。所以让我们使用go import语句导入链码的必要依赖库,我们将导入链码的shim 包和peer protobuf 包。接下来让我们添加一个结构 SimpleAsset作为链码 shim 函数的接受器。
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函数。
// Init is called during chaincode instantiation to initialize any data. func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { }
请注意! 链码升级时也会调用Init函数,在编写将升级现有链码的链码时,请适当的修改Init函数。特别是如果没有迁移或者没有任何内容作为升级的一部分进行初始化,请提供一个空的Init方法。
接下来我们将使用ChaincodeStubInterface.GetStringArgs函数检索Init调用的参数并检查其有效性,我们的例子中应当接受到的是一个键值对。
// 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") } }
下一步我们确定调用合法,我们将存储初始化状态到账本中。要做到这一点我们将调用ChaincodeStubInterface.PutState 并将键和值作为参数传入。假设一切顺利,返回一个初始化成功的peer.Response对象。
// 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函数的签名。
// 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函数的参数将会是要调用的chaincode应用程序的函数的名称。在我们的例子中,我们的应用程序只有两个参数: set和get, 它们允许设置资产的值或检索当前状态。我们首先调用ChaincodeStubInterface.GetFunctionAndParameters来提取链码应用函数的函数名称和参数。
// 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() }
接下来, 我们将验证函数名称为set或get,并调用这些chaincode的应用函数。通过shim返回适当的响应, shim.Success或shim.Error函数将响应序列化到gRPC protobuf消息。
// 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函数调用的函数。现在我们实现这些功能。请注意,正如我们上面提到的,为了访问账本的状态,我们将利用链码的shim API的ChaincodeStubInterface.PutState和ChaincodeStubInterface.GetState函数。
// 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 }
最后,我们需要添加主函数,它将调用shim.Start函数。这是整个Chaincode程序的完整代码。
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
如果没有错误,我们就可以测试链码。
通常,链码由peer启动和维护。 然而,在“开发模式”下, 链码由用户构建和启动。 开发模式在链码的代码编写、构建、运行、调试等开发阶段是非常好用的。
我们通过预先定制好的环境开运行开发者模式。因此用户可以立即编写并编译链码和发起呼叫。
如果你还没有安装Hyperledger Fabric,请阅读上一章,并完成安装。
进入到fabric-samples下的chaincode-docker-devmode目录下。
cd chaincode-docker-devmode
我们需要四个docker镜像才能运行docker compose脚本。如果你安装了fabric-samples仓库克隆并且按照说明下载了特定的二进制文件,那么你应该已经安装了比较的docker镜像了。
注意:
如果你是手动拉取的镜像,你应该将其标记为latest。
使用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
注意
如果你通过 download-platform-specific-binaries 下载的镜像, 应该可以看到其他的镜像,但是使用开发者模式的时候,我们只关心这四个镜像。
现在我们开启三个终端,并且导航到chaincode-docker-devmode目录下。
docker-compose -f docker-compose-simple.yaml up
以上命令使用SingleSampleMSPSole配置启动Orderer并且以开发模式启动Peer. 它还启动了两个容器,一个用于链码环境,一个用于与链码交互CLI。创建和加入peer的命令被嵌入到CLI容器中, 因此我们可以立即跳转到链码的调用。
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-chaincodedev模式,你仍然需要安装链码,以便链码的生命周期系统能通过检查。在将来,在--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
现在发出一个调用将“a”的值更改为“20”。
peer chaincode invoke -n mycc -c '{"Args":["set", "a", "20"]}' -C myc最后,查询一个。 我们应该看到20的值。
peer chaincode query -n mycc -c '{"Args":["query","a"]}' -C myc