跳转至

用 Golang 设计数据和事务模型

在 Hyperledger Fabric 中,chaincode 是一种由开发人员编写的智能合同形式。Chaincode 实现了区块链网络的利益相关者一致同意的业务逻辑。如果客户端应用程序具有正确的权限,则该功能将向客户端应用程序公开,供其调用。

Chaincode 作为独立的进程在其自己的容器中运行,与结构网络的其他组件隔离。认可对等体管理链代码和事务调用的生命周期。为响应客户调用,链码查询并更新分类帐,并生成交易建议。

在这一章中,我们将学习如何用 Go 语言开发 chaincode,并且我们将实现场景的智能契约业务逻辑。最后,我们将探索开发全功能链代码所必需的关键概念和库。

在接下来的部分中,我们将探索与概念相关的代码片段,您可以在以下地址获得链代码的完整实现:

https://github . com/hyperledger handson/trade-finance-logistics/tree/master/chain code/src/github . com/trade _ workflow _ v1

注意,这在我们在前一章创建的本地 git 克隆中也是可用的。我们有两个版本的链码,一个在trade_workflow文件夹中,另一个在trade_workflow_v1文件夹中。我们需要两个版本来演示稍后在 第九章区块链网络中的生活中的升级。在这一章中,我们使用v1版本来演示如何在 Go 中编写链码。

在本章中,我们将讨论以下主题:

  • 创建链码
  • 访问控制
  • 实现链码功能
  • 测试链码
  • 链码设计主题
  • 记录输出

开始链码开发

在我们开始编码我们的链码之前,我们需要首先启动我们的开发环境。

设置开发环境的步骤已在 第 3 章使用业务场景设置阶段中说明。但是,我们现在继续以开发模式启动结构网络。这种模式允许我们控制如何构建和运行链代码。我们将使用这个网络在开发环境中运行我们的链代码。

下面是我们如何在开发模式下启动结构网络:

$ cd $GOPATH/src/trade-finance-logistics/network
$ ./trade.sh up -d true  

如果您在网络启动时遇到任何错误,它可能是由一些剩余的 Docker 容器引起的。 您可以通过使用./trade.sh down -d true停止网络并运行以下命令来解决这个问题:./trade.sh clean -d true-dtrue 选项告诉我们的脚本对 dev 网络采取行动。

我们的开发网络现在运行在五个 Docker 容器中。该网络由单个订购者、运行在devmode中的单个对等体、chaincode 容器、CA 容器和 CLI 容器组成。CLI 容器在启动时创建一个名为tradechannel的区块链通道。我们将使用 CLI 与链码进行交互。

请随意检查日志目录中的日志消息。它列出了网络启动期间执行的组件和功能。我们将保持终端打开,因为一旦链码被安装和调用,我们将在这里收到进一步的日志消息。

编译和运行链代码

克隆的源代码已经包含了所有使用 Go vendoring 的依赖项。记住这一点,我们现在可以开始构建代码,并通过以下步骤运行链代码:

  1. 编译链码:在一个新的终端中,连接到链码容器,用下面的命令构建链码:
$ docker exec -it chaincode bash 
$ cd trade_workflow_v1 
$ go build 
  1. 使用以下命令运行 chaincode:
$ CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=tw:0 ./trade_workflow_v1  

我们现在有了一个连接到对等体的运行链代码。这里的日志消息表明 chaincode 已经启动并正在运行。您还可以检查网络终端中的日志消息,其中列出了到对等体上的链码的连接。

安装和实例化链代码

我们现在需要在初始化之前在通道上安装 chaincode,这将调用方法Init:

  1. 安装链码:在新的终端中,连接 CLI 容器,安装名为tw的链码,如下:
$ docker exec -it cli bash 
$ peer chaincode install -p chaincodedev/chaincode/trade_workflow_v1 -n tw -v 0
  1. 现在,实例化下面的chaincode:
$ peer chaincode instantiate -n tw -v 0 -c '{"Args":["init","LumberInc","LumberBank","100000","WoodenToys","ToyBank","200000","UniversalFreight","ForestryDepartment"]}' -C tradechannel 

CLI 连接的终端现在包含与链码交互的日志消息列表。chaincode终端显示来自chaincode方法调用的消息,网络终端显示来自对等体和订购者之间通信的消息。

调用链码

现在我们有了一个运行的链代码,我们可以开始调用一些函数了。我们的 chaincode 有几个创建和检索资产的方法。现在,我们将只调用其中的两个;第一个创建新的贸易协议,第二个从分类帐中检索它。为此,请完成以下步骤:

  1. 使用以下命令将一个具有唯一 ID trade-12的新贸易协议放入分类帐:
$ peer chaincode invoke -n tw -c '{"Args":["requestTrade", "trade-12", "50000", "Wood for Toys"]}' -C tradechannel
  1. 使用以下命令从分类帐中检索 ID 为trade-12的交易协议:
$ peer chaincode invoke -n tw -c '{"Args":["getTradeStatus", "trade-12"]}' -C tradechannel

我们现在在devmode有一个正在运行的网络,我们已经成功测试了我们的链码。在下一节中,我们将学习如何从头开始创建和测试链码。

开发模式 在生产环境中,链码的生命周期由对等体管理。当我们需要在一个开发环境中反复修改和测试链码时,我们可以使用devmode,它允许开发者控制链码的生命周期。另外,devmodestdoutstderr标准文件导入终端;否则,这些功能在生产环境中会被禁用。

要使用devmode,对等体必须连接到其他网络组件,就像在生产环境中一样,并以参数peer-chaincodedev=true开始。然后,链码被单独启动,并被配置为连接到对等体。在开发过程中,可以根据需要从终端重复编译、启动、调用和停止链码。

在接下来的章节中,我们将使用devmode启用的网络。

创建链码

我们现在准备开始实现我们的链码,我们将用 Go 语言编程。有几个 ide 可以为 Go 提供支持。一些比较好的 ide 包括 Atom、Visual Studio 代码等等。无论您选择什么样的环境,都将适用于我们的示例。

链码接口

每个链代码必须实现Chaincode interface,其方法被调用以响应接收到的事务提议。SHIM 包中定义的Chaincode interface如下所示:

type Chaincode interface { 
    Init(stub ChaincodeStubInterface) pb.Response 
    Invoke(stub ChaincodeStubInterface) pb.Response 
} 

如您所见,Chaincode类型定义了两个函数:InitInvoke

两个函数都有一个类型为ChaincodeStubInterface的参数stub

存根参数是我们在实现链代码功能时将使用的主要对象,因为它提供了访问和修改分类帐、获取调用参数等功能。

此外,SHIM 包提供了其他类型和功能来构建链码;可以在:https://godoc . org/github . com/hyperledger/fabric/core/chain code/shim 查看整个包。

设置链码文件

现在让我们设置chaincode文件。

我们将使用从 GitHub 克隆的文件夹结构。链码文件位于以下文件夹中:

$GOPATH/src/trade-finance-logistics/chaincode/src/github.com/trade_workflow_v1

您可以按照这些步骤检查文件夹中的代码文件,也可以创建一个新文件夹并按照描述创建代码文件。

  1. 首先,我们需要创建chaincode文件

在您喜欢的编辑器中,创建一个文件tradeWorkflow.go,并包含以下打包和导入语句:

package main

import (
    "fmt"
    "errors"
    "strconv"
    "strings"
    "encoding/json"
    "github.com/hyperledger/fabric/core/chaincode/shim"
    "github.com/hyperledger/fabric/core/chaincode/lib/cid"
    pb "github.com/hyperledger/fabric/protos/peer"
)

