链码开发者

什么是链码?

Chaincode是一个程序,用`Go <https://golang.org>`_, node.js, or `Java <https://java.com/en/>`_编写,实现指定的接口。链码运行在安全的Docker容器中,该容器与背书节点进程隔离。链码通过应用提交的交易初始化和管理账本状态。

链码通常处理网络成员同意的业务逻辑,因此它类似于“智能合约”。在一个提议交易中,可以调用链码来更新或查询账本。给予适当的权限,一个链码可以调用另一个链码(在相同通道中或在不同通道中)来访问其状态。注意,如果被调用的链码位于与调用链码不同的通道上,则只允许读查询。也就是说,在不同通道上被调用的链码只是一个“查询”,它不参与后续提交阶段的状态验证检查。

在下面的部分中,我们将通过应用开发者的视角来研究链码。我们将展示一个简单的链码示例应用程序,并介绍Chaincode Shim API中每个方法的用途。

链码API

每个链码程序都必须实现``Chaincode`` 接口,该接口的方法是在收到交易的响应中被调用的。你可以在下面找到适用于不同语言的Chaincode Shim API的参考文档:

在每种语言中,客户端调用``Invoke`` 方法来提交交易提议。此方法允许您使用链码读取和写入通道账本上的数据。

您还需要包含一个 Init 方法,它将作为链码的初始化函数。当链码启动或升级时,将调用此方法来初始化链码。默认情况下,这个函数永远不会执行。但是,您可以使用链码定义来请求执行 Init 函数。如果请求执行 Init ,Fabric将确保在调用任何其他函数之前调用 Init ,并且只调用一次。该选项为您提供了额外的控制,用户可以对其中的链码进行初始化,并能够将初始数据添加到账本。如果您正在使用节点的CLI来批准链码定义,请使用``–init-required`` 标志来请求执行``Init`` 函数。然后使用 peer chaincode invoke`命令调用``Init` 函数,并传入 --isInit 标志。如果您正在为Node.js使用Fabric SDK,请访问 `如何安装和启动您的链码<https://fabric-sdk-node.github.io/master/tutorial-chaincode-lifecycle.html>`__。更多信息,请看 Chaincode for Operators

在链码“shim”API中的另一个接口是“ChaincodeStubInterface”:

这些用于存取和修改账本,并在链码之间进行调用。

在使用Go链码的本教程中,我们将通过实现一个管理简单“资产”的简单链码应用程序来演示这些API的使用。

简单资产链码

我们的应用程序是一个基本的链码示例,用于在账本上创建资产(键值对)。

为代码选择一个位置

如果您没有用过Go编程,您可能希望确保已经安装了 Go Programming Language 并正确配置了系统。

现在,您需要为链码应用程序创建一个目录,作为``$GOPATH/src/``的子目录。

为了简单起见,我们使用以下命令:

mkdir -p $GOPATH/src/sacc && cd $GOPATH/src/sacc

现在,让我们创建源文件,我们将用下列代码填充:

touch sacc.go

辅助工作

首先,让我们从一些辅助工作开始。与每个链码一样,它实现了 Chaincode interface 特别是 InitInvoke 函数。因此,让我们添加Go import语句,用于链码的必要依赖项。我们将导入链码shim包和`peer protobuf package <https://godoc.org/github.com/hyperledger/fabric/protos/peer>`_。接下来,让我们添加一个结构 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 {
}

初始化链码

下面,我们将实现``Init`` 函数。

// Init is called during chaincode instantiation to initialize any data.
func (t *SimpleAsset) Init(stub shim.ChaincodeStubInterface) peer.Response {

}

注解

注意,链码升级也调用这个函数。在编写现有链码升级的链码时,请确保适当地修改``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 <https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim#ChaincodeStub.PutState>`_ 。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``中提取参数。函数的参数将是要调用的链码应用的函数名称。在我们的例子中,应用只有两个函数:set``和``get,它们允许设置资产的值或检索资产的当前状态。我们首先调用`ChaincodeStubInterface.GetFunctionAndParameters <https://godoc.org/github.com/hyperledger/fabric/core/chaincode/shim#ChaincodeStub.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))
}

实现链码应用程序

如前所述,我们的链码应用程序实现了两个可以通过``Invoke`` ‘函数调用的函数。现在我们来实现这些函数。请注意,如前所述,要访问账本的状态,我们将利用链码shim API的 ChaincodeStubInterface.PutStateChaincodeStubInterface.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 <https://godoc.org/github.com/hyperledger/fabric/core/chaincode/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)
    }
}

