Hyperledger Fabric 2.5.4链码编程|入门篇_智能合约

一、何谓链码(Chaincode)?¶

链码,也称链代码,是一个用Go、Node.js或Java编写的程序,用于实现指定的接口。Chaincode在一个独立于Peer节点的进程中运行,并通过客户端应用程序提交的交易来初始化和管理分类帐状态。

链代码通常处理经网络成员同意的业务逻辑,因此它类似于“智能合约”。可以调用链代码来更新或查询提案交易中的分类帐。根据给定的适当的权限,链代码可以调用同一通道或不同通道中的另一个链代码来访问其状态。

请注意,如果被调用的链码与调用链码分别位于不同的通道上,则只允许调用链码对被调用链码进行读取查询。也就是说,不同通道上被调用的链代码只是一个Query,它不参与后续提交阶段的状态验证检查。

在接下来的内容中,我们将通过应用程序开发人员的视角来探索链代码。我们将使用Fabric官方提供的一个资产转移链代码示例为蓝本,并展示Fabric合约API中每个方法的用途。如果您是一名正在将链代码部署到正在运行的Fabric网络的网络运营人员,结构结合我前面的几篇博文(Hyperledger Fabric 2.5.4开发之通道篇系列)来学习本部分内容。

注意:本文仅仅是概述Fabric智能合约API提供的高级API的使用方法,并没有详细展开介绍。

Fabric智能合约API

Fabric-contract-api为应用程序开发人员提供智能合约开发接口,这是一个高级api,用于实现智能合约。在Hyperledger Fabric中,智能合约也笼统地称为Chaincode,使用这个API为编写业务逻辑提供了一个高级入口。不同语言的Fabric智能合约API文档可在以下链接中找到:

请注意,在使用合约api时,调用的每个链代码函数都会传递一个事务上下文“ctx”(contractapi.TransactionContextInterface),您可以使用它来获取链代码存根(,也称“代理”,所用函数是GetStub()),这个存根又进一步提供了访问账本(通过函数GetState())和请求更新账本(通过函数PutState())的函数。

一个Hello-world级的链码程序由Fabric官方提供,地址是:

https://github.com/hyperledger/fabric-contract-api-go/blob/main/integrationtest/chaincode/simple/main.go

完整代码如下:

// Copyright the Hyperledger Fabric contributors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package main

