翻译官方文档https://hyperledger-fabric.readthedocs.io/en/latest/chaincode4ade.html
什么是链码
链码是程序,用Go,Node.js,Java其中一种语言编程。链码运行在Peer的独立进程中,负责初始化账本,管理账本状态。
链码通常用来处理网络成员同意的逻辑事务,所以它也被称为“智能合约”。可以调用链码更新或者查询交易。如果有合适的权限,两码可以调用另一个链码,无论是否在一个channel中,获取账本状态。注意如果被调用的链码和链码处于不同的channel中,只有读权限。也就是说被调用链码只有读功能,不参与后续事务的验证和检查。
接下来的部分,我们会从应用开发者的角度来探索链码。我们以一个例子来介绍链码API中的方法。如果你是部署链码的运维人员,可以查看https://hyperledger-fabric.readthedocs.io/en/latest/deploy_chaincode.html和https://hyperledger-fabric.readthedocs.io/en/latest/chaincode_lifecycle.html
本文档介绍的是链码的基础API。你也可以使用高级API——Fabric Contract API,相关文档https://hyperledger-fabric.readthedocs.io/en/latest/developapps/smartcontract.html
链码API
每个链码程序必须实现链码接口,用来响应接收的事务处理,不同语言的接口文档在下方
Go https://godoc.org/github.com/hyperledger/fabric-chaincode-go/shim#Chaincode
Node.js https://hyperledger.github.io/fabric-chaincode-node/master/api/fabric-shim.ChaincodeInterface.html
每种语言都会用Invoke方法来提交事务。这个方法可以让你读写账本数据。
链码中也需要定义一个Init函数,实现初始化。这个函数是链码需要的,并不需要app调用。你可以使用lifecycle进程来设定在调用链码之前是否需要Init函数。这方面资料查看https://hyperledger-fabric.readthedocs.io/en/latest/chaincode_lifecycle.html#step-three-approve-a-chaincode-definition-for-your-organization
另一个链码API是ChaincodeStubInterface
Go https://godoc.org/github.com/hyperledger/fabric-chaincode-go/shim#ChaincodeStubInterface
Node.js https://hyperledger.github.io/fabric-chaincode-node/master/api/fabric-shim.ChaincodeStub.html
用来访问和修改账本,且在链码之间调用。
本文采用Go语言链码,来演示一个简单的资产管理的例子。
资产链码
本例是在账本中创建一个简单的资产管理(key-value形式)
选择一个目录
如果你没有使用过Go编程,首选需要安装Go并配置环境。我们假定你选用的版本支持这些模块。
首先创建一个存放链码的目录
为了简单起见,我们执行下面的命令
mkdir sacc && cd sacc
现在来创建一个模块和脚本
go mod init sacc
touch sacc.go
初始整理
首先,我们做一些整理。和所有链码一样,需要实现Init和Invoke函数。我们先引入依赖包。我们需要引入shim和peer。然后再定义一个结构体来接收链码的shim函数。
package main
import (
"fmt"
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-protos-go/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 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")
}
}
我们已经验证了参数,接下来将数据保存到账本中。key-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的参数会成为链码中函数的名字。这里我们只定义两个内部的函数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数据
)
// 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))
}
实现链码内的函数
如前文所述,我们需要定义两个被链码调用的函数。注意我们之前提到的,管理账本装态需要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
}
完整代码
最后需要增加main函数,用来调用 shim.Start函数。完整代码展示
package main
import (
"fmt"
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-protos-go/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)
}
}
链码访问控制
链码可以通过调用GetCreator()函数来获取证书做访问控制。另外Go shim提供了扩展API,这些API通过提取提交者的证书中的客户端标识,来做访问控制。可以是客户端自身身份,组织身份,或者是客户端属性。
例如标识资产的键值对中包含客户端的身份(如JSON属性中指定资产拥有者),并且只有这个客户端才能升级键值对的数据。客户身份标识的拓展API可以用来在链码中提取提交者的信息和做访问控制。
详情https://github.com/hyperledger/fabric-chaincode-go/blob/master/pkg/cid/README.md
管理依赖包
链码需要的GO包不是标准库,需要将依赖包安装至链码包中。如果将链码构建为一个module,最简单的方式是在打包之前将包安装至vendor中
go mod tidy
go mod vendor
这会将依赖包放入到本地的vendor目录中.
如果依赖包已经被放入vendor中,peer chaincode package和peer chaincode install操作会将关联的依赖包都放入链码中