构建链码

现在让我们编译你的链码

go get -u github.com/hyperledger/fabric/core/chaincode/shim
go build

假设没有错误,现在我们可以继续下一步,测试您的链码。

用开发模式测试

通常,链码由节点启动和维护。但是在“开发模式”中,链码是由用户构建和启动的。这种模式在链码开发阶段对于快速代码/构建/运行/调试循环非常有用。

我们利用预生成的排序器和通道构件组成的示例开发网络来启动“开发模式”。因此,用户可以立即跳到编译链码和驱动调用的过程中。

安装超级账本Fabric示例

如果您还没有这样做,请 Install Samples, Binaries and Docker Images

导航到“fabric-samples”克隆体的“chaincode-docker-devmode”目录:

cd chaincode-docker-devmode

现在打开三个终端并导航到每个终端的“chaincode-docker-devmode”目录。

终端1 - 启动网络

docker-compose -f docker-compose-simple.yaml up

上面的代码使用“SingleSampleMSPSolo”排序器配置文件启动网络,并在“dev模式”中启动节点。它还启动了另外两个容器,一个用于链码环境,另一个用于CLI与链码交互。创建和加入通道的命令嵌入到CLI容器中,因此我们可以立即跳转到链码调用。

终端2 - 构建和启动链码

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 命令完成的。

终端3 - 使用链码

即使您处于 --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

最后,查询 a。我们应该看到’``20``的值。

peer chaincode query -n mycc -c '{"Args":["query","a"]}' -C myc

测试新链码

默认情况下,我们只挂载`sacc``。然而,您可以通过将不同的链码添加到 chaincode 子目录并重新启动您的网络,从而轻松地测试不同的链码。此时,它们将可在您的``chaincode``容器中被访问。

链码访问控制

链码可以通过调用GetCreator()函数来利用客户端(提交者)证书进行访问控制决策。此外,Go shim还提供了扩展API,从提交者的证书中提取客户端身份,这些API可用于访问控制决策,无论是基于客户端身份本身、org身份,还是基于客户端身份属性。

例如,表示为键/值的资产可能包含作为值的一部分的客户端身份(例如表示该资产所有者的JSON属性),并且只有这个客户端可能被授权在将来更新键/值。可以在链码中使用客户端身份库扩展API来检索提交者信息,从而做出这样的访问控制决策。

有关详细信息,请参见`客户端身份(CID)库文档 <https://github.com/hyperledger/fabric/blob/master/core/chaincode/shim/ext/cid/README.md>`_ 。

要将客户端身份shim扩展作为依赖项添加到链码中,请参见 管理用Go编写的链码的外部依赖关系

链码加密

链码在某些场景中,对与密钥关联的值进行整体或部分加密可能很有用。例如,如果一个人的社会安全号码或地址被写入了账本,那么您可能不希望这些数据以明文形式出现。链码加密是通过使用`实体扩展 <https://github.com/hyperledger/fabric/tree/master/core/chaincode/shim/ext/entities>`__ 来实现的,该扩展是一个BCCSP包装器,带有商品工厂和函数,用于执行类似加密和椭圆曲线数字签名等的加密操作。例如,要加密,链码的调用程序通过瞬态字段传入一个加密密钥。然后,可以将相同的密钥用于后续查询操作,从而对加密的状态值进行正确的解密。

有关更多信息和示例,请参见“fabric/examples”目录中的`Encc例子 <https://github.com/hyperledger/fabric/tree/master/examples/chaincode/go/enccc_example>`__。要特别注意 utils.go 辅助程序。这个实用程序加载链码shim API和实体扩展,并构建一个新的函数类(例如``encryptAndPutState`` & getStateAndDecrypt)来示范加密链码。因此,链码现在可以将基本shim API 的``Get`` /Put``与``Encrypt /``Decrypt``的附加功能结合起来。

要将加密实体扩展作为依赖项添加到链码中,请参见 管理用Go编写的链码的外部依赖关系.

管理用Go编写的链码的外部依赖关系

如果您的链码需要Go标准库没有提供的包,那么您需要在链码中包含这些包。将shim和任何扩展库作为依赖项添加到链码中也是一个很好的实践。

有`许多工具 <https://github.com/golang/go/wiki/PackageManagementTools>`__ 可用于管理(或“处理”)这些依赖项。下面演示如何使用``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目录。

一旦依赖项在chaincode目录中被处理,peer chaincode package``和``peer chaincode install 操作将把与依赖项相关的代码包含到链码包中。