import (
	"fmt"

	"github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SimpleContract with biz logic
type SimpleContract struct {
	contractapi.Contract
}

// HelloWorld - returns a string
func (sc *SimpleContract) HelloWorld(ctx contractapi.TransactionContextInterface) string {
	return "Hello World"
}

// CallAndResponse - Returns the string you send
func (sc *SimpleContract) CallAndResponse(ctx contractapi.TransactionContextInterface, value string) string {
	return value
}

// PutState - Adds a key value pair to the world state
func (sc *SimpleContract) PutState(ctx contractapi.TransactionContextInterface, key string, value string) error {
	return ctx.GetStub().PutState(key, []byte(value))
}

// GetState - Gets the value for a key from the world state
func (sc *SimpleContract) GetState(ctx contractapi.TransactionContextInterface, key string) (string, error) {
	bytes, err := ctx.GetStub().GetState(key)

	if err != nil {
  return "", nil
	}

	return string(bytes), nil
}

// ExistsState returns true when asset with given ID exists in world state
func (sc *SimpleContract) ExistsState(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
	bytes, err := ctx.GetStub().GetState(id)
	if err != nil {
  return false, fmt.Errorf("failed to read from world state: %v", err)
	}

	return bytes != nil, nil
}

// DeleteState - Deletes a key from the world state
func (sc *SimpleContract) DeleteState(ctx contractapi.TransactionContextInterface, key string) error {
	return ctx.GetStub().DelState(key)
}
//++++++++++++++++++++++++++++++
// Main entrance
//
func main() {
	simpleContract := new(SimpleContract)

	cc, err := contractapi.NewChaincode(simpleContract)

	if err != nil {
  panic(err.Error())
	}

	if err := cc.Start(); err != nil {
  panic(err.Error())
	}
}


本文将以GO语言为链码开发语言,我们将通过实现一个管理简单“资产”的资产转移链代码应用程序来演示这些API的使用。

二、资产转移链码入门实例¶

我们的应用程序是一个基本的链代码示例,用于:


  • 初始化带有资产的分类账
  • 创建、读取、更新和删除资产
  • 检查资产是否存在
  • 将资产从一个所有者转移到另一个所有者名下。

(一)定义示例程序位置¶

首先,请确保你已经安装了Go,并且保证你的系统配置正确。我们假设您使用的是支持模块的GO语言版本。

从Go v1.14 开始,模块被认为可以用于生产环境,并且鼓励所有用户从其他依赖管理系统迁移到模块。

接下来,您需要为链代码应用程序创建一个目录。

为了保持简单,让我们使用以下命令:

// atcc is shorthand for asset transfer chaincode
mkdir atcc && cd atcc

现在,让我们创建模块和一个空的GO语言源文件:

go mod init atcc
touch atcc.go

(二)导入库与数据定义¶

首先,让我们导入库并进行数据定义。与每个链代码一样,它需要实现Fabri智能合约api接口;所以,我们需要为链代码的必要依赖项添加相应的导入语句。我们将导入Fabri智能合约api包并定义我们的SmartContract结构。

package main

import (
  "fmt"
  "encoding/json"
  "log"
  "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

//我们在此定义这个SmartContract结构,用于实现管理资产
type SmartContract struct {
  contractapi.Contract
}

接下来,让我们定义一个结构Asset来表示账本上的简单资产。请注意每一个字段后面的JSON注释,通过这种技巧可以实现将资产封装为存储在账本上的JSON格式。

注意,JSON不是一种确定性的数据格式——元素的顺序可以改变,同时仍然在语义上表示相同的数据。因此,这里的一个小挑战就是:如何能够生成一组一致的JSON?下面的代码中展示了一种实现一致性的不错的方法——按照字母顺序创建Asset对象结构。这样一来,GO语言在封装成JSON格式时在不进行自动排序的情况下能够确保原来结构中各字段的顺序。

type Asset struct {
  AppraisedValue int    `json:"AppraisedValue"`
  Color          string `json:"Color"`
  ID             string `json:"ID"`
  Owner          string `json:"Owner"`
  Size           int    `json:"Size"`
}

(三)初始化链码(账本)¶

接下来,我们将实现InitLedger函数,此处我们使用一些初始数据填充账本。

// InitLedger adds a base set of assets to the ledger
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
  assets := []Asset{
    {ID: "asset1", Color: "blue", Size: 5, Owner: "Tomoko", AppraisedValue: 300},
    {ID: "asset2", Color: "red", Size: 5, Owner: "Brad", AppraisedValue: 400},
    {ID: "asset3", Color: "green", Size: 10, Owner: "Jin Soo", AppraisedValue: 500},
    {ID: "asset4", Color: "yellow", Size: 10, Owner: "Max", AppraisedValue: 600},
    {ID: "asset5", Color: "black", Size: 15, Owner: "Adriana", AppraisedValue: 700},
    {ID: "asset6", Color: "white", Size: 15, Owner: "Michel", AppraisedValue: 800},
  }

  for _, asset := range assets {
    assetJSON, err := json.Marshal(asset)
    if err != nil {
        return err
    }

    err = ctx.GetStub().PutState(asset.ID, assetJSON)
    if err != nil {
        return fmt.Errorf("failed to put to world state. %v", err)
    }
  }

  return nil
}

(四)添加新资产:CreateAsset

接下来,我们编写一个函数CreateAsset,在账本上创建一个新的(以前尚不存在)的资产。

在编写链代码时,最好在对账本执行操作之前先检查该资产是否已经存在,CreateAsset函数源码如下所示。

// CreateAsset issues a new asset to the world state with given details.
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
  exists, err := s.AssetExists(ctx, id)
  if err != nil {
    return err
  }
  if exists {
    return fmt.Errorf("the asset %s already exists", id)
  }

  asset := Asset{
    ID:             id,
    Color:          color,
    Size:           size,
    Owner:          owner,
    AppraisedValue: appraisedValue,
  }
  assetJSON, err := json.Marshal(asset)
  if err != nil {
    return err
  }

  return ctx.GetStub().PutState(id, assetJSON)
}

(五)读取资产:ReadAsset

现在,我们已经用一些初始资产填充了分类账并创建了一个新的资产,那么接下来,让我们编写一个函数ReadAsset,实现从账本中读取资产。

// ReadAsset returns the asset stored in the world state with given id.
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface, id string) (*Asset, error) {
  assetJSON, err := ctx.GetStub().GetState(id)
  if err != nil {
    return nil, fmt.Errorf("failed to read from world state: %v", err)
  }
  if assetJSON == nil {
    return nil, fmt.Errorf("the asset %s does not exist", id)
  }

  var asset Asset
  err = json.Unmarshal(assetJSON, &asset)
  if err != nil {
    return nil, err
  }

  return &asset, nil
}

(六)更新资产:UpdateAsset

至此,我们的账本上已经有了可以交互的资产。接下来,让我们编写一个链码函数UpdateAsset,它允许我们更新允许更改的资产属性

