这是Hyperledger Fabric V1.0 官方文档里的Chaincode for Developers章节。
第一次翻译,不妥之处还请网友指出,我们一起学习一起进步。
原文地址(http://hyperledger-fabric.readthedocs.io/en/latest/chaincode4ade.html)
Chaincode是一个用Go编写的程序,最终在其他编程语言(如Java)中实现了一个规定的界面。 链码运行在与背书节点相隔离的安全的Docker容器中。Chaincode通过由应用程序提交的交易来初始化和管理账本状态。
链码通常用来处理网络成员所同意的业务逻辑,因此可被视为“智能合约”。 由链码创建的状态仅限于该链码,不能被另一个链码直接访问。 然而,在同一个网络中,给定适当的权限,一个链码可以调用另一个链码来访问其状态。
在下面的章节中,我们将通过一个应用程序开发人员的眼睛探索chaincode。 我们将介绍一个简单的链码示例应用程序,并介绍 Shim API中的每个方法。
每个链码程序必须实现链码接口(Chaincode interface),其方法在响应接收到的交易时被调用。 特别地,当链码接收到实例化或更新交易时,调用初始化Init方法,使得链码可以执行任何必需的初始化,包括应用程序状态的初始化。 Invoke方法在响应接收到一个用来处理交易提议的invoke交易时被调用。
链码“shim”API中的另一个接口是ChaincodeStubInterface,用于访问和修改账本,并在链码之间进行调用。
在本教程中,我们将通过实现一个用来管理简单资产的链码应用程序来演示这些API的使用。
我们的应用程序是用来在账本上创建资产(键值对)的一个基本示例链码。
如果你没有go语言的环境,请先确认正确安装和配置go语言。
现在你要在$*GOPATH*/src/下面为你的链码应用程序建立一个子目录。
为了让事情简单点,我们执行下面的命令:
mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc
现在让我们创建源文件并编写代码:
touch sacc.go
首先,让我们先进行一些准备工作。与每个链码一样,链码就是实现了链码接口Chaincode interface
https://github.com/hyperledger/fabric/blob/master/core/chaincode/shim/interfaces.go#L28
特别是 init和invoke函数。
所以我们要为链码导入必须的依赖包。
我们将导入链码的shim包和peer protobuf 包。
package main
import (
"fmt"
"github.com/hyperledger/fabric/core/chaincode/shim"
"github.com/hyperledger/fabric/protos/peer"
)
接下来我们要实现一个初始化函数。
// Init is called during chaincode instantiation to initialize any data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response { }
请注意链码升级也会调用这个函数。当升级一个现有的链码时,请确保修改相应的初始化 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,并调用这些链代码应用程序函数,并通过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))
}
如上所述,我们的chaincode应用程序实现了可以通过Invoke函数调用的两个函数。 现在我们来实现这些功能。 请注意,如上所述,我们将使用chaincode 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函数的main函数。
整个链码的源码如下所示:
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 build
如果没有编译错误的话,我们可以进行下一步,测试链码。
通常链码由peer节点启动和维护。 然而,在“开发模式”中,链码由用户构建和启动。 在链码开发阶段的快速编码/构建/运行/调试期间,此模式非常有用。
我们通过利用示例开发网络中预先生成的排序和通道工件来启动“开发模式”。 因此,用户可以立即进入编译链码和驱动调用的过程。
如果你还没有示例网络,请先安装Hyperledger Fabric Samples。
安装完成后,进入fabric-samples中的chaincode-docker-devmode文件夹:
cd chaincode-docker-devmode
我们需要四个docker镜像,以便“开发模式”运行提供的docker compose 脚本。 如果您安装了fabric-samples repo克隆,并安装操作指南了下载特定平台的二进制文件,那么您应该在本地已经安装了必要的Docker映像。
如果您选择手动下载镜像,则必须把它们重新标记为latest。
执行docker images 命令可以显示您本地的Docker注册信息。 你应该看到类似于以下内容:
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-rc1-snapshot-f20846c6 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-rc1-snapshot-f20846c6 0df93ba35a25 4 hours ago 179 MB
hyperledger/fabric-peer latest 533aec3f5a01 4 hours ago 182 MB
hyperledger/fabric-peer x86_64-1.0.0-rc1-snapshot-f20846c6 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-rc1-snapshot-f20846c6 4b70698a71d3 4 hours ago 1.29 GB
如果你通过下载平台特定的二进制文件获得的镜像,则会列出的其他的镜像文件。 但是,我们只关心这四个。
现在打开3个终端,每个都进入到chaincode-docker-devmode文件夹。
docker-compose -f docker-compose-simple.yaml up
上面的命令使用 SingleSampleMSPSolo 排序节点配置文件启动网络,并以“开发模式”启动peer节点。 它还启动了两个额外的容器 - 一个用于chaincode环境,一个用于与链码交互的CLI。 由于用于创建和连接通道的命令嵌入在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节点一起启动并且在peer节点成功注册后链码日志中会有显示。。 请注意,在此阶段,链码与任何通道都不相关。 后续步骤将在实例化命令中完成。
即使您处于–peer-chaincodedev模式,您仍然必须安装链码,以便生命周期系统链码可以正常检查。 在-peer-chaincodedev模式下,这个要求将来可能会被删除。我们将利用CLI容器来驱动这些调用。
docker exec -it cli bash
cd ../
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
最后,查询 a 的值,我们将会得到20
peer chaincode query -n mycc -c '{"Args":["query","a"]}' -C myc
默认情况下,我们仅安装sacc。 但是,您可以通过将新的链码添加到chaincode子目录中并重新启动网络来轻松测试不同的链码。 此时它们将在您的chaincode容器中可以被访问。