Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例

前言

在上个实验 Hyperledger Fabric 多组织多排序节点部署在多个主机上 中,我们已经实现了多组织多排序节点部署在多个主机上,但到目前为止,我们所有的实验都只是研究了联盟链的网络配置方法(尽管这确实是重难点),而没有考虑具体的应用开发。本文将在前面实验的基础上,首先尝试使用 Go 语言开发了一个工作室联盟链的项目信息智能合约,并成功将其部署至联盟链上;然后依据官方示例,使用 fabric-gateway 模块实现了一个能够管理项目信息智能合约的客户端;之后对比了 fabric-gateway 模块和 fabric-sdk-* 模块各自的优缺点,分析官方示例源码实现了通过 fabric-sdk-* 模块管理整个联盟链网络。一般语境下,本文默认智能合约等于链码。

工作准备

本文工作

以三组织三排序节点的方式启动 Hyperledger Fabric 网络,实验共包含四个组织—— council 、 soft 、 web 、 hard , 其中 council 组织为网络提供 TLS-CA 服务,并且运行维护着三个 orderer 服务;其余每个组织都运行维护着一个 peer 节点、一个 admin 用户和一个 user 用户。网络结构为(实验代码已上传至:https://github.com/wefantasy/FabricLearn6_ContractGatewayAndSDK 下):

运行端口 说明
council.ifantasy.net 7050 council 组织的 CA 服务, 为联盟链网络提供 TLS-CA 服务
orderer1.council.ifantasy.net 7051 council 组织的 orderer1 服务
orderer1.council.ifantasy.net 7052 council 组织的 orderer1 服务的 admin 服务
orderer2.council.ifantasy.net 7054 council 组织的 orderer2 服务
orderer2.council.ifantasy.net 7055 council 组织的 orderer2 服务的 admin 服务
orderer3.council.ifantasy.net 7057 council 组织的 orderer3 服务
orderer3.council.ifantasy.net 7058 council 组织的 orderer3 服务的 admin 服务
soft.ifantasy.net 7250 soft 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.soft.ifantasy.net 7251 soft 组织的 peer1 成员节点
web.ifantasy.net 7350 web 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.web.ifantasy.net 7351 web 组织的 peer1 成员节点
hard.ifantasy.net 7450 hard 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.hard.ifantasy.net 7451 hard 组织的 peer1 成员节点

实验准备

本文网络结构直接将 Hyperledger Fabric无排序组织以Raft协议启动多个Orderer服务、TLS组织运行维护Orderer服务 中创建的 4-2_RunOrdererByCouncil 复制为 6_ContractGatewayAndSDK 并修改(建议直接将本案例仓库 FabricLearn 下的 6_ContractGatewayAndSDK 目录拷贝到本地运行),文中大部分命令在 Hyperledger Fabric定制联盟链网络工程实践 中已有介绍因此不会详细说明。默认情况下,所有命令皆在 6_ContractGatewayAndSDK 根目录下执行,在开始后面的实验前按照以下命令启动基础实验网络:

  1. 设置DNS(如果未设置): ./setDNS.sh
  2. 设置环境变量: source envpeer1soft
  3. 启动CA网络: ./0_Restart.sh

本实验初始 docker 网络为:
Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例

基础环境

注册用户

直接运行根目录下的 1_RegisterUser.sh 即可完成本实验所需用户的注册。以往我们每个组织只有一个 peer 节点和一个 admin 节点,但这些节点都不适合为客户端所用,因此基础环境的改变主要包含了为每个组织新增一个 client 类型的用户。以 soft 组织为例,其注册用户命令为:

echo "Working on soft" export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/ca/crypto/ca-cert.pem export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/ca/admin fabric-ca-client enroll -d -u https://ca-admin:ca-adminpw@soft.ifantasy.net:7250 # client 类型用户注册 fabric-ca-client register -d --id.name user1 --id.secret user1 --id.type client -u https://soft.ifantasy.net:7250 fabric-ca-client register -d --id.name peer1 --id.secret peer1 --id.type peer -u https://soft.ifantasy.net:7250 fabric-ca-client register -d --id.name admin1 --id.secret admin1 --id.type admin -u https://soft.ifantasy.net:7250 

组织证书构建

直接运行根目录下的 2_EnrollUser.sh 即可完成本实验所需证书的构建,每个组织主要增加了 client 类型用户的证书构建每个注册用户单元配置文件 config.yaml ,以 soft 组织为例,其生成组织证书的命令为:

echo "Start Soft=============================" # 新增 echo "Enroll User1" export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/user1 export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem export FABRIC_CA_CLIENT_MSPDIR=msp fabric-ca-client enroll -d -u https://user1:user1@soft.ifantasy.net:7250  echo "Enroll Admin1" export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1 export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem export FABRIC_CA_CLIENT_MSPDIR=msp fabric-ca-client enroll -d -u https://admin1:admin1@soft.ifantasy.net:7250 mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincerts cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincerts/cert.pem  echo "Enroll Peer1" export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1 export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem export FABRIC_CA_CLIENT_MSPDIR=msp fabric-ca-client enroll -d -u https://peer1:peer1@soft.ifantasy.net:7250 # for TLS export FABRIC_CA_CLIENT_MSPDIR=tls-msp export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/tls-ca-cert.pem fabric-ca-client enroll -d -u https://peer1soft:peer1soft@council.ifantasy.net:7050 --enrollment.profile tls --csr.hosts peer1.soft.ifantasy.net cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/*_sk $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/key.pem mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincerts cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincerts/cert.pem  mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/admincerts mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/cacerts mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/tlscacerts mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/users cp $LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/cacerts/ cp $LOCAL_CA_PATH/soft.ifantasy.net/assets/tls-ca-cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/tlscacerts/ cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/admincerts/cert.pem  cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/msp/config.yaml # 新增 cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/user1/msp/config.yaml cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/config.yaml cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/config.yaml echo "End Soft=============================" 

为了配合使用每个用户的单元配置文件,需要将所有用户 msp 目录下的 cacerts/council-ifantasy-net-7050.pem 文件名修改为 cacerts/ca-cert.pem ,因此在 2_EnrollUser.sh 的末尾追加一行批量修改文件名的命令来实现此目的:

# 按正则匹配并批量修改符合要求的文件 find orgs/ -regex ".+cacerts.+.pem" -not -regex ".+tlscacerts.+" | rename 's/cacerts/.+.pem/cacerts/ca-cert.pem/' 

配置通道

直接运行根目录下的 3_Configtxgen.sh 即可完成本实验所需通道配置,需要注意的是,为了使通道组织架构更加清晰,将通道配置文件 configtx.yaml 中各组织名称从 orgnameMSP 改为了 orgname ,以 soft 组织为例,其组织通道配置如下:

- &soft     Name: softMSP     ID: softMSP     MSPDir: ../orgs/soft.ifantasy.net/msp     Policies:         Readers:             Type: Signature             Rule: "OR('softMSP.admin', 'softMSP.peer', 'softMSP.client')"         Writers:             Type: Signature             Rule: "OR('softMSP.admin', 'softMSP.client')"         Admins:             Type: Signature             Rule: "OR('softMSP.admin')"         Endorsement:             Type: Signature             Rule: "OR('softMSP.peer')"     AnchorPeers:         - Host: peer1.soft.ifantasy.net             Port: 7251 

智能合约开发

本节将参考官方示例智能合约 asset-transfer-basic 开发工作室联盟链的 项目资源管理智能合约 ,其在官方示例的基础上进行了依赖和结构上的简化。本示例是基于 Go 语言的智能合约,因此建议先学习 Go 语言基础概念和规范,不然自行定制可能会有一些 Bug 。

合约代码

  1. 初始化目录/文件
    在实验根目录 6_ContractGatewayAndSDK 下创建目录 contract 作为智能合约根目录,并在其下创建智能合约文件 project_contract.go ,后续代码皆在 project_contract.go 中。
  2. 智能合约结构体
    type ProjectContract struct {     contractapi.Contract } 

    智能合约结构体一般是固定写法,创建任意一个结构体然后继承 contractapi.Contract 即可,当部署至链上后利用其继承的 contractapi.Contract 的接口实现对合约操作。

  3. 项目信息结构体
    type Project struct {     ID           string `json:"ID"`             // 项目唯一ID     Name         string `json:"Name"`           // 项目名称     Developer    string `json:"Developer"`      // 项目主要负责人     Organization string `json:"Organization"`   // 项目所属组织     Category     string `json:"Category"`       // 项目所属类别      Url          string `json:"Url"`            // 项目介绍地址     Describes    string `json:"Describes"`      // 项目描述 } 

    项目信息结构体主要定义了单个项目的基本信息,类似于 Java 的 Entity 类、数据库的单个表。

  4. 初始化智能合约数据
    func (s *ProjectContract) InitLedger(ctx contractapi.TransactionContextInterface) error {     projects := []Project{         {ID: "FA8B31A55CD59DB352BCBF4D2AE791AD", Name: "工作室联盟链管理系统", Developer: "Fantasy", Organization: "Web", Category: "blockchain", Url: "https://github.com/wefantasy/FabricLearn", Describes: "本项目虚拟了一个工作室联盟链需求并将逐步实现,致力于提供一个易理解、可复现的Fabric学习项目,其中项目部署步骤的各个环节都清晰可见,并且将所有实验打包为脚本使之能够被快速复现在任何一台主机上"},     }     for _, project := range projects {         projectJSON, err := json.Marshal(project)         if err != nil {             return err         }         err = ctx.GetStub().PutState(project.ID, projectJSON)         if err != nil {             return fmt.Errorf("failed to put to world state. %v", err)         }     }     return nil } 

    在 Fabric 某个旧版本之前必须提供智能合约初始化函数,但在本实验所用的 Fabric 2.4 则是可选项,在此仅仅是为了写入预设实验数据。Fabric 底层使用默认键值对(key-value)状态数据库 LevelDB 储存数据,在操作体验上十分像 redis 数据库。

  5. 判断项目信息是否已存在
    func (s *ProjectContract) ProjectExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {     projectJSON, err := ctx.GetStub().GetState(id)     if err != nil {         return false, fmt.Errorf("failed to read from world state: %v", err)     }      return projectJSON != nil, nil } 
  6. 写入新项目信息
    func (s *ProjectContract) CreateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {     exists, err := s.ProjectExists(ctx, id)     if err != nil {         return err     }     if exists {         return fmt.Errorf("the project %s already exists", id)     }     project := Project{         ID:           id,         Name:         name,         Developer:    developer,         Organization: organization,         Category:     category,         Url:          url,         Describes:    describes,     }     projectJSON, err := json.Marshal(project)     if err != nil {         return err     }     return ctx.GetStub().PutState(id, projectJSON) } 
  7. 删除指定项目信息
    func (s *ProjectContract) DeleteProject(ctx contractapi.TransactionContextInterface, id string) error {     exists, err := s.ProjectExists(ctx, id)     if err != nil {         return err     }     if !exists {         return fmt.Errorf("the project %s does not exist", id)     }      return ctx.GetStub().DelState(id) } 

    Fabric 联盟链作为区块链的一种特殊形式,同样具有可追溯特性,因此任何对数据的增删改操作都是软操作——留下操作记录。

  8. 修改项目信息
    func (s *ProjectContract) UpdateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {     exists, err := s.ProjectExists(ctx, id)     if err != nil {         return err     }     if !exists {         return fmt.Errorf("the project %s does not exist", id)     }     project := Project{         ID:           id,         Name:         name,         Developer:    developer,         Organization: organization,         Category:     category,         Url:          url,         Describes:    describes,     }     projectJSON, err := json.Marshal(project)     if err != nil {         return err     }     return ctx.GetStub().PutState(id, projectJSON) } 
  9. 查询项目信息
    func (s *ProjectContract) ReadProject(ctx contractapi.TransactionContextInterface, id string) (*Project, error) {     projectJSON, err := ctx.GetStub().GetState(id)     if err != nil {         return nil, fmt.Errorf("failed to read from world state: %v", err)     }     if projectJSON == nil {         return nil, fmt.Errorf("the project %s does not exist", id)     }      var project Project     err = json.Unmarshal(projectJSON, &project)     if err != nil {         return nil, err     }      return &project, nil } 
  10. 查询链上所有项目信息
    func (s *ProjectContract) GetAllProjects(ctx contractapi.TransactionContextInterface) ([]*Project, error) {     // GetStateByRange 查询参数为两个空字符串时即查询所有数据     resultsIterator, err := ctx.GetStub().GetStateByRange("", "")     if err != nil {         return nil, err     }     defer resultsIterator.Close()      var projects []*Project     for resultsIterator.HasNext() {         queryResponse, err := resultsIterator.Next()         if err != nil {             return nil, err         }          var project Project         err = json.Unmarshal(queryResponse.Value, &project)         if err != nil {             return nil, err         }         projects = append(projects, &project)     }      return projects, nil } 
  11. 智能合约入口函数/主函数
    func main() {     chaincode, err := contractapi.NewChaincode(&ProjectContract{})     if err != nil {         log.Panicf("Error creating project-manage chaincode: %v", err)     }      if err := chaincode.Start(); err != nil {         log.Panicf("Error starting project-manage chaincode: %v", err)     } } 

至此,项目信息管理智能合约核心代码以编写完毕,完整 project_contract.go 文件内容如下(需要注意的是合约入口必须属于 main 包):

package main  import ( 	"encoding/json" 	"fmt" 	"github.com/hyperledger/fabric-contract-api-go/contractapi" 	"log" )  type ProjectContract struct { 	contractapi.Contract }  type Project struct { 	ID           string `json:"ID"`             // 项目唯一ID 	Name         string `json:"Name"`           // 项目名称 	Developer    string `json:"Developer"`      // 项目主要负责人 	Organization string `json:"Organization"`   // 项目所属组织 	Category     string `json:"Category"`       // 项目所属类别  	Url          string `json:"Url"`            // 项目介绍地址 	Describes    string `json:"Describes"`      // 项目描述 }  // 初始化智能合约数据 func (s *ProjectContract) InitLedger(ctx contractapi.TransactionContextInterface) error { 	projects := []Project{ 		{ID: "FA8B31A55CD59DB352BCBF4D2AE791AD", Name: "工作室联盟链管理系统", Developer: "Fantasy", Organization: "Web", Category: "blockchain", Url: "https://github.com/wefantasy/FabricLearn", Describes: "本项目虚拟了一个工作室联盟链需求并将逐步实现,致力于提供一个易理解、可复现的Fabric学习项目,其中项目部署步骤的各个环节都清晰可见,并且将所有实验打包为脚本使之能够被快速复现在任何一台主机上"}, 	} 	for _, project := range projects { 		projectJSON, err := json.Marshal(project) 		if err != nil { 			return err 		} 		err = ctx.GetStub().PutState(project.ID, projectJSON) 		if err != nil { 			return fmt.Errorf("failed to put to world state. %v", err) 		} 	} 	return nil }  // 写入新项目 func (s *ProjectContract) CreateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error { 	exists, err := s.ProjectExists(ctx, id) 	if err != nil { 		return err 	} 	if exists { 		return fmt.Errorf("the project %s already exists", id) 	}  	project := Project{ 		ID:           id, 		Name:         name, 		Developer:    developer, 		Organization: organization, 		Category:     category, 		Url:          url, 		Describes:    describes, 	} 	projectJSON, err := json.Marshal(project) 	if err != nil { 		return err 	} 	return ctx.GetStub().PutState(id, projectJSON) }  // 读取指定ID的项目信息 func (s *ProjectContract) ReadProject(ctx contractapi.TransactionContextInterface, id string) (*Project, error) { 	projectJSON, err := ctx.GetStub().GetState(id) 	if err != nil { 		return nil, fmt.Errorf("failed to read from world state: %v", err) 	} 	if projectJSON == nil { 		return nil, fmt.Errorf("the project %s does not exist", id) 	}  	var project Project 	err = json.Unmarshal(projectJSON, &project) 	if err != nil { 		return nil, err 	}  	return &project, nil }  // 更新项目信息. func (s *ProjectContract) UpdateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error { 	exists, err := s.ProjectExists(ctx, id) 	if err != nil { 		return err 	} 	if !exists { 		return fmt.Errorf("the project %s does not exist", id) 	}  	project := Project{ 		ID:           id, 		Name:         name, 		Developer:    developer, 		Organization: organization, 		Category:     category, 		Url:          url, 		Describes:    describes, 	} 	projectJSON, err := json.Marshal(project) 	if err != nil { 		return err 	}  	return ctx.GetStub().PutState(id, projectJSON) }  // 删除指定ID的项目信息 func (s *ProjectContract) DeleteProject(ctx contractapi.TransactionContextInterface, id string) error { 	exists, err := s.ProjectExists(ctx, id) 	if err != nil { 		return err 	} 	if !exists { 		return fmt.Errorf("the project %s does not exist", id) 	}  	return ctx.GetStub().DelState(id) }  // 判断某项目是否存在 func (s *ProjectContract) ProjectExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) { 	projectJSON, err := ctx.GetStub().GetState(id) 	if err != nil { 		return false, fmt.Errorf("failed to read from world state: %v", err) 	}  	return projectJSON != nil, nil }  // 读取所有项目信息 func (s *ProjectContract) GetAllProjects(ctx contractapi.TransactionContextInterface) ([]*Project, error) { 	// GetStateByRange 查询参数为两个空字符串时即查询所有数据 	resultsIterator, err := ctx.GetStub().GetStateByRange("", "") 	if err != nil { 		return nil, err 	} 	defer resultsIterator.Close()  	var projects []*Project 	for resultsIterator.HasNext() { 		queryResponse, err := resultsIterator.Next() 		if err != nil { 			return nil, err 		}  		var project Project 		err = json.Unmarshal(queryResponse.Value, &project) 		if err != nil { 			return nil, err 		} 		projects = append(projects, &project) 	}  	return projects, nil }  func main() { 	chaincode, err := contractapi.NewChaincode(&ProjectContract{}) 	if err != nil { 		log.Panicf("Error creating project-manage chaincode: %v", err) 	}  	if err := chaincode.Start(); err != nil { 		log.Panicf("Error starting project-manage chaincode: %v", err) 	} } 

依赖下载

合约代码编写完成后并不能直接部署到联盟链上,需要将合约中 import 导入的包下载到本地以供后面一起打包,本小节所有命令默认运行于 6_ContractGatewayAndSDK/contract 下。

  1. 初始化模块
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract 
  2. 将所有依赖下载到本地
    go mod vendor 

以上命令运行成功后,智能合约开发工作基本结束,此时 contract 目录结构如下:

6_ContractGatewayAndSDK/contract ├── go.mod ├── go.sum ├── project_contract.go └── vendor     ├── github.com     ├── golang.org     ├── google.golang.org     ├── gopkg.in     └── modules.tx 

合约部署测试

如无特殊说明,以下命令默认运行于实验根目录 6_ContractGatewayAndSDK 下:

  1. 合约打包
     source envpeer1soft  peer lifecycle chaincode package basic.tar.gz --path contract --lang golang --label basic_1 
  2. 三组织安装
     source envpeer1soft  peer lifecycle chaincode install basic.tar.gz  peer lifecycle chaincode queryinstalled  source envpeer1web  peer lifecycle chaincode install basic.tar.gz  peer lifecycle chaincode queryinstalled  source envpeer1hard  peer lifecycle chaincode install basic.tar.gz  peer lifecycle chaincode queryinstalled 
  3. 三组织批准
     export CHAINCODE_ID=basic_1:0f1f1ffc8e3865a9179e70a3c56237482b3eb4dcecd30ab51ab01a6f5d3daeff  source envpeer1soft  peer lifecycle chaincode approveformyorg -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA  --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID  peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1  source envpeer1web  peer lifecycle chaincode approveformyorg -o orderer3.council.ifantasy.net:7057 --tls --cafile $ORDERER_CA  --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID  peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1  source envpeer1hard  peer lifecycle chaincode approveformyorg -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA  --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID  peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1 

    注意要将 CHAINCODE_ID 的值改为三组织安装时输出的连码包 ID

  4. 提交并测试
     source envpeer1soft  peer lifecycle chaincode commit -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --init-required --version 1.0 --sequence 1 --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE  peer chaincode invoke --isInit -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["InitLedger"]}'  sleep 5  peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["GetAllProjects"]}' 

    Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例

fabric-gateway 客户端示例

客户端代码

  1. 初始化目录/文件
    在实验根目录 6_ContractGatewayAndSDK 下创建目录 contract-gateway 作为 fabric-gateway 客户端的根目录,并在其下创建联盟链网络连接文件 connect.go 和 客户端主程序 app.go 。实验最终目录结构为:
    contract-gateway ├── app.go ├── connect.go ├── go.mod └── go.sum 
  2. connect.go 写入以下内容
    package main  import (     "crypto/x509"     "fmt"     "io/ioutil"     "path"     "github.com/hyperledger/fabric-gateway/pkg/identity"     "google.golang.org/grpc"     "google.golang.org/grpc/credentials" )  const (     mspID         = "softMSP"				// 所属组织的MSPID     cryptoPath    = "/root/FabricLearn/6_ContractGatewayAndSDK/orgs/soft.ifantasy.net"	// 中间变量     certPath      = cryptoPath + "/registers/user1/msp/signcerts/cert.pem"		// client 用户的签名证书     keyPath       = cryptoPath + "/registers/user1/msp/keystore/"		// client 用户的私钥路径     tlsCertPath   = cryptoPath + "/assets/tls-ca-cert.pem"			// client 用户的 tls 通信证书     peerEndpoint  = "peer1.soft.ifantasy.net:7251"			// 所连 peer 节点的地址     gatewayPeer   = "peer1.soft.ifantasy.net"		// 网关 peer 节点名称 )  // 创建指向联盟链网络的 gRPC 连接. func newGrpcConnection() *grpc.ClientConn {     certificate, err := loadCertificate(tlsCertPath)     if err != nil {         panic(err)     }      certPool := x509.NewCertPool()     certPool.AddCert(certificate)     transportCredentials := credentials.NewClientTLSFromCert(certPool, gatewayPeer)      connection, err := grpc.Dial(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))     if err != nil {         panic(fmt.Errorf("failed to create gRPC connection: %w", err))     }      return connection }  // 根据用户指定的X.509证书为这个网关连接创建一个客户端标识。 func newIdentity() *identity.X509Identity {     certificate, err := loadCertificate(certPath)     if err != nil {         panic(err)     }      id, err := identity.NewX509Identity(mspID, certificate)     if err != nil {         panic(err)     }     return id }  // 加载证书文件 func loadCertificate(filename string) (*x509.Certificate, error) {     certificatePEM, err := ioutil.ReadFile(filename)     if err != nil {         return nil, fmt.Errorf("failed to read certificate file: %w", err)     }     return identity.CertificateFromPEM(certificatePEM) }  // 使用私钥从消息摘要生成数字签名 func newSign() identity.Sign {     files, err := ioutil.ReadDir(keyPath)     if err != nil {         panic(fmt.Errorf("failed to read private key directory: %w", err))     }     privateKeyPEM, err := ioutil.ReadFile(path.Join(keyPath, files[0].Name()))      if err != nil {         panic(fmt.Errorf("failed to read private key file: %w", err))     }      privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM)     if err != nil {         panic(err)     }      sign, err := identity.NewPrivateKeySign(privateKey)     if err != nil {         panic(err)     }      return sign } 

    值得说明的是,不论是 gateway 客户端还是 fabric-sdk 客户端,一般都可以通过 client 、 admin 类型的用户连接联盟链网络,只是创建单独的 client 类型的专用用户连接网络更符合开发理念。

  3. app.go 写入以下内容
    package main  import (     "bytes"     "encoding/json"     "fmt"     "time"     "github.com/hyperledger/fabric-gateway/pkg/client" )  const (     channelName   = "testchannel"	// 连接的通道     chaincodeName = "basic"			// 连接的链码 )  func main() {     clientConnection := newGrpcConnection()     defer clientConnection.Close()      id := newIdentity()     sign := newSign()      gateway, err := client.Connect(         id,         client.WithSign(sign),         client.WithClientConnection(clientConnection),         client.WithEvaluateTimeout(5*time.Second),         client.WithEndorseTimeout(15*time.Second),         client.WithSubmitTimeout(5*time.Second),         client.WithCommitStatusTimeout(1*time.Minute),     )     if err != nil {         panic(err)     }     defer gateway.Close()      network := gateway.GetNetwork(channelName)     contract := network.GetContract(chaincodeName)      fmt.Println("getAllAssets:")     getAllAssets(contract) } func getAllAssets(contract *client.Contract) {     fmt.Println("Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger")      evaluateResult, err := contract.EvaluateTransaction("GetAllProjects")     if err != nil {         panic(fmt.Errorf("failed to evaluate transaction: %w", err))     }     result := formatJSON(evaluateResult)      fmt.Printf("*** Result:%sn", result) }  func formatJSON(data []byte) string {     var prettyJSON bytes.Buffer     if err := json.Indent(&prettyJSON, data, " ", ""); err != nil {         panic(fmt.Errorf("failed to parse JSON: %w", err))     }     return prettyJSON.String() } 

客户端演示

如无特殊说明,以下命令默认运行于实验根目录 contract-gateway 下:

  1. 初始化模块
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway 
  2. 下载依赖
    go get 

    此时实验目录结构为

  3. 运行客户端
    go run . 

    因为本目录下同时有两个 packagemain 的 go 文件,所以要用 . 的方式运行,运行结果如下:
    Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例

fabric-sdk-go 客户端示例

刚接触 Fabric 你可能会很疑惑,有些案例使用 fabric-gateway 连接联盟链、另一些案例通过 fabric-sdk-* 连接联盟链,并且似乎都可以操纵网络,那么有什么区别呢? fabric-sdk-* 被定义为 Fabric 的低级 SDK ,主要为开发者提供账本管理、通道管理、用户管理等联盟链管理的 API ,它的开发成本更高但功能丰富;而 fabric-gateway 被定义为 Fabric 的高级 SDK ,这里的高级主要体现在其抽象程度更高,主要为开发者提供账本管理的 API ,它的开发成本更低但功能较少。因此建议优先学习 fabric-sdk-* 的使用。

连接配置文件

就像刚才说的, fabric-sdk-* 开发成本比较高,我觉得高出来的开发成本有一半都在连接配置文件的配置上,它让我花费了至少半天的时间来排错,而网上几乎没有能把连接配置文件讲清楚的文章(也许是我没有找到),只能通过官方示例代码慢慢推导出正确的配置方法。
从 fabric-sdk-* 官方示例 assetTransfer.go 中引用的 connection-org1.yaml 连接配置文件出发,可以定位到生成它的相关文件为 ccp-generate.shccp-template.yaml ,后者为连接配置文件的基准模板,前者使用 bash 命令将基准模板替换为具体连接配置文件。连接配置文件有 json 和 yaml 两种格式,我觉得 yaml 语法更为简洁,后续实验以此为例。将 ccp-generate.sh 文件中的函数展开后,可以很容易的得生成连接配置文件的过程,本节所有命令默认运行于 6_ContractGatewayAndSDK 目录下,通过如下命令生成 soft 组织的连接配置文件:

  1. 创建模板文件
    将官方模板 ccp-template.yaml 复制一份至我们项目的 6_ContractGatewayAndSDK/config/ccp-template.yaml 中,由于我们的命名规范与官方不同,且该模板通用性不高,因此将其内容改为如下:
    --- name: test-network-${ORG} version: 1.0.0 client: organization: ${ORG} connection:     timeout:     peer:         endorser: '300' organizations: ${ORG}:     mspid: ${ORG}MSP     peers:     - peer1.${ORG}.ifantasy.net     certificateAuthorities:     - ${ORG}.ifantasy.net peers: peer1.${ORG}.ifantasy.net:     url: grpcs://peer1.${ORG}.ifantasy.net:${P0PORT}     tlsCACerts:     pem: |         ${PEERPEM}     grpcOptions:     ssl-target-name-override: peer1.${ORG}.ifantasy.net     hostnameOverride: peer1.${ORG}.ifantasy.net certificateAuthorities: ${ORG}.ifantasy.net:     url: https://${ORG}.ifantasy.net:${CAPORT}     caName: ${ORG}.ifantasy.net     tlsCACerts:     pem:          - |         ${CAPEM}     httpOptions:     verify: false 

    这个模板可以跟我们项目很好的契合,需要特别注意的是其中组织名和组织ID必须与 configtx.yaml 文件中相匹配,这是前面修改 configtx.yaml 的原因,不然很容易出错,其中各个参数的含义可以对照下面的模板参数理解。

  2. 设置模板参数
    ORG=soft P0PORT=7251 CAPORT=7250 cryptoPath=$LOCAL_CA_PATH/soft.ifantasy.net PEERPEM=$cryptoPath/assets/tls-ca-cert.pem CAPEM=$cryptoPath/assets/ca-cert.pem 
  3. 获取 tls 证书和 ca 证书
    PP="`awk 'NF {sub(/\n/, ""); printf "%s\\\n",$0;}' $PEERPEM`" CP="`awk 'NF {sub(/\n/, ""); printf "%s\\\n",$0;}' $CAPEM`" 
  4. 生成模板文件
    sed -e "s/${ORG}/$ORG/"          -e "s/${P0PORT}/$P0PORT/"          -e "s/${CAPORT}/$CAPORT/"          -e "s#${PEERPEM}#$PP#"          -e "s#${CAPEM}#$CP#"          config/ccp-template.yaml | sed -e $'s/\\n/\n          /g'  > connection-soft.yaml 

依次执行上述命令,最后会将连接配置文件 connection-soft.yaml 输出到实验根目录中,本例中其内容如下:

--- name: test-network-soft version: 1.0.0 client:   organization: soft   connection:     timeout:       peer:         endorser: '300' organizations:   soft:     mspid: softMSP     peers:     - peer1.soft.ifantasy.net     certificateAuthorities:     - soft.ifantasy.net peers:   peer1.soft.ifantasy.net:     url: grpcs://peer1.soft.ifantasy.net:7251     tlsCACerts:       pem: |           -----BEGIN CERTIFICATE-----           MIICHzCCAcWgAwIBAgIUbO4XSCy2KbQQN/E63zvkhUJfMzwwCgYIKoZIzj0EAwIw           bDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK           EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMR0wGwYDVQQDExRjb3VuY2ls           LmlmYW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGwx           CzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChML           SHlwZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEdMBsGA1UEAxMUY291bmNpbC5p           ZmFudGFzeS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQecDRTwml7bcaD           nZdPiEYiTxFwHa+g2nw+mq+6KeMPW98WT3BPNErb1gw9BQa6GRcTypJ7Ga1lSqLS           IFD+aypYo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAd           BgNVHQ4EFgQUq3Q80AlYM9lGKHWVupCEjpyBb1kwCgYIKoZIzj0EAwIDSAAwRQIh           AJashZ+Sob7DoOpYII22wDOPSV8updo1W9LNEAaxzMyTAiAokfgCVjtlX3EJnV+m           qc5EBQCjA0AaX1HPNBTUII7T+Q==           -----END CERTIFICATE-----                grpcOptions:       ssl-target-name-override: peer1.soft.ifantasy.net       hostnameOverride: peer1.soft.ifantasy.net certificateAuthorities:   soft.ifantasy.net:     url: https://soft.ifantasy.net:7250     caName: soft.ifantasy.net     tlsCACerts:       pem:          - |           -----BEGIN CERTIFICATE-----           MIICGDCCAb+gAwIBAgIUXF3f1cgHiAMO03c/61iyFWAD/0AwCgYIKoZIzj0EAwIw           aTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK           EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRowGAYDVQQDExFzb2Z0Lmlm           YW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGkxCzAJ           BgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChMLSHlw           ZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEaMBgGA1UEAxMRc29mdC5pZmFudGFz           eS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASP0Vs5wUaRzIyiXx2ygH6A           IQyCLe6VhTxnNPmJhMUVOmO+iyLJqMUuQRRHIcCgiNGPR9cqd4ygcRJBvsG+sooY           o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4E           FgQUkPhZPSjyHVdL5NkQED1Rdif7GdowCgYIKoZIzj0EAwIDRwAwRAIgfOt69wD8           HEqroGm/zVFf/NiqivluaK5Yf3Ryn0C7p5ECID/KNGjbt5b53ivuL5slK5B+8eA2           KGUN7ysBzX8hTzPj           -----END CERTIFICATE-----                httpOptions:       verify: false 

上述操作已打包至 5_GenConnectYaml.sh 中,也可以直接在根目录下运行 5_GenConnectYaml.sh 来了生成连接配置文件。

客户端代码

  1. 初始化目录/文件
    在实验根目录 6_ContractGatewayAndSDK 下创建目录 contract-sdk 作为 fabric-sdk 客户端的根目录,并在其下创建主程序 app.go 。将上节生成的 connection-soft.yaml 复制到该目录下,最终目录结构为:
     contract-sdk  ├── app.go  ├── connection-soft.yaml  ├── go.mod  ├── go.sum  ├── keystore  └── wallet      └── appUser.id 
  2. 向 app.go 写入以下内容
     package main   import (      "fmt"      "io/ioutil"      "log"      "os"      "path/filepath"       "github.com/hyperledger/fabric-sdk-go/pkg/core/config"      "github.com/hyperledger/fabric-sdk-go/pkg/gateway"  )   func main() {      log.Println("============ application-golang starts ============")       err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true")      if err != nil {          log.Fatalf("Error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err)      }       wallet, err := gateway.NewFileSystemWallet("wallet")      if err != nil {          log.Fatalf("Failed to create wallet: %v", err)      }       err = populateWallet(wallet)      // 调试建议注释这里      // if !wallet.Exists("appUser") {      // 	err = populateWallet(wallet)      // 	if err != nil {      // 		log.Fatalf("Failed to populate wallet contents: %v", err)      // 	}      // }       ccpPath := filepath.Join(          "connection-soft.yaml",      )       gw, err := gateway.Connect(          gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))),          gateway.WithIdentity(wallet, "appUser"),      )      if err != nil {          log.Fatalf("Failed to connect to gateway: %v", err)      }      defer gw.Close()            network, err := gw.GetNetwork("testchannel")      if err != nil {          log.Fatalf("Failed to get network: %v", err)      }            contract := network.GetContract("basic")       log.Println("--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger")      result, err := contract.EvaluateTransaction("GetAllProjects")      if err != nil {          log.Fatalf("Failed to evaluate transaction: %v", err)      }      log.Println(string(result))       log.Println("--> Submit Transaction: DeleteProject, delete new project info with ID arguments")      result, err = contract.SubmitTransaction("DeleteProject", "FA8B31A55CD59DB352BCBF4D2AE791AD")      if err != nil {          log.Fatalf("Failed to Submit transaction: %v", err)      }      log.Println(string(result))  }   func populateWallet(wallet *gateway.Wallet) error {      log.Println("============ Populating wallet ============")      credPath := filepath.Join(          "..",          "orgs",          "soft.ifantasy.net",          "registers",          "user1",          "msp",      )       certPath := filepath.Join(credPath, "signcerts", "cert.pem")      // read the certificate pem      cert, err := ioutil.ReadFile(filepath.Clean(certPath))      if err != nil {          return err      }       keyDir := filepath.Join(credPath, "keystore")      // there's a single file in this dir containing the private key      files, err := ioutil.ReadDir(keyDir)      if err != nil {          return err      }      if len(files) != 1 {          return fmt.Errorf("keystore folder should have contain one file")      }      keyPath := filepath.Join(keyDir, files[0].Name())      key, err := ioutil.ReadFile(filepath.Clean(keyPath))      if err != nil {          return err      }       identity := gateway.NewX509Identity("softMSP", string(cert), string(key))       return wallet.Put("appUser", identity)  } 

客户端演示

如无特殊说明,以下命令默认运行于实验根目录 contract-sdk 下:

  1. 初始化模块
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway 
  2. 下载依赖
    go get 
  3. 运行客户端
    go run . 

    Hyperledger Fabric 智能合约开发及 fabric-sdk-go/fabric-gateway 使用示例

Q&A

遇到错误:

QueryBlockConfig failed: no channel peers configured for channel [testchannel] 

解决方法: 大概率是连接配置文件组织名称啥的写错了,再次检查组织配置文件与configtx.yaml中声明的是否匹配。

遇到错误:

2022/06/10 15:55:44 Failed to get network: Failed to create new channel client: event service creation failed: could not get chConfig cache reference: QueryBlockConfig failed: QueryBlockConfig failed: target(s) required 

解决方法: 可能是因为 wallet 目录下的身份与所申明的身份不匹配,建议每次启动前删除 wallet 目录让它重新生成。

遇到错误:

2022/06/10 16:08:13 Failed to Submit transaction: Failed to submit: error getting channel response for channel [testchannel]: no successful response received from any peer: access denied 

解决方法: 此时检查对应的 peer 节点容器日志若有 implicit policy evaluation failed 错误,则说明当前使用的身份权限不足。在实验中使用 peer 类型的用户身份则会导致此问题,建议使用 client 身份的用户(admin 身份也行)。

遇到错误:

2022/06/10 16:08:13 Failed to Submit transaction: Failed to submit: error getting channel response for channel [testchannel]: no successful response received from any peer: access denied 

解决方法: 此时检查对应的 peer 节点容器日志若有 implicit policy evaluation failed 错误,则说明当前使用的身份权限不足。在实验中使用 peer 类型的用户身份则会导致此问题,建议使用 client 身份的用户(admin 身份也行)。

参考

[1]: hyperledger-fabric. Fabric Contract APIs and Application APIs. readthedocs.io. [-]
[2]: barney2k7. What is the difference between fabric-chaincode-go and fabric-contract-api-go?. stackoverflow.com. [2020-05-08]
[3]: Nikos Karamolegkos. fabric-sdk-go vs fabric-gateway. When to use each one?. hyperledger.org. [2021-12-07]
[4]: kid1999 Karamolegkos. Fabric智能合约Go开发包简单理解. github.io. [2021-06-26]

发表评论

相关文章