// UpdateAsset updates an existing asset in the world state with provided parameters.
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface, id string, color string, size int, owner string, appraisedValue int) error {
  exists, err := s.AssetExists(ctx, id)
  if err != nil {
    return err
  }
  if !exists {
    return fmt.Errorf("the asset %s does not exist", id)
  }

  // overwriting original asset with new asset
  asset := Asset{
    ID:             id,
    Color:          color,
    Size:           size,
    Owner:          owner,
    AppraisedValue: appraisedValue,
  }
  assetJSON, err := json.Marshal(asset)
  if err != nil {
    return err
  }

  return ctx.GetStub().PutState(id, assetJSON)
}

(七)删除资产——DeleteAsset

在某些情况下,我们可能需要从分类账中删除资产,所以让我们编写一个DeleteAsset函数来处理这一要求。

// DeleteAsset deletes an given asset from the world state.
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface, id string) error {
  exists, err := s.AssetExists(ctx, id)
  if err != nil {
    return err
  }
  if !exists {
    return fmt.Errorf("the asset %s does not exist", id)
  }

  return ctx.GetStub().DelState(id)
}

(八)判断资产是否存在:AssetExists

我们之前说过,在对资产采取行动之前,检查资产是否存在是一个好主意;所以,现在让我们编写一个名为AssetExists的函数来实现该要求。

// AssetExists returns true when asset with given ID exists in world state
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
  assetJSON, err := ctx.GetStub().GetState(id)
  if err != nil {
    return false, fmt.Errorf("failed to read from world state: %v", err)
  }

  return assetJSON != nil, nil
}

(九)转移资产:TransferAsset

这里的转移操作,本质上就是更新操作!

接下来,我们将编写一个称为TransferAsset的函数,该函数可以将资产从一个所有者转移到另一个所有者。

// TransferAsset updates the owner field of asset with given id in world state.
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface, id string, newOwner string) error {
  asset, err := s.ReadAsset(ctx, id)
  if err != nil {
    return err
  }

  asset.Owner = newOwner
  assetJSON, err := json.Marshal(asset)
  if err != nil {
    return err
  }

  return ctx.GetStub().PutState(id, assetJSON)
}


(十)查询资产:GetAllAssets

让我们编写一个称为GetAllAssets的函数,该函数允许查询账本以返回账本上的所有资产。

// GetAllAssets returns all assets found in world state
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
  // range query with empty string for startKey and endKey does an
  // open-ended query of all assets in the chaincode namespace.
  resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
  if err != nil {
    return nil, err
  }
  defer resultsIterator.Close()

  var assets []*Asset
  for resultsIterator.HasNext() {
    queryResponse, err := resultsIterator.Next()
    if err != nil {
      return nil, err
    }

    var asset Asset
    err = json.Unmarshal(queryResponse.Value, &asset)
    if err != nil {
      return nil, err
    }
    assets = append(assets, &asset)
  }

  return assets, nil
}


(十一)链码主程序

最后,我们来编写main函数,它将调用ContractChaincode.Start函数。完整代码如下所示:

package main