在前面的代码片段中,我们可以看到第 4 到 8 行导入了 Go 语言系统包,第 9 到 11 行导入了shimcidpb Fabric 包。pb包提供了对等protobuf类型的定义,cid提供了访问控制功能。在访问控制一节中,我们将进一步了解 CID。

  1. 现在我们需要定义Chaincode类型。让我们添加将实现 chaincode 函数的TradeWorkflowChaincode类型,如下面的代码片段所示:
type TradeWorkflowChaincode struct {
    testMode bool
}

记下第 2 行的testMode字段。我们将使用这个字段来规避测试期间的访问控制检查。

  1. 为了实现shim.Chaincode接口,需要TradeWorkflowChaincode。为了使TradeWorkflowChaincode成为shim包的有效Chaincode类型,必须实现接口的方法。
  2. 一旦链码被安装到区块链网络上,就调用Init方法。它仅由部署其自身链码实例的每个认可对等体执行一次。此方法可用于初始化、引导和设置链代码。下面的代码片段显示了Init方法的默认实现。注意,第 3 行中的方法在标准输出中写入一行来报告它的调用。在第 4 行,该方法返回函数shim的调用结果。参数值为nil的成功表示执行成功,结果为空,如下所示:
// TradeWorkflowChaincode implementation
func (t *TradeWorkflowChaincode) Init(stub SHIM.ChaincodeStubInterface)         pb.Response {
    fmt.Println("Initializing Trade Workflow")
    return shim.Success(nil)
}

chaincode 方法的调用必须返回一个pb.Response对象的实例。下面的代码片段列出了 SHIM 包中用于创建响应对象的两个助手函数。以下函数将响应序列化为 gRPC protobuf 消息:

// Creates a Response object with the Success status and with argument of a 'payload' to return
// if there is no value to return, the argument 'payload' should be set to 'nil'
func shim.Success(payload []byte)

// creates a Response object with the Error status and with an argument of a message of the error
func shim.Error(msg string)
  1. 现在是时候讨论调用参数了。这里,该方法将使用stub.GetFunctionAndParameters函数检索调用的参数,并验证已经提供了预期数量的参数。Init方法要么不接收任何参数,因此保持分类帐不变。当调用Init功能时,会发生这种情况,因为分类帐上的链码被升级到较新的版本。当第一次安装 chaincode 时,它希望收到八个参数,其中包括参与者的详细信息,这些信息将被记录为初始状态。如果提供的参数数量不正确,该方法将返回错误。代码块验证参数如下:
_, args := stub.GetFunctionAndParameters()
var err error

// Upgrade Mode 1: leave ledger state as it was
if len(args) == 0 {
  return shim.Success(nil)
}

// Upgrade mode 2: change all the names and account balances
if len(args) != 8 {
 err = errors.New(fmt.Sprintf("Incorrect number of arguments. Expecting 8: {" +
             "Exporter, " +
             "Exporter's Bank, " +
             "Exporter's Account Balance, " +
             "Importer, " +
             "Importer's Bank, " +
             "Importer's Account Balance, " +
             "Carrier, " +
             "Regulatory Authority" +
             "}. Found %d", len(args)))
  return shim.Error(err.Error())
}

正如我们在前面的代码片段中看到的,当提供了包含参与者的姓名和角色的预期数量的参数时,该方法将验证参数并将其转换为正确的数据类型,并将它们作为初始状态记录到分类帐中。

在以下代码片段的第 2 行和第 7 行,该方法将参数转换为整数。如果转换失败,它将返回一个错误。在第 14 行,一个字符串数组由字符串常量构成。这里,我们引用文件constants.go中定义的词法常量,该文件位于chaincode文件夹中。常量代表将初始值记录到分类帐中的键。最后,在第 16 行,每个常数都有一条记录(资产)被写到分类账上。函数stub.PutState将一个键和值对记录到分类账中。

注意,分类帐上的数据存储为字节数组;我们要存储在分类帐中的任何数据都必须首先转换成一个字节数组,如下面的代码片段所示:

// Type checks
_, err = strconv.Atoi(string(args[2]))
if err != nil {
    fmt.Printf("Exporter's account balance must be an integer. Found %s\n", args[2])
    return shim.Error(err.Error())
}
_, err = strconv.Atoi(string(args[5]))
if err != nil {
    fmt.Printf("Importer's account balance must be an integer. Found %s\n", args[5])
    return shim.Error(err.Error())
}

// Map participant identities to their roles on the ledger
roleKeys := []string{ expKey, ebKey, expBalKey, impKey, ibKey, impBalKey, carKey, raKey }
for i, roleKey := range roleKeys {
    err = stub.PutState(roleKey, []byte(args[i]))
    if err != nil {
        fmt.Errorf("Error recording key %s: %s\n", roleKey, err.Error())
        return shim.Error(err.Error())
    }
}

调用方法

每当查询或修改区块链的状态时,就会调用Invoke方法。

对总账上持有的资产的所有创建读取更新删除 ( CRUD )操作都由Invoke方法封装。

当调用客户端创建一个事务时,就会调用这个方法。当查询总账的状态时(即检索到一项或多项资产,但没有修改总账的状态),客户端收到Invoke的响应后,将丢弃该上下文交易。一旦分类账被修改,修改将被记录到交易中。在收到将交易记录在分类账中的响应后,客户将把该交易提交给订购服务。下面的代码片段显示了一个空的Invoke方法:

func (t *TradeWorkflowChaincode) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
    fmt.Println("TradeWorkflow Invoke")
}

通常,chaincode 的实现将包含多个查询和修改函数。如果这些函数非常简单,可以直接在Invoke方法的主体中实现。然而,更好的解决方案是独立实现每个函数,然后从Invoke方法调用它们。

SHIM API 提供了几个函数来检索Invoke方法的调用参数。这些在下面的代码片段中列出。由开发人员选择参数的含义和顺序;然而,按照惯例,Invoke方法的第一个参数是函数的名称,下面的参数是该函数的参数。

// Returns the first argument as the function name and the rest of the arguments as parameters in a string array.
// The client must pass only arguments of the type string.
func GetFunctionAndParameters() (string, []string)

// Returns all arguments as a single string array.
// The client must pass only arguments of the type string.
func GetStringArgs() []string

// Returns the arguments as an array of byte arrays.
func GetArgs() [][]byte

// Returns the arguments as a single byte array.
func GetArgsSlice() ([]byte, error)

