本Fabric中文文档专栏的阅读前言:前言
文章目录
- 什么是链码?
- [Fabric 合约 API](#Fabric 合约 API)
- 资产转移链码
- 链码访问控制
-
- 客户端身份链码库介绍
- 使用客户端身份链码库简介
-
- [获取客户端 ID](#获取客户端 ID)
- [获取 MSP ID](#获取 MSP ID)
- 获取属性值
- 断言属性值
- [检查特定 OU 值](#检查特定 OU 值)
- [获取客户端的 X509 证书](#获取客户端的 X509 证书)
- 更高效地执行多次操作
- 向身份添加属性
-
- [使用 Fabric CA 管理属性](#使用 Fabric CA 管理属性)
- 证书中的属性格式
- [使用 Go 编写链码时管理外部依赖](#使用 Go 编写链码时管理外部依赖)
- [JSON 确定性](#JSON 确定性)
- 技术问题
- 一个解决方案
什么是链码?
Chaincode 是一个程序,由 Go 、Node.js 或 Java 编写,实现了一个预定的接口。链码运行在与 peer 分离的独立进程中,通过应用程序提交的事务来初始化并管理账本状态。
链码通常处理网络成员达成一致的业务逻辑,因此类似于"智能合约"。 链码可以在提案交易中被调用以更新或查询账本。 在获得适当权限的情况下,链码可以调用同一通道或不同通道中的另一个链码,以访问其状态。 请注意,如果被调用的链码位于与调用链码不同的通道中 ,则只允许执行读取查询。 也就是说,在不同通道上的链码只作为 Query
被调用,不参与后续提交阶段的状态验证检查。
在以下部分,我们将以应用开发者的视角来探索链码。 我们将展示一个资产转移(asset-transfer)链码示例演练,并说明 Fabric Contract API 中每个方法的目的。
本教程提供了 Fabric Contract API 提供的高级 API 的概览。 如要进一步了解如何使用 Fabric Contract API 开发智能合约,请访问 Fabric Contract APIs and Application APIs主题。
Fabric 合约 API
fabric-contract-api
提供了合约接口,是面向应用开发者的高级 API,用于实现智能合约(在 Hyperledger Fabric 中,智能合约也称为 Chaincode)。使用该 API 能提供一个编写业务逻辑的高层入口。
不同语言的 Fabric Contract API 文档可在以下链接找到:
请注意,当使用合约 API 时,每个被调用的链码函数都会获得一个事务上下文 "ctx",从中可以获取链码存根 (GetStub()
),该存根具有访问账本(例如 GetState()
)和更新账本(例如 PutState()
)的功能。
你可以通过以下语言特定链接了解更多信息:
注 :
- shim 提供了最基础的 Chaincode 接口(Init、Invoke、ChaincodeStubInterface),是所有链码与 Fabric peer 通信的核心。换句话说,shim不参与具体业务逻辑,只负责把链码逻辑的执行结果(读写集、返回值、错误信息)打包发送给 peer。
- contractapi 则在内部调用 shim,是对 shim 的高层封装,简化开发帮你做了:方法分发(根据函数名调到对应的合约方法),上下文封装(ctx → stub),错误处理和返回值封装,多合约支持
资产转移链码
我们的应用是一个基本示例链码,用于初始化账本资产、创建资产、读取资产、更新资产、删除资产、检查资产是否存在,以及将资产从一个所有者转移到另一个所有者。
选择代码的存放位置
如果你还未进行过 Go 编程,建议确保你已安装 Go 并正确配置系统环境。我们假设你使用的版本支持模块(modules)。
现在,你需要为你的链码应用创建一个目录。
为了简化操作,我们使用以下命令:
bash
// atcc 是 asset transfer chaincode 的缩写
mkdir atcc && cd atcc
接下来,让我们创建一个模块并生成我们将填写代码的源文件:
bash
go mod init atcc
touch atcc.go
清理准备
首先,让我们做一些准备工作。像所有链码一样,它实现了 fabric-contract-api
接口,因此我们添加必要的 Go 导入语句以引入所需依赖,并定义我们的 SmartContract
。
go
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是一个挑战。下面展示了一种良好的方法,即创建一个按字母顺序排列字段的资产对象结构体。
go
// Asset 描述构成简单资产的基本细节
// 按字母顺序插入结构字段,以在不同语言中实现确定性
// Go 在 marshal 时保留字段顺序,但不会自动排序
type Asset struct {
AppraisedValue int `json:"AppraisedValue"`
Color string `json:"Color"`
ID string `json:"ID"`
Owner string `json:"Owner"`
Size int `json:"Size"`
}
初始化链码
接下来,我们将实现 InitLedger
函数,以使用一些初始数据填充账本。
go
// InitLedger 向账本添加一系列基础资产
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
函数所示。
go
// CreateAsset 使用给定的详细信息向世界状态发布一个新资产。
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)
}
注:你可能已经注意到了s.AssetExists(ctx, id)和ctx.GetStub().GetState(id)这两个函数的作用是一样的,实际上前者是对后者进行了简单的封装,增加了错误处理,
现在我们已经在账本中添加了一些资产并创建了一个资产,让我们编写一个函数 ReadAsset
,用于从账本中读取一个资产。
go
// ReadAsset 返回存储在世界状态中,具有给定 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
,允许我们更新资产的可修改属性。
go
// UpdateAsset 使用提供的参数更新世界状态中的现有资产。
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)
}
// 用新资产覆盖原有资产
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
函数来处理此需求:
go
// DeleteAsset 从世界状态中删除给定资产。
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
的函数来实现该需求。
go
// AssetExists 当具有给定 ID 的资产存在于世界状态中时,返回 true
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
,用于将资产从一个所有者转移给另一个所有者。
go
// TransferAsset 更新具有给定 ID 的资产所有者字段
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
,用于查询账本并返回账本中的所有资产。
go
// GetAllAssets 返回在世界状态中找到的所有资产
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// 使用空字符串作为起始键和结束键进行范围查询,
// 对链码命名空间中的所有资产进行开放式查询。
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
包导入链码包以便于单元测试。若要查看实际示例,请参见 fabric-samples 中的资产转移 Go 链码。如果查看assetTransfer.go
,你会发现它包含package main
并导入定义在smartcontract.go
且位于fabric-samples/asset-transfer-basic/chaincode-go/chaincode/
的chaincode
包。
整合所有功能
最后,我们需要添加 main
函数,它将调用 ContractChaincode.Start
。以下是整个链码程序源代码。
go
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// SmartContract 提供管理资产的函数
type SmartContract struct {
contractapi.Contract
}
// 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 向账本添加一组基础资产
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 向世界状态中发布具有给定详细信息的新资产。
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 返回具有给定 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 使用提供的参数更新世界状态中的现有资产。
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)
}
// 用新资产覆盖原有资产
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 从世界状态中删除给定资产。
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 当具有给定 ID 的资产存在于世界状态中时返回 true
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 更新具有给定 ID 的资产所有者字段
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 返回在世界状态中发现的所有资产
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
// 使用空字符串作为起始和结束键进行范围查询,
// 对链码命名空间中的所有资产进行开放式查询。
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
}
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)
}
}
链码访问控制
链码可以使用客户端(请求提交者)的证书来进行访问控制决策。客户端证书可以通过 ctx.GetStub().GetCreator()
合约 API 获取。
Fabric contract API 还提供扩展 API,可从提交者的证书中提取客户端身份,用于访问控制决策,无论是基于客户端身份本身、组织身份,还是证书中的客户端身份属性(例如 OU 或自定义属性)。
例如 库账本上以键/值形式表示的资产,可以将客户端身份作为值的一部分(例如某个 JSON 属性表示该资产所有者),链码逻辑可以确保只有该资产所有者被授权将来更新该资产的键/值。
客户端身份库扩展 API 可在链码中使用,以检索该提交者信息用于访问控制决策。
注:下文身份链码库部分为链码访问控制章节(即本章)的拓展延伸,注意区分其演示代码与本文主代码,如果暂时不感兴趣可以直接跳转至使用 Go 编写链码时管理外部依赖章节继续阅读。
客户端身份链码库介绍
客户端身份链码库(Client Identity Chaincode Library)使你能够编写基于客户端身份(即链码调用者)做访问控制决策的链码。特别地,你可以根据与客户端关联的以下信息的任意一个或组合来做访问控制决策:
-
客户端身份的 MSP(Membership Service Provider,成员服务提供者)ID
-
与客户端身份关联的属性
-
与客户端身份关联的 OU(组织单位)值
属性只是与身份关联的名称和值对。例如,email=me@gmail.com
表示某个身份具有名为 email
且值为 me@gmail.com
的属性。
使用客户端身份链码库简介
本节描述如何使用客户端身份链码库。
下面所有代码示例假设两点:
-
变量的类型与传入链码的
stub
相同:ChaincodeStubInterface
-
你的链码中已添加如下 import 语句:
go
import "github.com/hyperledger/fabric-chaincode-go/v2/pkg/cid"
获取客户端 ID
下面演示如何获取客户端的 ID,该 ID 在 MSP 内保证唯一:
go
id, err := cid.GetID(stub)
获取 MSP ID
下面演示如何获取客户端身份的 MSP ID:
go
mspid, err := cid.GetMSPID(stub)
获取属性值
下面演示如何获取属性 attr1
的值:
go
val, ok, err := cid.GetAttributeValue(stub, "attr1")
if err != nil {
// 尝试获取属性时出现错误
}
if !ok {
// 客户端身份不具备该属性
}
// 对 'val' 的值执行操作
断言属性值
通常,你希望基于属性值做访问控制决策,即断言某属性的值。例如,下面示例中,如果客户端没有 myapp.admin
属性值为 true
,将返回错误:
go
err := cid.AssertAttributeValue(stub, "myapp.admin", "true")
if err != nil {
// 返回错误
}
这实际上是使用属性来实现基于角色的访问控制(RBAC)。
检查特定 OU 值
go
found, err := cid.HasOUValue(stub, "myapp.admin")
if err != nil {
// 返回错误
}
if !found {
// 客户端身份不属于该组织单位
// 返回错误
}
获取客户端的 X509 证书
下面演示如何获取客户端的 X509 证书,如果客户端身份不是基于 X509 证书,则返回 nil:
go
cert, err := cid.GetX509Certificate(stub)
注意,如果身份不是使用 X509 证书,则 cert
和 err
都可能为 nil。
更高效地执行多次操作
有时,你可能需要执行多次操作才能做出访问决策。例如,下面演示如何为 MSP 为 org1MSP
且具有 attr1
属性,或 MSP 为 org2MSP
且具有 attr2
属性的身份授予访问权限:
go
// 获取 Client ID 对象
id, err := cid.New(stub)
if err != nil {
// 处理错误
}
mspid, err := id.GetMSPID()
if err != nil {
// 处理错误
}
switch mspid {
case "org1MSP":
err = id.AssertAttributeValue("attr1", "true")
case "org2MSP":
err = id.AssertAttributeValue("attr2", "true")
default:
err = errors.New("Wrong MSP")
}
虽然不是必须的,但如果需要执行多次操作,先获取 Client ID 对象更高效,如上所示。
注:为什么Fabric底层已经有丰富的权限管理机制这里还需要开发者进行访问控制呢?
- 因为Fabric 的底层访问控制只能判断某个 MSP 是否有权调用链码,但不能区分链码内不同功能的调用权限 。
例如,你有一个链码管理公司资产,MSP org1 的所有用户可以调用链码,但你希望只有 HR 部门可以修改工资,只有财务部门可以审批报销,这就需要在链码里判断用户身份或属性。- Fabric 核心只知道 MSP 成员身份,但链码可以通过 客户端身份库(cid) 读取证书里的属性、OU 或自定义字段,实现基于角色或属性的访问控制(RBAC)。
向身份添加属性
本节描述如何在使用 Hyperledger Fabric CA 以及外部 CA 时向证书添加自定义属性。
使用 Fabric CA 管理属性
有两种方法可以通过 fabric-ca
向注册证书(enrollment certificate)添加属性:
- 注册身份时,可以指定默认情况下注册证书应包含某个属性。此行为在注册时可以覆盖,但适用于建立默认行为,并且假设注册发生在应用之外,不需要任何应用更改。
下面示例展示如何注册 user1
并添加两个属性:app1Admin
和 email
。:ecert
后缀表示 appAdmin
属性默认添加到 user1
的注册证书中,而 email
属性默认不会添加:
bash
fabric-ca-client register --id.name user1 --id.secret user1pw --id.type user --id.affiliation org1 --id.attrs 'app1Admin=true:ecert,email=user1@gmail.com'
- 在注册身份时,可以请求向证书添加一个或多个属性。对于每个请求的属性,可以指定是否为可选。如果不是可选属性,但身份不具备该属性,则注册失败。
下面示例展示如何注册 user1
并添加 email
属性,不添加 app1Admin
,并可选添加 phone
属性(如果用户拥有该属性):
bash
fabric-ca-client enroll -u http://user1:user1pw@localhost:7054 --enrollment.attrs "email,phone:opt"
证书中的属性格式
属性存储在 X509 证书的扩展中,使用 ASN.1 OID(对象标识符)1.2.3.4.5.6.7.8.1
。扩展的值是如下形式的 JSON 字符串:
json
{"attrs":{<attrName>:<attrValue>}}
下面是包含属性 attr1
值为 val1
的证书示例(注意 X509v3 扩展部分最后一条):
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
1e:49:98:e9:f4:4f:d0:03:53:bf:36:81:c0:a0:a4:31:96:4f:52:75
Signature Algorithm: ecdsa-with-SHA256
Issuer: CN=fabric-ca-server
Validity
Not Before: Sep 8 03:42:00 2017 GMT
Not After : Sep 8 03:42:00 2018 GMT
Subject: CN=MyTestUserWithAttrs
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
EC Public Key:
pub:
04:e6:07:5a:f7:09:d5:af:38:e3:f7:a2:90:77:0e:
32:67:5b:70:a7:37:ca:b5:c9:d8:91:77:39:ae:03:
a0:36:ad:72:b3:3c:89:6d:1e:f6:1b:6d:2a:88:49:
92:6e:6e:cc:bc:81:52:fa:19:88:18:5c:d7:6e:eb:
d4:73:cc:51:79
ASN1 OID: prime256v1
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
D8:28:B4:C0:BC:92:4A:D3:C3:8C:54:6C:08:86:33:10:A6:8D:83:AE
X509v3 Authority Key Identifier:
keyid:C4:B3:FE:76:0D:E2:DE:3C:FC:75:FB:AE:55:86:04:F0:BB:7F:F6:01
X509v3 Subject Alternative Name:
DNS:Anils-MacBook-Pro.local
1.2.3.4.5.6.7.8.1:
{"attrs":{"attr1":"val1"}}
Signature Algorithm: ecdsa-with-SHA256
30:45:02:21:00:fb:84:a9:65:29:b2:f4:d3:bc:1a:8b:47:92:
5e:41:27:2d:26:ec:f7:cd:aa:86:46:a4:ac:da:25:be:40:1d:
c5:02:20:08:3f:49:86:58:a7:20:48:64:4c:30:1b:da:a9:a2:
f2:b4:16:28:f6:fd:e1:46:dd:6b:f2:3f:2f:37:4a:4c:72
如果你想像前面描述的那样使用客户端身份库提取或断言属性值,但没有使用 Hyperledger Fabric CA,则必须确保外部 CA 签发的证书包含上述格式的属性。特别是,证书必须包含 X509v3 扩展,其中的 JSON 值包含身份的属性名称和值。
如果你的外部 CA 不支持未知 OID 的扩展,则可以使用证书主题 DN 的 OU 字段作为属性的替代,并在链码中使用 cid
库的 HasOUValue
函数进行访问控制。
欲了解更多详细信息,请参考 client identity library extension APIs。
使用 Go 编写链码时管理外部依赖
你的 Go 链码依赖于不属于 Go 标准库的 Go 包(例如链码 shim)。这些包必须包含在安装到 peer 的链码包中。
如果你的链码已按模块组织,最简单的方式是在打包链码前使用 go mod vendor
将依赖打包到本地。
bash
go mod tidy
go mod vendor
这样会将外部依赖放入本地 vendor
目录中。
一旦依赖被 vendored 到链码目录中,使用 peer chaincode package
和 peer chaincode install
操作时,这些依赖的代码也会被包含到链码包中。
JSON 确定性
能够可预测地处理数据格式是至关重要的,同时也需要能够搜索区块链中保存的数据。
技术问题
在 Fabric 中存储的数据格式由用户自行决定。最低层的 API 接受一个字节数组并存储它 ------ 这个字节数组具体代表什么并不是 Fabric 关心的事情。重要的是,在模拟交易时,给定相同的输入,链码必须返回相同的字节数组。否则,各节点背书结果可能不一致,交易将无法提交或会被判定为无效。
JSON 通常被用作在账本中存储数据的格式,并且在使用 CouchDB 查询时是必须的。
然而,JSON 并不是一个确定性的数据格式 ------ 元素的顺序可以发生变化,但语义上依然表示相同的数据。因此,挑战在于如何生成一致的 JSON 集合。
一个解决方案
在多种语言之间生成一致的 JSON 集合。每种语言都有不同的特性和库可以用来将对象转换为 JSON。为了在不同语言之间实现确定性,最好的方法是选择一种规范化的方式作为通用准则来格式化 JSON。为了在不同语言之间得到一致的哈希值,可以按照字母顺序对 JSON 进行格式化。
Golang
在 Golang 中,encoding/json
包被用来将结构体对象序列化为 JSON。更具体来说,Marshal
函数会对 map 按照键的排序顺序进行编组,并保持结构体字段的声明顺序。由于结构体是按照字段声明顺序进行编组的,因此在定义新的结构时应遵循字母顺序。
Node.js
在 JavaScript 中,将对象序列化为 JSON 时通常使用 JSON.stringify()
函数。然而,为了获得一致的结果,需要使用确定性的 JSON.stringify()
版本;通过这种方式,可以从字符串化的结果中得到一致的哈希值。json-stringify-deterministic
是一个很好的库,可以与 sort-keys-recursive
结合使用,以实现字母顺序。这里有一个更深入的教程。
Java
Java 提供了多个库来将对象序列化为 JSON 字符串。然而,并不是所有库都能保证一致性和顺序。例如,Gson
库就不能提供一致性,因此在这种场景下应避免使用。另一方面,Genson
库非常适合我们的目的,因为它能够按字母顺序生成一致的 JSON。
你可以在 asset-transfer-basic 链码中找到这种实践的一个很好的示例。
注意:这只是我们认为可能有效的众多方法之一。在序列化时,你可以采用多种方式来实现一致性;然而,考虑到 Fabric 使用的编程语言的不同特性,字母顺序的方法代表了一种简单而高效的解决方案。总之,如果其他方法更适合你的需求,完全可以使用不同的方法。