import (
  "encoding/json"
  "fmt"
  "log"

  "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// SmartContract provides functions for managing an Asset
type SmartContract struct {
  contractapi.Contract
}

// Asset describes basic details of what makes up a simple asset
type Asset struct {
  ID             string `json:"ID"`
  Color          string `json:"color"`
  Size           int    `json:"size"`
  Owner          string `json:"owner"`
  AppraisedValue int    `json:"appraisedValue"`
}

//......上面的几个资产操作函数(略)
//InitLedger/CreateAsset/ReadAsset/UpdateAsset/DeleteAsset
//AssetExists/TransferAsset/GetAllAssets

//__________________________________________________
//MAIN ENTRANCE
//
func main() {
  assetChaincode, err := contractapi.NewChaincode(&SmartContract{})
  if err != nil {
    log.Panicf("Error creating asset-transfer-basic chaincode: %v", err)
  }

  if err := assetChaincode.Start(); err != nil {
    log.Panicf("Error starting asset-transfer-basic chaincode: %v", err)
  }
}

上面完整的链代码示例是为了使文章思路尽可能清晰明了。在现实世界的链码开发实现中,包可能会被分割存储,其中主包导入链代码包,以便进行简单的单元测试。具体示例,请参阅Fabric示例中的资产转移Go链代码。如果你查看assetTransfer.go,你会发现它包含包main,并导入smartcontract.go中定义的包链代码,位于路径:fabric samples/asset-transfer basic/chaincode-go/chaincode/。

三、链码访问控制问题¶

链码开发结束后,可以利用客户端(提交者)证书通过ctx.GetStub().GetCreator()实现访问控制决策。此外,Fabric Contract API还提供了一组扩展API,这些扩展API能够从提交者的证书中提取客户端身份,从而实现访问控制决策。这个身份可以是基于客户端身份本身、组织身份,还是基于客户端身份属性。

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

四、管理Go编写的链码的外部依赖项

用Go语言编写的链代码依赖于不属于标准库的Go包(如链码shim包)。

当链码包安装到Peer设备时,这些包的源代码必须包含在链码包中。

如果你已经将链代码以结构化方式创建为一个模块,那么最简单的方法是在打包链代码之前,使用go mod vendor提供依赖关系。

go mod tidy
go mod vendor

这个命令会将链码的外部依赖项放置到本地vendor目录中。

归纳起来就是,一旦在您的链码目录中包含了供应商依赖项,peer chaincode packagepeer chaincode install操作就会将与依赖项相关的代码包含到链码软件包中。

完整的go mod vendor命令是:

go mod vendor [-e] [-v] [-o outdir]


通过此命令,供应商将重置主模块的vendor目录,以便包括所有软件包需要构建和测试所有主模块的包,但是不包括供应商包的测试代码。

-v标志:使供应商把供应商名下的模块和包的名称打印到标准错误。

-e标志:会导致供应商在出现加载程序包时遇到错误的情况下尝试继续操作。

-o标志:使供应商在给定的路径创建供应商路径而不是“vendor”。注:go命令在模块根目录中只能使用一个命名为“vendor”的vendor目录,因此这个标志主要是为其他语言工具使用的。


五、JSON决定论¶

能够可预测地处理数据格式是至关重要的,也是搜索区块链中保存的数据的重要技术。

(一)技术问题¶

说到底,存储在Fabric网络中的数据的格式由用户自行决定的。最低层级的API只能接收一个字节数组并存储该数组,至于这个数组表示的内容与Fabric网络毫无关系。

一个重要的问题是,在模拟事务时,给定相同的输入,链代码会给出相同的字节数组。否则,背书可能不会完全匹配,从而导致提交交易失败或交易无效。

另一方面,JSON通常用作在账本上存储数据的数据格式。如果使用CouchDB查询的话,则肯定需要使用JSON数据格式。

还需要说明的是,JSON不是一种确定性的数据格式——元素的顺序可以改变,同时仍然在语义上表示相同的数据。因此,这方面存在的一个小小的挑战就是:如何能够确保生成一组一致的JSON?

(二)解决方案¶

办法是:跨多种语言生成一组一致的JSON。

每种语言都会提供不同的特征和库支持,因此,我们可以使用这些功能将对象转换为JSON。跨不同语言确保转换一致性的最佳方法是选择一种规范的方式作为格式化JSON的通用指南。为了获得跨语言的一致散列结果,我们可以按字母顺序来格式化JSON(如前面所提到的那样)。

(1)Go语言¶

在Golang中,encoding/json包用于将Struct对象串行化为json。更具体地说,使用Marshal函数时,后者按排序键顺序封装映射,并按字段声明的顺序保留结构。由于结构是按字段声明顺序封装的,因此在定义新结构时可以遵循按字母顺序排列各字段。

(2)Node.js¶

在Javascript中,将对象序列化为JSON时,通常使用函数JSON.stringify()函数。然而,为了获得一致的结果,需要一个确定版本的JSON.stringify();通过这种方式,可以从字符串化的结果中获得一致的散列结果。json-stringify-deterministic是一个很好的库,可以与sort-keys-recursive结合使用以获得字母顺序。有关json-stringify-deterministic库的使用,可以参考链接https://www.npmjs.com/package/json-stringify-deterministic

(3)Java¶

Java提供了多个库来将对象序列化为JSON字符串。然而,并不是所有这些库都能提供一致性和有序性。例如,Gson库就没有提供任何一致性,因此应避免应用于此类应用程序中。另一方面,Genson库非常适合我们的目的,因为它按字母顺序生成一致的JSON格式。

这只是我们认为有效的众多方法之一。在序列化时,您可以使用各种方法来实现一致性;然而,考虑到Fabric中使用的编程语言的不同特性,字母方法代表了该问题的简单有效的解决方案。总之,如果最适合你的需求,可以随意使用不同的方法。

六、小结

本篇中仅是从宏观角度,结合对于一个基本型资产转移案例的分析,介绍了实现基于Go语言进行链码开发的基本思路,还没有完全覆盖链码开发的全过程。因此,我会在后面文章中更细节地介绍链码开发的问题。

七、参考资料