在下面的代码片段中,使用stub.GetFunctionAndParameters函数在第 1 行检索调用的参数。从第 3 行开始,一系列的if条件通过执行,连同参数一起进入请求的函数(requestTradeacceptTrade等等)。这些功能中的每一个都单独实现它们的功能。如果请求一个不存在的函数,该方法将返回一个错误,指示所请求的函数不存在,如第 18 行所示:

    function, args := stub.GetFunctionAndParameters()

    if function == "requestTrade" {
        // Importer requests a trade
        return t.requestTrade(stub, creatorOrg, creatorCertIssuer, args)
    } else if function == "acceptTrade" {
        // Exporter accepts a trade
        return t.acceptTrade(stub, creatorOrg, creatorCertIssuer, args)
    } else if function == "requestLC" {
        // Importer requests an L/C
        return t.requestLC(stub, creatorOrg, creatorCertIssuer, args)
    } else if function == "issueLC" {
        // Importer's Bank issues an L/C
        return t.issueLC(stub, creatorOrg, creatorCertIssuer, args)
    } else if function == "acceptLC" {
  ...

  return shim.Error("Invalid invoke function name")

正如您所看到的,Invoke方法是提取和验证被请求函数将使用的参数所需的任何共享代码的合适位置。在下一节中,我们将研究访问控制机制,并将一些共享的访问控制代码放入Invoke方法中。

访问控制

在我们深入研究Chaincode函数的实现之前,我们需要首先定义我们的访问控制机制。

安全和许可的区块链的一个关键特征是访问控制。在 Fabric 中,成员资格服务提供商 ( MSP )在启用访问控制方面发挥着关键作用。结构网络的每个组织可以有一个或多个 MSP 提供商。MSP 被实现为一个认证机构 ( 结构 CA )。有关 Fabric CA 的更多信息,包括其文档,请访问:https://hyperledger-fabric-ca.readthedocs.io/.

Fabric CA 为网络用户颁发注册证书 ( ecerts )。ecert 表示用户的身份,并在用户向 Fabric 提交时用作签名事务。因此,在调用事务之前,用户必须首先注册并从结构 CA 获得 ecert。

Fabric 支持一种基于属性的访问控制机制,链代码可以使用这种机制来控制对其功能和数据的访问。ABAC 允许链码根据与用户身份相关的属性做出访问控制决定。拥有 ecert 的用户还可以访问一系列附加属性(即名称/值对)。

在调用期间,链代码将提取属性并做出访问控制决定。在接下来的章节中,我们将仔细研究 ABAC 机制。

列线图

在下面的步骤中,我们将向您展示如何注册一个用户并创建一个带有属性的 ecert。然后,我们将在链码中检索用户身份和属性,以验证访问控制。然后,我们将把这个功能集成到我们的教程链代码中。

首先,我们必须向结构 CA 注册一个新用户。作为注册过程的一部分,我们必须定义 ecert 生成后将使用的属性。通过运行命令fabric-ca-client register来注册用户。使用后缀:ecert添加访问控制属性。

注册用户

这些步骤仅供参考,不能执行。更多信息可以参考 GitHub 知识库https://GitHub . com/hyperledger handson/trade-finance-logistics/blob/master/chain code/ABAC . MD

现在让我们用名为importer的自定义属性和值true注册一个用户。请注意,属性的值可以是任何类型,并且不限于布尔值,如下面的代码片段所示:

fabric-ca-client register --id.name user1 --id.secret pwd1 --id.type user --id.affiliation ImporterOrgMSP --id.attrs 'importer=true:ecert'

前面的代码片段向我们展示了使用属性importer=true注册用户时的命令行。请注意,id.secret和其他参数的值取决于结构 CA 配置。

前面的命令也可以一次定义多个默认属性,比如:--id.attrsimporter=true:ecert,email=user1@gmail.com

下表包含用户注册期间使用的默认属性:

| 属性名 | 命令行参数 | 属性值 | | hf。注册 ID | (自动) | 身份的注册 ID | | hf。类型 | id .类型 | 身份的类型 | | hf。加入 | id .从属关系 | 身份的从属关系 |

如果 ecert 需要前面的任何属性,必须首先在用户注册命令中定义它们。例如,下面的命令用属性hf.Affiliation=ImporterOrgMSP注册user1,默认情况下,该属性将被复制到 ecert 中:

fabric-ca-client register --id.name user1 --id.secret pwd1 --id.type user --id.affiliation ImporterOrgMSP --id.attrs 'importer=true:ecert,hf.Affiliation=ImporterOrgMSP:ecert'

注册用户

在这里,我们将注册用户并创建 ecert。enrollment.attrs定义哪些属性将从用户注册中复制到 ecert 中。后缀 opt 定义了那些从注册中复制的属性中哪些是可选的。如果用户注册中未定义一个或多个非可选属性,注册将会失败。以下命令将注册一个属性为importer的用户:

fabric-ca-client enroll -u http://user1:pwd1@localhost:7054 --enrollment.attrs "importer,email:opt"

在链码中检索用户身份和属性

在这一步中,我们将在链码执行期间检索用户的身份。链码可用的 ABAC 功能由客户端身份链码 ( CID )库提供。

提交给 chaincode 的每个交易建议都带有调用者(提交交易的用户)的 ecert。chaincode 通过导入 CID 库并调用带有参数ChaincodeStubInterface的库函数,即在InitInvoke方法中接收的参数stub来访问 ecert。

chaincode 可以使用证书来提取关于调用者的信息,包括:

  • 调用者的 ID
  • 颁发调用者证书的会员服务提供商 ( MSP )的唯一 ID
  • 证书的标准属性,如域名、电子邮件等
  • 与客户端身份相关联的 ecert 属性,存储在证书中

CID 库提供的函数在下面的代码片段中列出:

// Returns the ID associated with the invoking identity. 
// This ID is unique within the MSP (Fabric CA) which issued the identity, however, it is not guaranteed to be unique across all MSPs of the network. 
func GetID() (string, error) 

// Returns the unique ID of the MSP associated with the identity that submitted the transaction. 
// The combination of the MSPID and of the identity ID are guaranteed to be unique across the network. 
func GetMSPID() (string, error) 

// Returns the value of the ecert attribute named `attrName`. 
// If the ecert has the attribute, the `found` returns true and the `value` returns the value of the attribute. 
// If the ecert does not have the attribute, `found` returns false and `value` returns empty string. 
func GetAttributeValue(attrName string) (value string, found bool, err error) 

// The function verifies that the ecert has the attribute named `attrName` and that the attribute value equals to `attrValue`. 
// The function returns nil if there is a match, else, it returns error. 
func AssertAttributeValue(attrName, attrValue string) error 

// Returns the X509 identity certificate. 
// The certificate is an instance of a type Certificate from the library "crypto/x509". 
func GetX509Certificate() (*x509.Certificate, error)  

在下面的代码块中,我们定义了一个函数getTxCreatorInfo,它获取关于调用者的基本身份信息。首先,我们必须导入 CID 和 x509 库,如第 3 行和第 4 行所示。在第 13 行检索唯一的 MSPID,在第 19 行获得 X509 证书。在第 24 行,我们检索证书的CommonName,它包含网络中结构 CA 的唯一字符串。这两个属性由函数返回,并在后续的访问控制验证中使用,如下面的代码片段所示:

import ( 
   "fmt" 
   "github.com/hyperledger/fabric/core/chaincode/shim" 
   "github.com/hyperledger/fabric/core/chaincode/lib/cid" 
   "crypto/x509" 
) 

func getTxCreatorInfo(stub shim.ChaincodeStubInterface) (string, string, error) { 
   var mspid string 
   var err error 
   var cert *x509.Certificate 

   mspid, err = cid.GetMSPID(stub) 
   if err != nil { 
         fmt.Printf("Error getting MSP identity: %sn", err.Error()) 
         return "", "", err 
   } 

   cert, err = cid.GetX509Certificate(stub) 
   if err != nil { 
         fmt.Printf("Error getting client certificate: %sn", err.Error()) 
         return "", "", err 
   } 

   return mspid, cert.Issuer.CommonName, nil 
}

我们现在需要在链代码中定义和实现简单的访问控制策略。链码的每个功能只能由特定组织的成员调用;因此,每个 chaincode 函数将验证调用者是否是所需组织的成员。例如,函数requestTrade只能由Importer组织的成员调用。在下面的代码片段中,函数authenticateImporterOrg验证调用者是否是ImporterOrgMSP的成员。然后将从requestTrade函数中调用该函数来实施访问控制。

func authenticateExportingEntityOrg(mspID string, certCN string) bool {
    return (mspID == "ExportingEntityOrgMSP") && (certCN == "ca.exportingentityorg.trade.com")
}
func authenticateExporterOrg(mspID string, certCN string) bool {
return (mspID == "ExporterOrgMSP") && (certCN == "ca.exporterorg.trade.com")
}
func authenticateImporterOrg(mspID string, certCN string) bool {
    return (mspID == "ImporterOrgMSP") && (certCN == "ca.importerorg.trade.com")
}
func authenticateCarrierOrg(mspID string, certCN string) bool {
    return (mspID == "CarrierOrgMSP") && (certCN == "ca.carrierorg.trade.com")
}
func authenticateRegulatorOrg(mspID string, certCN string) bool {
    return (mspID == "RegulatorOrgMSP") && (certCN == "ca.regulatororg.trade.com")
}

在下面的代码片段中,显示了访问控制验证的调用,它只授予ImporterOrgMSP的成员访问权限。使用从getTxCreatorInfo函数获得的参数调用该函数。

creatorOrg, creatorCertIssuer, err = getTxCreatorInfo(stub)
if !authenticateImporterOrg(creatorOrg, creatorCertIssuer) {
    return shim.Error("Caller not a member of Importer Org. Access denied.")
}

现在,我们需要将我们的认证函数放入一个单独的文件accessControlUtils.go,它与主文件tradeWorkflow.go位于同一个目录中。这个文件在编译时会自动导入到主chaincode文件中,这样我们就可以引用其中定义的函数。

实现链码功能

至此,我们已经有了链码的基本构件。我们有启动链码的Init方法和从客户端和访问控制机制接收请求的Invoke方法。现在,我们需要定义链码的功能。

基于我们的场景,下表总结了记录和检索分类帐数据的函数列表,以提供智能合约的业务逻辑。这些表还定义了组织成员的访问控制定义,这是调用相应功能所必需的。

下表说明了链码修改功能,即如何在分类帐中记录交易:

| 功能名称 | 允许调用 | 描述 | | requestTrade | 进口商 | 请求贸易协定 | | acceptTrade | 出口商 | 接受贸易协定 | | requestLC | 进口商 | 请求信用证 | | issueLC | 进口商 | 开具信用证 | | acceptLC | 出口商 | 接受信用证 | | requestEL | 出口商 | 申请出口许可证 | | issueEL | 调整者 | 签发出口许可证 | | prepareShipment | 出口商 | 准备装运 | | acceptShipmentAndIssueBL | 带菌者 | 接受货物并签发提单 | | requestPayment | 出口商 | 请求付款 | | makePayment | 进口商 | 进行支付 | | updateShipmentLocation | 带菌者 | 更新装运地点 |

下表说明了链码查询函数,即从分类帐中检索数据所需的函数:

| 功能名称 | 允许调用 | 描述 | | getTradeStatus | 出口商/出口实体/进口商 | 获取贸易协议的当前状态 | | getLCStatus | 出口商/出口实体/进口商 | 获取信用证的当前状态 | | getELStatus | 出口实体/监管机构 | 获取出口许可证的当前状态 | | getShipmentLocation | 出口商/出口实体/进口商/承运人 | 获取货物的当前位置 | | getBillOfLading | 出口商/出口实体/进口商 | 拿提单 | | getAccountBalance | 出口商/出口实体/进口商 | 获取给定参与者的当前帐户余额 |

定义链码资产

我们现在要确定我们资产的结构,这将被记录到分类账中。在 Go 中,资产被定义为具有一系列属性名称和类型的结构类型。定义还需要包含 JSON 属性名,这将用于将资产序列化到 JSON 对象中。在下面的代码片段中,您将看到我们的应用程序中四种资产的定义。请注意,结构的属性可以封装其他结构,从而允许创建多级树。

type TradeAgreement struct { 
   Amount                    int               `json:"amount"` 
   DescriptionOfGoods        string            `json:"descriptionOfGoods"` 
   Status                    string            `json:"status"` 
   Payment                   int               `json:"payment"` 
} 

type LetterOfCredit struct { 
   Id                        string            `json:"id"` 
   ExpirationDate            string            `json:"expirationDate"` 
   Beneficiary               string            `json:"beneficiary"` 
   Amount                    int               `json:"amount"` 
   Documents                 []string          `json:"documents"` 
   Status                    string            `json:"status"` 
} 

type ExportLicense struct { 
   Id                        string            `json:"id"` 
   ExpirationDate            string            `json:"expirationDate"` 
   Exporter                  string            `json:"exporter"` 
   Carrier                   string            `json:"carrier"` 
   DescriptionOfGoods        string            `json:"descriptionOfGoods"` 
   Approver                  string            `json:"approver"` 
   Status                    string            `json:"status"` 
} 

type BillOfLading struct { 
   Id                        string            `json:"id"` 
   ExpirationDate            string            `json:"expirationDate"` 
   Exporter                  string            `json:"exporter"` 
   Carrier                   string            `json:"carrier"` 
   DescriptionOfGoods        string            `json:"descriptionOfGoods"` 
   Amount                    int               `json:"amount"` 
   Beneficiary               string            `json:"beneficiary"` 
   SourcePort                string            `json:"sourcePort"` 
   DestinationPort           string            `json:"destinationPort"` 
}  

编码链码功能

在这一节中,我们将实现我们之前看到的 chaincode 函数。为了实现 chaincode 函数,我们将使用三个 SHIM API 函数,它们将从 Worldstate 中读取资产并记录变更。正如我们已经了解的,这些函数的读取和写入分别被记录到ReadSetWriteSet中,并且这些改变不会立即影响分类帐的状态。只有在交易通过验证并提交到分类帐后,更改才会生效。

下面的代码片段显示了资产 API 函数的列表:

// Returns the value of the `key` from the Worldstate. 
// If the key does not exist in the Worldstate the function returns (nil, nil). 
// The function does not read data from the WriteSet and hence uncommitted values modified by PutState are not returned. 
func GetState(key string) ([]byte, error) 

// Records the specified `key` and `value` into the WriteSet. 
// The function does not affect the ledger until the transaction is committed into the ledger. 
func PutState(key string, value []byte) error 

// Marks the the specified `key` as deleted in the WriteSet. 
// The key will be marked as deleted and removed from Worldstate once the transaction is committed into the ledger. 
func DelState(key string) error

创建资产

既然我们可以实现我们的第一个 chaincode 函数,我们将继续并实现一个requestTrade函数,它将创建一个状态为REQUESTED的新贸易协议,然后将该协议记录在分类帐中。

该函数的实现如下面的代码片段所示。正如您将看到的,在第 9 行中,我们验证了调用者是ImporterOrg的成员,并且拥有调用该函数的权限。从第 13 行到第 21 行,我们验证并提取参数。在第 23 行,我们创建了一个新的TradeAgreement实例,它由接收到的参数初始化。正如我们之前了解到的,分类帐以字节数组的形式存储值。因此,在第 24 行,我们用 JSON 将TradeAgreement序列化为一个字节数组。在第 32 行,我们创建了一个惟一的键,在这个键下我们将存储TradeAgreement。最后,在第 37 行,我们使用键和序列化的TradeAgreement以及函数PutState将值存储到WriteSet中。

下面的代码片段说明了requestTrade函数:

func (t *TradeWorkflowChaincode) requestTrade(stub shim.ChaincodeStubInterface, creatorOrg string, creatorCertIssuer string, args []string) pb.Response { 
   var tradeKey string 
   var tradeAgreement *TradeAgreement 
   var tradeAgreementBytes []byte 
   var amount int 
   var err error 

   // Access control: Only an Importer Org member can invoke this transaction 
   if !t.testMode && !authenticateImporterOrg(creatorOrg, creatorCertIssuer) { 
         return shim.Error("Caller not a member of Importer Org. Access denied.") 
   } 

   if len(args) != 3 { 
         err = errors.New(fmt.Sprintf("Incorrect number of arguments. Expecting 3: {ID, Amount, Description of Goods}. Found %d", len(args))) 
         return shim.Error(err.Error()) 
   } 

   amount, err = strconv.Atoi(string(args[1])) 
   if err != nil { 
         return shim.Error(err.Error()) 
   } 

   tradeAgreement = &TradeAgreement{amount, args[2], REQUESTED, 0} 
   tradeAgreementBytes, err = json.Marshal(tradeAgreement) 
   if err != nil { 
         return shim.Error("Error marshaling trade agreement structure") 
   } 

   // Write the state to the ledger 
   tradeKey, err = getTradeKey(stub, args[0]) 
   if err != nil { 
         return shim.Error(err.Error()) 
   } 
   err = stub.PutState(tradeKey, tradeAgreementBytes) 
   if err != nil { 
         return shim.Error(err.Error()) 
   } 
   fmt.Printf("Trade %s request recorded", args[0]) 

   return shim.Success(nil) 
}  

读取和修改资产

在我们实现了创建贸易协议的函数之后,我们需要实现一个接受贸易协议的函数。此功能将检索协议,将其状态修改为ACCEPTED,并将其放回分类帐。

这个函数的实现如下面的代码片段所示。在代码中,我们构造了想要检索的贸易协议的惟一组合键。在第 22 行,我们用函数GetState检索值。在第 33 行,我们将字节数组反序列化为TradeAgreement结构的实例。在第 41 行,我们修改了状态,因此它显示为ACCEPTED;最后,在第 47 行,我们将更新后的值存储在分类账中,如下所示:

func (t *TradeWorkflowChaincode) acceptTrade(stub shim.ChaincodeStubInterface, creatorOrg string, creatorCertIssuer string, args []string) pb.Response { 
   var tradeKey string 
   var tradeAgreement *TradeAgreement 
   var tradeAgreementBytes []byte 
   var err error 

   // Access control: Only an Exporting Entity Org member can invoke this transaction 
   if !t.testMode && !authenticateExportingEntityOrg(creatorOrg, creatorCertIssuer) { 
         return shim.Error("Caller not a member of Exporting Entity Org. Access denied.") 
   } 

   if len(args) != 1 { 
         err = errors.New(fmt.Sprintf("Incorrect number of arguments. Expecting 1: {ID}. Found %d", len(args))) 
         return shim.Error(err.Error()) 
   } 

   // Get the state from the ledger 
   tradeKey, err = getTradeKey(stub, args[0]) 
   if err != nil { 
         return shim.Error(err.Error()) 
   } 
   tradeAgreementBytes, err = stub.GetState(tradeKey) 
   if err != nil { 
         return shim.Error(err.Error()) 
   } 

   if len(tradeAgreementBytes) == 0 { 
         err = errors.New(fmt.Sprintf("No record found for trade ID %s", args[0])) 
         return shim.Error(err.Error()) 
   } 

   // Unmarshal the JSON 
   err = json.Unmarshal(tradeAgreementBytes, &tradeAgreement) 
   if err != nil { 
         return shim.Error(err.Error()) 
   } 

   if tradeAgreement.Status == ACCEPTED { 
         fmt.Printf("Trade %s already accepted", args[0]) 
   } else { 
         tradeAgreement.Status = ACCEPTED 
         tradeAgreementBytes, err = json.Marshal(tradeAgreement) 
         if err != nil { 
               return shim.Error("Error marshaling trade agreement structure") 
         } 
         // Write the state to the ledger 
         err = stub.PutState(tradeKey, tradeAgreementBytes) 
         if err != nil { 
               return shim.Error(err.Error()) 
         } 
   } 
   fmt.Printf("Trade %s acceptance recordedn", args[0]) 

   return shim.Success(nil) 
}  

主要功能

最后但同样重要的是,我们将添加main函数:Go 程序的初始点。当链代码的一个实例被部署在一个对等体上时,执行main函数来启动链代码。

在下面代码片段的第 2 行中,chaincode 被实例化。函数shim.Start启动第 4 行的链码,并将其注册到对等体,如下所示:

func main() { 
   twc := new(TradeWorkflowChaincode) 
   twc.testMode = false 
   err := shim.Start(twc) 
   if err != nil { 
         fmt.Printf("Error starting Trade Workflow chaincode: %s", err) 
   } 
} 

测试链码

现在我们可以为我们的 chaincode 函数编写单元测试,我们将使用内置的自动化 Go 测试框架。欲了解更多信息和文档,请访问 Go 官方网站:https://golang.org/pkg/testing/

框架自动寻找并执行具有以下签名的函数:

 func TestFname(*testing.T)

函数名Fname是一个任意名称,必须以大写字母开头。

注意,包含单元测试的测试套件文件必须以后缀_test.go结尾;因此,我们的测试套件文件将被命名为tradeWorkflow_test.go,并与我们的chaincode文件放在同一个目录中。test函数的第一个参数是T类型的,它提供了管理测试状态和支持格式化测试日志的函数。测试的输出被写入标准输出,可以在终端中检查。

SHIM 嘲讽

SHIM 包提供了一个全面的模仿模型,可以用来测试链代码。在我们的单元测试中,我们将使用MockStub类型,它为单元测试链代码提供了ChaincodeStubInterface的实现。

测试 Init 方法

首先,我们需要定义调用Init方法所需的函数。该函数将接收对MockStub的引用,以及传递给Init方法的参数数组。在下面的代码的第 2 行中,使用接收到的参数调用 chaincode 函数Init,然后在第 3 行中进行验证。

下面的代码片段说明了对Init方法的调用:

 func checkInit(t *testing.T, stub *shim.MockStub, args [][]byte) { 
   res := stub.MockInit("1", args) 
   if res.Status != shim.OK { 
         fmt.Println("Init failed", string(res.Message)) 
         t.FailNow() 
   } 
} 

我们现在将定义准备Init函数参数值的默认数组所需的函数,如下所示:

func getInitArguments() [][]byte { 
   return [][]byte{[]byte("init"), 
               []byte("LumberInc"), 
               []byte("LumberBank"), 
               []byte("100000"), 
               []byte("WoodenToys"), 
               []byte("ToyBank"), 
               []byte("200000"),
               []byte("UniversalFreight"), 
               []byte("ForestryDepartment")} 
} 

我们现在将定义Init函数的测试,如下面的代码片段所示。测试首先创建 chaincode 的一个实例,然后设置模式为 test,最后为 chaincode 创建一个新的MockStub。在第 7 行,调用checkInit函数并执行Init函数。最后,从第 9 行开始,我们将验证分类帐的状态,如下所示:

func TestTradeWorkflow_Init(t *testing.T) { 
   scc := new(TradeWorkflowChaincode) 
   scc.testMode = true 
   stub := shim.NewMockStub("Trade Workflow", scc) 

   // Init 
   checkInit(t, stub, getInitArguments()) 

   checkState(t, stub, "Exporter", EXPORTER) 
   checkState(t, stub, "ExportersBank", EXPBANK) 
   checkState(t, stub, "ExportersAccountBalance", strconv.Itoa(EXPBALANCE)) 
   checkState(t, stub, "Importer", IMPORTER) 
   checkState(t, stub, "ImportersBank", IMPBANK) 
   checkState(t, stub, "ImportersAccountBalance", strconv.Itoa(IMPBALANCE)) 
   checkState(t, stub, "Carrier", CARRIER) 
   checkState(t, stub, "RegulatoryAuthority", REGAUTH) 
}

接下来,我们使用checkState函数验证每个键的状态是否如预期的那样,如下面的代码块所示:

func checkState(t *testing.T, stub *shim.MockStub, name string, value string) { 
  bytes := stub.State[name] 
  if bytes == nil { 
    fmt.Println("State", name, "failed to get value") 
    t.FailNow() 
  } 
  if string(bytes) != value {
    fmt.Println("State value", name, "was", string(bytes), "and not", value, "as expected")
    t.FailNow()
  }
} 

测试调用方法

现在是时候为Invoke函数定义测试了。在下面代码块的第 7 行中,checkInit被调用来初始化分类帐,然后第 13 行的checkInvoke调用requestTrade函数。requestTrade函数创建一个新的交易资产,并将其存储在分类账中。为了验证分类帐包含正确的状态,在第 17 行计算新的组合键之前,在第 15 和 16 行创建并序列化一个新的TradeAgreement。最后,在第 18 行,根据序列化值验证键的状态。

此外,如前所述,我们的链码包含一系列共同定义交易工作流的函数。我们将在测试中将这些函数的调用链接成一个序列,以验证整个工作流。整个函数的代码可以在位于chaincode文件夹的测试文件中找到。

func TestTradeWorkflow_Agreement(t *testing.T) { 
   scc := new(TradeWorkflowChaincode) 
   scc.testMode = true 
   stub := shim.NewMockStub("Trade Workflow", scc) 

   // Init 
   checkInit(t, stub, getInitArguments()) 

   // Invoke 'requestTrade' 
   tradeID := "2ks89j9" 
   amount := 50000 
   descGoods := "Wood for Toys" 
   checkInvoke(t, stub, [][]byte{[]byte("requestTrade"), []byte(tradeID), []byte(strconv.Itoa(amount)), []byte(descGoods)}) 

   tradeAgreement := &TradeAgreement{amount, descGoods, REQUESTED, 0} 
   tradeAgreementBytes, _ := json.Marshal(tradeAgreement) 
   tradeKey, _ := stub.CreateCompositeKey("Trade", []string{tradeID}) 
   checkState(t, stub, tradeKey, string(tradeAgreementBytes)) 
   ... 
}

下面的代码片段显示了函数checkInvoke

func checkInvoke(t *testing.T, stub *shim.MockStub, args [][]byte) { 
   res := stub.MockInvoke("1", args) 
   if res.Status != shim.OK { 
         fmt.Println("Invoke", args, "failed", string(res.Message)) 
         t.FailNow() 
   } 
}

运行测试

我们现在准备运行我们的测试!go test命令将执行在tradeWorkflow_test.go文件中找到的所有测试。该文件包含一长串测试,这些测试验证了我们的工作流中定义的功能。

现在,让我们使用以下命令在终端中运行测试:

$ cd $GOPATH/src/trade-finance-logistics/chaincode/src/github.com/trade_workflow_v1 
$ go test 

前面的命令应该生成以下输出:

Initializing Trade Workflow 
Exporter: LumberInc 
Exporter's Bank: LumberBank 
Exporter's Account Balance: 100000 
Importer: WoodenToys 
Importer's Bank: ToyBank 
Importer's Account Balance: 200000 
Carrier: UniversalFreight 
Regulatory Authority: ForestryDepartment 
... 
Amount paid thus far for trade 2ks89j9 = 25000; total required = 50000 
Payment request for trade 2ks89j9 recorded 
TradeWorkflow Invoke 
TradeWorkflow Invoke 
Query Response:{"Balance":"150000"} 
TradeWorkflow Invoke 
Query Response:{"Balance":"150000"} 
PASS 
ok       trade-finance-logistics/chaincode/src/github.com/trade_workflow_v1      0.036s 

链码设计主题

组合键

我们经常需要在分类账中存储同一类型的多个实例,比如多个贸易协议、信用证等等。在这种情况下,这些实例的键通常由属性的组合构成—例如,"Trade" + ID, yielding ["Trade1","Trade2", ...]。可以在代码中定制实例的键,或者可以在 SHIM 中提供 API 函数,以基于几个属性的组合来构造实例的组合键(换句话说,惟一键)。这些函数简化了组合键的构造。组合键可以像普通的字符串键一样使用PutState()GetState()函数来记录和检索值。

以下代码片段显示了创建和使用组合键的函数列表:

// The function creates a key by combining the attributes into a single string. 
// The arguments must be valid utf8 strings and must not contain U+0000 (nil byte) and U+10FFFF charactres. 
func CreateCompositeKey(objectType string, attributes []string) (string, error) 

// The function splits the compositeKey into attributes from which the key was formed. 
// This function is useful for extracting attributes from keys returned by range queries. 
func SplitCompositeKey(compositeKey string) (string, []string, error) 

在下面的代码片段中,我们可以看到一个函数getTradeKey,它通过将关键字Trade与交易的 ID 相结合,构建了一个交易协议的唯一组合键:

func getTradeKey(stub shim.ChaincodeStubInterface, tradeID string) (string, error) { 
   tradeKey, err := stub.CreateCompositeKey("Trade", []string{tradeID}) 
   if err != nil { 
         return "", err 
   } else { 
         return tradeKey, nil 
   } 
}

在更复杂的场景中,键可以由多个属性构成。组合键还允许您在范围查询中基于键的组成部分搜索资产。我们将在接下来的章节中更详细地探讨搜索。

范围查询

除了使用惟一键检索资产,SHIM 还为 API 函数提供了根据范围标准检索资产集的机会。此外,可以对组合键进行建模,以支持针对键的多个组件的查询。

range 函数返回一个迭代器(StateQueryIteratorInterface),遍历一组匹配查询标准的键。返回的键按词法顺序排列。迭代器必须通过调用函数Close()来关闭。此外,当一个组合键有多个属性时,范围查询功能GetStateByPartialCompositeKey()可用于搜索匹配属性子集的键。

例如,可以在与特定的TradeId相关联的所有支付中搜索由TradeIdPaymentId组成的支付的关键字,如下面的代码片段所示:

 // Returns an iterator over all keys between the startKey (inclusive) and endKey (exclusive). 
// To query from start or end of the range, the startKey and endKey can be an empty. 
func GetStateByRange(startKey, endKey string) (StateQueryIteratorInterface, error) 

// Returns an iterator over all composite keys whose prefix matches the given partial composite key. 
// Same rules as for arguments of CreateCompositeKey function apply. 
func GetStateByPartialCompositeKey(objectType string, keys []string) (StateQueryIteratorInterface, error) 

我们还可以使用以下查询搜索 ID 在 1-100 范围内的所有贸易协议:

startKey, err = getTradeKey(stub, "1") 
endKey, err = getTradeKey(stub, "100") 

keysIterator, err := stub.GetStateByRange(startKey, endKey) 
if err != nil { 
    return shim.Error(fmt.Printf("Error accessing state: %s", err)) 
} 

defer keysIterator.Close() 

var keys []string 
for keysIterator.HasNext() { 
    key, _, err := keysIterator.Next() 
    if err != nil { 
        return shim.Error(fmt.Printf("keys operation failed. Error accessing state: %s", err)) 
    } 
    keys = append(keys, key) 
}

状态查询和 CouchDB

默认情况下,Fabric 使用 LevelDB 作为 Worldstate 的存储。Fabric 还提供了配置对等点以在 CouchDB 中存储 Worldstate 的选项。当资产以 JSON 文档的形式存储时,CouchDB 允许您基于资产状态对资产执行复杂的查询。

查询以本机 CouchDB 声明性 JSON 查询语法进行格式化。此语法的当前版本可在: http://docs.couchdb.org/en/2.1.1/api/database/find.html. 获得

Fabric 将查询转发给 CouchDB 并返回一个迭代器(StateQueryIteratorInterface()),该迭代器可用于迭代结果集。基于状态的查询函数的声明如下:

func GetQueryResult(query string) (StateQueryIteratorInterface, error)

在下面的代码片段中,我们可以看到一个基于州的查询,查询所有状态为ACCEPTED且收到的付款超过 1000 英镑的贸易协议。然后执行查询,并将找到的文档写入终端,如下所示:

// CouchDB query definition
queryString :=
`{
    "selector": {
            "status": "ACCEPTED"
            "payment": {
                    "$gt": 1000
            }
    }
}`

fmt.Printf("queryString:\n%s\n", queryString)

// Invoke query
resultsIterator, err := stub.GetQueryResult(queryString)
if err != nil {
    return nil, err
}
defer resultsIterator.Close()

var buffer bytes.Buffer
buffer.WriteString("[")

// Iterate through all returned assets
bArrayMemberAlreadyWritten := false
for resultsIterator.HasNext() {
    queryResponse, err := resultsIterator.Next()
    if err != nil {
        return nil, err
    }
    if bArrayMemberAlreadyWritten == true {
        buffer.WriteString(",")
    }
    buffer.WriteString("{\"Key\":")
    buffer.WriteString("\"")
    buffer.WriteString(queryResponse.Key)
    buffer.WriteString("\"")

    buffer.WriteString(", \"Record\":")
    buffer.WriteString(string(queryResponse.Value))
    buffer.WriteString("}")
    bArrayMemberAlreadyWritten = true
}
buffer.WriteString("]")

fmt.Printf("queryResult:\n%s\n", buffer.String())

注意,与对键的查询不同,对状态的查询不会被记录到事务的ReadSet中。因此,事务的验证实际上不能验证在事务的执行和提交之间是否发生了世界状态的变化。因此,链码设计必须考虑到这一点;如果查询基于预期的调用序列,可能会出现无效的事务。

指数

对大型数据集执行查询是一项计算复杂的任务。Fabric 提供了一种在 CouchDB 托管的 Worldstate 上定义索引的机制,以提高效率。请注意,查询中的排序操作也需要索引。

在 JSON 中,索引是在一个单独的文件中定义的,扩展名为*.json。该格式的完整定义可从以下网址获得:http://docs . couch db . org/en/2 . 1 . 1/API/database/find . html # d b-index

以下代码片段展示了一个索引,该索引与我们之前查看的贸易协定的查询相匹配:

 { 
  "index": { 
    "fields": [ 
      "status", 
      "payment" 
    ] 
  }, 
  "name": "index_sp", 
  "type": "json" 
}  

这里,索引文件放在文件夹/META-INF/statedb/couchdb/indexes中。在编译期间,索引与链码一起打包。在对等体上安装和实例化链代码时,索引会自动部署到 Worldstate 上,供查询使用。

耳机和书写套件

在收到来自客户端的事务调用消息时,签署方执行事务。该执行调用对等体的 Worldstate 上下文中的 chaincode,并将分类帐中的所有数据读写记录到一个ReadSetWriteSet中。

事务的WriteSet包含一个键和值对的列表,这些键和值对在链代码执行期间被修改。当一个键的值被修改时(也就是说,记录一个新的键和值或者用一个新值更新一个现有的键),这个WriteSet将包含更新的键和值对。

当一个键被删除时,WriteSet将包含带有一个属性的键,该属性将该键标记为已删除。如果一个键在链码执行过程中被多次修改,WriteSet将包含最近一次修改的值。

事务的ReadSet包含一个键及其版本的列表,这些键和版本在链代码执行期间被访问。密钥的版本号是从块号和块内的事务号的组合中导出的。这种设计能够有效地搜索和处理数据。事务的另一部分包含关于范围查询及其结果的信息。请记住,当 chaincode 读取一个键的值时,将返回分类帐中最新提交的值。

如果在链码执行期间引入的修改存储在WriteSet中,当链码正在读取执行期间修改的键时,将返回已提交(未修改)值。因此,如果在同一个执行过程中稍后需要修改的值,那么必须实现 chaincode,以便它保留并使用正确的值。

事务的ReadSetWriteSet的示例如下:

{
  "rwset": {
    "reads": [
      {
        "key": "key1",
        "version": {
          "block_num": {
            "low": 9546,
            "high": 0,
            "unsigned": true
          },
          "tx_num": {
            "low": 0,
            "high": 0,
            "unsigned": true
          }
        }
      }
    ],
    "range_queries_info": [],
    "writes": [
      {
        "key": "key1",
        "is_delete": false,
        "value": "value1"
      },
      {
        "key": "key2",
        "is_delete": true
      }
    ]
  }
}

多版本并发控制

Fabric 使用多版本并发控制 ( MVCC )机制来确保分类帐的一致性,并防止重复支出。重复消费攻击旨在通过引入多次使用或修改相同资源的交易来利用系统中的缺陷,例如在加密货币网络中多次消费相同的硬币。键冲突是在处理并行客户端提交的事务时可能发生的另一种问题,它可能试图同时修改相同的键/值对。

此外,由于 Fabric 的分散体系结构,事务执行的顺序可以在不同的 Fabric 组件(包括签署者、排序者和提交者)上进行不同的排序和提交,这反过来会在事务的计算和提交之间引入延迟,在此期间可能会发生键冲突。分散化还使网络容易受到潜在问题和攻击的影响,因为客户端有意或无意地修改了交易的顺序。

为了确保一致性,数据库等计算机系统通常使用锁定机制。但是,锁定需要一种集中的方法,这在 Fabric 中是不可用的。同样值得注意的是,锁定有时会带来性能损失。

为了解决这个问题,Fabric 使用存储在分类帐中的密钥的版本控制系统。版本化系统的目的是确保交易按顺序排序并提交到分类帐中,不会引起不一致。当在提交对等体上接收到块时,将验证块的每个事务。该算法检查ReadSet中的密钥及其版本;如果ReadSet中每个键的版本与世界状态中相同键的版本相匹配,或者与同一块中先前事务的版本相匹配,则该事务被认为是有效的。换句话说,该算法验证在事务执行期间从世界状态读取的数据都没有被更改。

如果事务包含范围查询,这些也将被验证。对于每个范围查询,该算法检查执行查询的结果是否与链代码执行期间的结果完全相同,或者是否发生了任何修改。

未通过此验证的事务在分类帐中被标记为无效,它们引入的更改不会投射到 Worldstate。请注意,由于分类账是不可变的,交易会保留在分类账中。

如果一个事务通过了验证,那么WriteSet将被投射到 Worldstate 上。由事务修改的每个密钥在世界状态中被设置为在WriteSet中指定的新值,并且世界状态中的密钥版本被设置为从事务派生的版本。这样,就可以防止任何不一致的情况,如重复支出。同时,在可能发生键冲突的情况下,链码设计必须考虑 MVCC 的行为。有多种众所周知的解决键冲突和 MVCC 的策略,比如分割资产、使用多个键、事务排队等等。

记录输出

日志记录是系统代码的一个重要部分,支持运行时问题的分析和检测。

登录 Fabric 基于标准 Go 日志包github.com/op/go-logging。日志记录机制提供了基于严重性的日志控制和漂亮的消息装饰。日志记录级别按严重性降序定义,如下所示:

CRITICAL | ERROR | WARNING | NOTICE | INFO | DEBUG 

来自所有组件的日志信息被合并并写入标准错误文件(stderr)。日志可以通过对等体和模块的配置来控制,也可以在链代码的代码中控制。

配置

对等日志记录的默认配置设置为信息级别,但是可以通过以下方式控制该级别:

  1. 命令行选项日志记录级别。该选项覆盖默认配置,如下所示:
peer node start --logging-level=error  

请注意,任何模块或链代码都可以通过命令行选项进行配置,如下面的代码片段所示:

 peer node start --logging-level=chaincode=error:main=info
  1. 默认的日志记录级别也可以用一个environment变量CORE_LOGGING_LEVEL来定义,如下面的代码片段所示:
peer0.org1.example.com:
    environment:
        - CORE_LOGGING_LEVEL=error
  1. core.yml文件中定义网络配置的配置属性也可用于以下代码:
logging:
    level: info
  1. core.yml文件还允许您为特定模块配置日志记录级别,比如为chaincode或消息格式配置日志记录级别,如下面的代码片段所示:
 chaincode: 
   logging: 
         level:  error 
         shim:   warning  

关于各种配置选项的更多细节在core.yml文件的注释中提供。

日志 API

SHIM 包为链代码提供 API 来创建和管理日志记录对象。这些对象生成的日志与对等日志集成在一起。

chaincode 可以创建和使用任意数量的日志对象。每个日志记录对象必须有一个唯一的名称,该名称用于在输出中为日志记录添加前缀,并区分不同日志记录对象和填充程序的记录。(请记住,日志记录对象名称 SHIM API 是保留名称,不应在 chaincode 中使用。)每个日志记录对象都设置了一个日志记录严重性级别,在该级别上,日志记录将被发送到输出。严重级别为CRITICAL的日志记录总是出现在输出中。下面的代码片段列出了在链代码中创建和管理日志记录对象的 API 函数。

// Creates a new logging object. 
func NewLogger(name string) *ChaincodeLogger 

// Converts a case-insensitive string representing a logging level into an element of LoggingLevel enumeration type. 
// This function is used to convert constants of standard GO logging levels (i.e. CRITICAL, ERROR, WARNING, NOTICE, INFO or DEBUG) into the shim's enumeration LoggingLevel type (i.e. LogDebug, LogInfo, LogNotice, LogWarning, LogError, LogCritical). 
func LogLevel(levelString string) (LoggingLevel, error) 

// Sets the logging level of the logging object. 
func (c *ChaincodeLogger) SetLevel(level LoggingLevel) 

// Returns true if the logging object will generate logs at the given level. 
func (c *ChaincodeLogger) IsEnabledFor(level LoggingLevel) bool 

日志记录对象ChaincodeLogger提供了为每个严重级别记录日志的功能。下面列出了ChaincodeLogger的功能。

func (c *ChaincodeLogger) Debug(args ...interface{}) 
func (c *ChaincodeLogger) Debugf(format string, args ...interface{}) 
func (c *ChaincodeLogger) Info(args ...interface{}) 
func (c *ChaincodeLogger) Infof(format string, args ...interface{}) 
func (c *ChaincodeLogger) Notice(args ...interface{}) 
func (c *ChaincodeLogger) Noticef(format string, args ...interface{}) 
func (c *ChaincodeLogger) Warning(args ...interface{}) 
func (c *ChaincodeLogger) Warningf(format string, args ...interface{}) 
func (c *ChaincodeLogger) Error(args ...interface{}) 
func (c *ChaincodeLogger) Errorf(format string, args ...interface{}) 
func (c *ChaincodeLogger) Critical(args ...interface{}) 
func (c *ChaincodeLogger) Criticalf(format string, args ...interface{}) 

记录的默认格式由 SHIM 的配置定义,它在输入参数的打印表示之间放置一个空格。对于每个严重性级别,日志记录对象都提供了一个带有后缀f的附加功能。这些函数允许您使用参数format控制输出的格式。

日志记录对象生成的输出模板如下:

[timestamp] [logger name] [severity level] printed arguments 

所有记录对象和垫片的输出被组合并被发送到标准误差(stderr)。

以下代码块阐释了创建和使用日志记录对象的示例:

var logger = shim.NewLogger("tradeWorkflow") 
logger.SetLevel(shim.LogDebug) 

_, args := stub.GetFunctionAndParameters() 
logger.Debugf("Function: %s(%s)", "requestTrade", strings.Join(args, ",")) 

if !authenticateImporterOrg(creatorOrg, creatorCertIssuer) { 
   logger.Info("Caller not a member of Importer Org. Access denied:", creatorOrg, creatorCertIssuer) 
} 

填补日志记录级别

链代码还可以通过使用 API 函数SetLoggingLevel直接控制其 SHIM 的日志记录严重级别,如下所示:

logLevel, _ := shim.LogLevel(os.Getenv("TW_SHIM_LOGGING_LEVEL"))
shim.SetLoggingLevel(logLevel)

Stdout and stderr

以及由 SHIM API 提供并与对等体集成的日志记录机制,在开发阶段,链代码可以使用标准输出文件。链码作为独立过程执行,因此可以使用标准输出(stdout)和标准误差(stderr)文件,通过标准 Go 打印功能(例如fmt.Printf(...)os.Stdout)记录输出。默认情况下,当链码过程独立启动时,标准输出在Dev模式下可用。

在生产环境中,当链码流程由对等方管理时,出于安全原因,标准输出被禁用。需要时,可以通过设置对等体的配置变量CORE_VM_DOCKER_ATTACHSTDOUT来启用。链码的输出然后与对等体的输出组合。请记住,这些输出仅用于调试目的,不应在生产环境中启用。

以下代码片段说明了附加的 SHIM API 函数:

peer0.org1.example.com: 
   environment: 
         - CORE_VM_DOCKER_ATTACHSTDOUT=true 

清单 4.1:在docker-compose文件中的对等体上启用链码标准输出文件。

附加 SHIM API 函数

在这一节中,我们提供了对链码可用的 shim 的其余 API 函数的概述。

 // Returns an unique Id of the transaction proposal. 
func GetTxID() string 

// Returns an Id of the channel the transaction proposal was sent to. 
func GetChannelID() string 

// Calls an Invoke function on a specified chaincode, in the context of the current transaction. 
// If the invoked chaincode is on the same channel, the ReadSet and WriteSet will be added into the same transaction. 
// If the invoked chaincode is on a different channel, the invocation can be used only as a query. 
func InvokeChaincode(chaincodeName string, args [][]byte, channel string) pb.Response 

// Returns a list of historical states, timestamps and transactions ids. 
func GetHistoryForKey(key string) (HistoryQueryIteratorInterface, error) 

// Returns the identity of the user submitting the transaction proposal. 
func GetCreator() ([]byte, error) 

// Returns a map of fields containing cryptographic material which may be used to implement custom privacy layer in the chaincode. 
func GetTransient() (map[string][]byte, error) 

// Returns data which can be used to enforce a link between application data and the transaction proposal. 
func GetBinding() ([]byte, error) 

// Returns data produced by peer decorators which modified the chaincode input. 
func GetDecorations() map[string][]byte 

// Returns data elements of a transaction proposal. 
func GetSignedProposal() (*pb.SignedProposal, error) 

// Returns a timestamp of the transaction creation by the client. The timestamp is consistent across all endorsers. 
func GetTxTimestamp() (*timestamp.Timestamp, error) 

// Sets an event attached to the transaction proposal response. This event will be be included in the block and ledger. 
func SetEvent(name string, payload []byte) error  

摘要

设计和实现功能良好的 chaincode 是一项复杂的软件工程任务,需要了解 Fabric 体系结构、API 函数和 GO 语言,以及正确实现业务需求。

在本章中,我们一步步地学习了如何在开发模式下启动一个适合链码实现和测试的区块链网络,以及如何使用 CLI 部署和调用链码。然后,我们学习了如何实现我们场景的链码。我们研究了 Chaincode 从客户端接收请求的InitInvoke函数,研究了访问控制机制和开发人员可以用来实现 chaincode 功能的各种 API。

最后,我们学习了如何测试 chaincode 以及如何将日志功能集成到代码中。为了准备下一章,你现在应该使用./trade.sh down -d true 来停止你的网络。


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组