教程 - Chaincode for Developers
1. 什么是链码
链码 (chaincode) 是用 Go,node.js 或 Java 编写的程序,可实现规定的接口。 链码在与背书对端节点进程隔离的安全 Docker 容器中运行。 链码通过应用程序提交的交易初始化和管理帐本状态。
链码通常处理网络成员同意的业务逻辑,因此它类似于智能合约。可以调用链码来更新或查询提案交易中的帐本。在获得适当许可的情况下,一个链码可以在同一通道或不同通道中调用另一个链码以访问其状态。请注意,如果被调用链码与调用链码位于不同的通道,则仅允许读取查询。也就是说,在另一个通道上的被调用链码仅仅是一个查询,它不参与后续提交阶段的状态验证检查。
在以下各节中,我们将通过应用程序开发人员的眼光探索链码。我们将展示一个简单的链码示例应用程序,并逐步介绍 Chaincode Shim API 中每种方法的用途。
2. 链码 API
每个链码程序必须实现 Chaincode
接口:
响应接收到的交易而调用其方法。特别是当链码接收到实例化或升级交易时,将调用 Init
方法,以便链码可以执行任何必要的初始化,包括应用程序状态的初始化。响应于接收到用于处理交易提案的调用交易而调用 Invoke
方法。
Chaincode shim API 中的另一个接口是 ChaincodeStubInterface
:
用于访问和修改帐本,以及在链码之间进行调用。
在使用 Go 链码的本教程中,我们将通过实现一个管理简单资产的简单链码应用程序来演示这些 API 的用法。
3. 简单资产链码
我们的应用程序是一个基本示例链码,用于在账本上创建资产 (键值对)。
3.1 选择链码位置
如果你尚未使用 Go 进行编程,则可能需要确保已安装 Go 编程语言 并正确配置了系统。
现在,你将要为链码应用程序创建一个目录,作为 $GOPATH/src/
的子目录。
为简单起见,我们使用以下命令:
$ mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc
现在,让我们创建将用代码填充的源文件:
touch sacc.go
3.2 Housekeeping
首先,让我们开始做一些整理工作。与每个链码一样,它实现了 Chaincode interface,尤其是 Init
和 Invoke
函数。因此,让我们添加 Go 导入语句以获取链码所需的依赖关系。我们将导入 chaincode shim package 和 peer protobuf package。接下来,让我们添加一个结构 SimpleAsset
作为 Chaincode 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 {
}
3.3 初始化链码
接下来,我们将实现 Init
函数。
// 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)
}
3.4 调用链码
首先,让我们添加 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
函数的参数将是要调用的链码应用程序函数的名称。在我们的例子中,我们的应用程序将仅具有两个功能: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))
}
3.5 实现链码应用程序
如前所述,我们的链码应用程序实现了两个可以通过 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
}
3.6 全部整合起来
最后,我们需要添加 main 函数,该函数将调用 shim.Start 函数。这是整个链码程序的来源。
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)
}
}
3.7 构建链码
现在,我们来编译你的链码。
go get -u github.com/hyperledger/fabric/core/chaincode/shim
go build
假设没有错误,现在我们可以继续下一步,测试你的链码。
3.8 使用开发模式测试
通常,链码由对端节点启动和维护。但是,在开发模式下,链码由用户构建和启动。在链代码开发阶段,此模式对于快速执行代码/构建/运行/调试周期周转非常有用。
我们通过利用示例开发网络的预生成交易排序程序和通道组件来启动开发模式。这样,用户可以立即进入编译链码和驱动调用的过程。
4. 安装 Hyperledger Fabric Samples
如果尚未安装,请参考 Install Samples, Binaries and Docker Images。
导航到 fabric-samples
克隆的 chaincode-docker-devmode
目录:
$ cd chaincode-docker-devmode
现在打开三个终端,并在每个终端中导航到 chaincode-docker-devmode
目录。
5. 终端 1 - 启动网络
$ sudo docker-compose -f docker-compose-simple.yaml up
上面的代码使用 SingleSampleMSPSolo 交易排序器配置文件启动网络,并以开发模式启动对端节点。它还启动了另外两个容器 - 一个用于链码环境的容器和一个与链码进行交互的 CLI。用于创建和加入通道的命令嵌入在 CLI 容器中,因此我们可以立即跳转到链码调用。
6. 终端 2 - 构建 & 启动链码
$ sudo docker exec -it chaincode bash
你应该看到以下内容:
root@d2629980e76b:/opt/gopath/src/chaincode#
现在,编译你的链码:
# cd sacc
# go build
现在运行链码:
# CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=mycc:0 ./sacc
链码以对端节点和链码日志开始,指示向对端节点成功注册。请注意,在此阶段,链码未与任何通道关联。这是在随后的步骤中使用 instantiate
命令完成的。
7. 终端 3 - 使用链码
即使你处于 --peer-chaincodedev
模式,你仍然必须安装链码,以便生命周期系统链码可以正常进行检查。将来在 --peer-chaincodedev
模式下,可以删除此要求。
我们将利用 CLI 容器来驱动这些调用。
$ sudo 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
最后,查询 a。我们应该看到 a 的值为 20。
# peer chaincode query -n mycc -c '{"Args":["query","a"]}' -C myc
8. 测试新链码
默认情况下,我们仅安装 sacc。但是,你可以通过将不同的链码添加到 chaincode
子目录并重新启动网络来轻松测试它们。此时,你可以在你的 chaincode
容器中访问它们。
9. 链码访问控制
链码可以通过调用 GetCreator() 函数来利用客户端 (提交者) 证书进行访问控制决策。此外,Go shim 提供了扩展 API,这些 API 从提交者的证书中提取客户端身份,可用于访问控制决策,无论是基于客户端身份本身,组织身份还是基于客户端身份属性。
例如,表示为键/值的资产可以将客户端的身份包括为值的一部分 (例如,作为表示该资产所有者的 JSON 属性),并且只有该客户端可以被授权对键/值进行更新在将来。客户端身份库扩展 API 可以在链码中使用,以检索此提交者信息以做出此类访问控制决策。
有关更多详细信息,请参见 客户端身份 (Client Identity, CID) 库文档。
要将客户端身份 shim 扩展名作为依赖项添加到你的链码中,请参阅 管理用 Go 编写的链码的外部依赖项。
10. 链码加密
在某些情况下,对整个或部分加密与密钥关联的值进行加密可能很有用。例如,如果某人的社会安全号码或地址被写入帐本,则你可能不希望此数据以纯文本形式显示。链码加密是通过利用 实体扩展 来实现的,该扩展是带有商品工厂和功能的 BCCSP 包装器,用于执行加密操作,例如加密和椭圆曲线数字签名。例如,要进行加密,链码的调用者会通过瞬态字段传入加密密钥。然后,可以将相同的密钥用于后续的查询操作,以允许对加密的状态值进行适当的解密。
有关更多信息和示例,请参阅 fabric/examples
目录中的 Encc 示例。请特别注意 utils.go 帮助程序。该实用程序将加载链码填充程序 API 和实体扩展,并构建示例加密链码随后利用的新功能类 (例如 cryptoAndPutState 和 getStateAndDecrypt)。因此,链码现在可以将 Get 和 Put 的基本填充 API 与 Encrypt 和 Decrypt 的附加功能结合在一起。
要将加密实体扩展作为依赖项添加到你的链码中,请参阅 管理用 Go 编写的链码的外部依赖项。
11. 管理用 Go 编写的链码的外部依赖关系
如果你的链码需要 Go 标准库未提供的软件包,则需要在链码中包含这些软件包。将填充程序和任何扩展库作为依赖项添加到你的链码中也是一种好习惯。
有 许多工具 可用于管理 (或供应) 这些依赖项。以下演示了如何使用 govendor:
govendor init
govendor add +external // Add all external package, or
govendor add github.com/external/pkg // Add specific external package
这会将外部依赖项导入本地 vendor
目录。如果要供应 Fabric shim 或 shim 扩展,请在执行 govendor 命令之前将 Fabric 存储库克隆到 $GOPATH/src/github.com/hyperledger
目录中。
一旦将依赖项供应到你的链码目录中,peer chaincode package
和 peer chaincode install
操作便会将与该依赖项关联的代码包含在链码程序包中。
Reference
- Docs » Tutorials » Chaincode for Developers, https://hyperledger-fabric.readthedocs.io/en/release-1.4/chaincode4ade.html
- https://golang.org/
- https://nodejs.org/en/
- https://java.com/en/
- https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim#Chaincode
- https://fabric-shim.github.io/release-1.4/fabric-shim.ChaincodeInterface.html?redirect=true
- https://hyperledger.github.io/fabric-chaincode-java/release-1.4/api/org/hyperledger/fabric/shim/Chaincode.html
- https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim#ChaincodeStubInterface
- https://fabric-shim.github.io/release-1.4/fabric-shim.ChaincodeStub.html?redirect=true
- https://hyperledger.github.io/fabric-chaincode-java/release-1.4/api/org/hyperledger/fabric/shim/ChaincodeStub.html
- Docs » Getting Started » Prerequisites, https://hyperledger-fabric.readthedocs.io/en/release-1.4/prereqs.html#golang
- Docs » Getting Started » Install Samples, Binaries and Docker Images, https://hyperledger-fabric.readthedocs.io/en/release-1.4/install.html
- https://github.com/hyperledger/fabric/tree/master/examples/chaincode/go/enccc_example
- https://github.com/golang/go/wiki/PackageManagementTools
项目源代码
项目源代码会逐步上传到 Github,地址为 https://github.com/windstamp。
Contributor
- Windstamp, https://github.com/windstamp
网友评论