前言

在上个实验 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/FabricLearn 的 6_ContractGatewayAndSDK 下):

运行端口说明
council.ifantasy.net7050council 组织的 CA 服务, 为联盟链网络提供 TLS-CA 服务
orderer1.council.ifantasy.net7051council 组织的 orderer1 服务
orderer1.council.ifantasy.net7052council 组织的 orderer1 服务的 admin 服务
orderer2.council.ifantasy.net7054council 组织的 orderer2 服务
orderer2.council.ifantasy.net7055council 组织的 orderer2 服务的 admin 服务
orderer3.council.ifantasy.net7057council 组织的 orderer3 服务
orderer3.council.ifantasy.net7058council 组织的 orderer3 服务的 admin 服务
soft.ifantasy.net7250soft 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.soft.ifantasy.net7251soft 组织的 peer1 成员节点
web.ifantasy.net7350web 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.web.ifantasy.net7351web 组织的 peer1 成员节点
hard.ifantasy.net7450hard 组织的 CA 服务, 包含成员: peer1 、 admin1 、user1
peer1.hard.ifantasy.net7451hard 组织的 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 网络为:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SSmqbTxa-1654947331595)(https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/hyperledger_fabric_5_run_orderer_by_oneself-2022-04-12-09-24-43.png “初始 docker 网络”)]

基础环境

注册用户

直接运行根目录下的 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.pemexport FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/ca/adminfabric-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:7250fabric-ca-client register -d --id.name peer1 --id.secret peer1 --id.type peer -u https://soft.ifantasy.net:7250fabric-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/user1export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pemexport FABRIC_CA_CLIENT_MSPDIR=mspfabric-ca-client enroll -d -u https://user1:user1@soft.ifantasy.net:7250echo "Enroll Admin1"export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pemexport FABRIC_CA_CLIENT_MSPDIR=mspfabric-ca-client enroll -d -u https://admin1:admin1@soft.ifantasy.net:7250mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincertscp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincerts/cert.pemecho "Enroll Peer1"export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pemexport FABRIC_CA_CLIENT_MSPDIR=mspfabric-ca-client enroll -d -u https://peer1:peer1@soft.ifantasy.net:7250# for TLSexport FABRIC_CA_CLIENT_MSPDIR=tls-mspexport FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/tls-ca-cert.pemfabric-ca-client enroll -d -u https://peer1soft:peer1soft@council.ifantasy.net:7050 --enrollment.profile tls --csr.hosts peer1.soft.ifantasy.netcp $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/*_sk $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/key.pemmkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincertscp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincerts/cert.pemmkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/admincertsmkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/cacertsmkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/tlscacertsmkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/userscp $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.pemcp $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.yamlcp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/config.yamlcp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/config.yamlecho "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 组织为例,其组织通道配置如下:

- &softName: softMSPID: softMSPMSPDir: ../orgs/soft.ifantasy.net/mspPolicies:Readers:Type: SignatureRule: "OR('softMSP.admin', 'softMSP.peer', 'softMSP.client')"Writers:Type: SignatureRule: "OR('softMSP.admin', 'softMSP.client')"Admins:Type: SignatureRule: "OR('softMSP.admin')"Endorsement:Type: SignatureRule: "OR('softMSP.peer')"AnchorPeers:- Host: peer1.soft.ifantasy.netPort: 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"` // 项目唯一IDName string `json:"Name"` // 项目名称Developerstring `json:"Developer"`// 项目主要负责人Organization string `json:"Organization"` // 项目所属组织Category string `json:"Category"` // 项目所属类别 Urlstring `json:"Url"`// 项目介绍地址Describesstring `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 Projecterr = 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 []*Projectfor resultsIterator.HasNext() {queryResponse, err := resultsIterator.Next()if err != nil {return nil, err}var project Projecterr = 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 mainimport ("encoding/json""fmt""github.com/hyperledger/fabric-contract-api-go/contractapi""log")type ProjectContract struct {contractapi.Contract}type Project struct {ID string `json:"ID"` // 项目唯一IDName string `json:"Name"` // 项目名称Developerstring `json:"Developer"`// 项目主要负责人Organization string `json:"Organization"` // 项目所属组织Category string `json:"Category"` // 项目所属类别 Urlstring `json:"Url"`// 项目介绍地址Describesstring `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 Projecterr = 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 []*Projectfor resultsIterator.HasNext() {queryResponse, err := resultsIterator.Next()if err != nil {return nil, err}var project Projecterr = 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"]}'

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQmaKEPb-1654947331596)(https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/hyperledger_fabric_9_contract_and_sdk_application_example-2022-06-11-14-09-35.png “提交并测试”)]

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 mainimport ("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"// 所属组织的MSPIDcryptoPath= "/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 mainimport ("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:%s\n", result)}func formatJSON(data []byte) string {var prettyJSON bytes.Bufferif 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 文件,所以要用 . 的方式运行,运行结果如下:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I7NdXRB5-1654947331597)(https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/hyperledger_fabric_9_contract_and_sdk_application_example-2022-06-11-14-44-26.png “运行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.sh 和 ccp-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.0client:organization: ${ORG}connection:timeout:peer:endorser: '300'organizations:${ORG}:mspid: ${ORG}MSPpeers:- peer1.${ORG}.ifantasy.netcertificateAuthorities:- ${ORG}.ifantasy.netpeers:peer1.${ORG}.ifantasy.net:url: grpcs://peer1.${ORG}.ifantasy.net:${P0PORT}tlsCACerts:pem: |${PEERPEM}grpcOptions:ssl-target-name-override: peer1.${ORG}.ifantasy.nethostnameOverride: peer1.${ORG}.ifantasy.netcertificateAuthorities:${ORG}.ifantasy.net:url: https://${ORG}.ifantasy.net:${CAPORT}caName: ${ORG}.ifantasy.nettlsCACerts:pem: - |${CAPEM}httpOptions:verify: false

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

  2. 设置模板参数
    ORG=softP0PORT=7251CAPORT=7250cryptoPath=$LOCAL_CA_PATH/soft.ifantasy.netPEERPEM=$cryptoPath/assets/tls-ca-cert.pemCAPEM=$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-softversion: 1.0.0client:organization: softconnection:timeout:peer:endorser: '300'organizations:soft:mspid: softMSPpeers:- peer1.soft.ifantasy.netcertificateAuthorities:- soft.ifantasy.netpeers:peer1.soft.ifantasy.net:url: grpcs://peer1.soft.ifantasy.net:7251tlsCACerts:pem: |-----BEGIN CERTIFICATE-----MIICHzCCAcWgAwIBAgIUbO4XSCy2KbQQN/E63zvkhUJfMzwwCgYIKoZIzj0EAwIwbDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMR0wGwYDVQQDExRjb3VuY2lsLmlmYW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGwxCzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChMLSHlwZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEdMBsGA1UEAxMUY291bmNpbC5pZmFudGFzeS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQecDRTwml7bcaDnZdPiEYiTxFwHa+g2nw+mq+6KeMPW98WT3BPNErb1gw9BQa6GRcTypJ7Ga1lSqLSIFD+aypYo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUq3Q80AlYM9lGKHWVupCEjpyBb1kwCgYIKoZIzj0EAwIDSAAwRQIhAJashZ+Sob7DoOpYII22wDOPSV8updo1W9LNEAaxzMyTAiAokfgCVjtlX3EJnV+mqc5EBQCjA0AaX1HPNBTUII7T+Q==-----END CERTIFICATE-----grpcOptions:ssl-target-name-override: peer1.soft.ifantasy.nethostnameOverride: peer1.soft.ifantasy.netcertificateAuthorities:soft.ifantasy.net:url: https://soft.ifantasy.net:7250caName: soft.ifantasy.nettlsCACerts:pem: - |-----BEGIN CERTIFICATE-----MIICGDCCAb+gAwIBAgIUXF3f1cgHiAMO03c/61iyFWAD/0AwCgYIKoZIzj0EAwIwaTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQKEwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRowGAYDVQQDExFzb2Z0LmlmYW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChMLSHlwZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEaMBgGA1UEAxMRc29mdC5pZmFudGFzeS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASP0Vs5wUaRzIyiXx2ygH6AIQyCLe6VhTxnNPmJhMUVOmO+iyLJqMUuQRRHIcCgiNGPR9cqd4ygcRJBvsG+sooYo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4EFgQUkPhZPSjyHVdL5NkQED1Rdif7GdowCgYIKoZIzj0EAwIDRwAwRAIgfOt69wD8HEqroGm/zVFf/NiqivluaK5Yf3Ryn0C7p5ECID/KNGjbt5b53ivuL5slK5B+8eA2KGUN7ysBzX8hTzPj-----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 .

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AZKjNmKr-1654947331597)(https://cdn.jsdelivr.net/gh/wefantasy/FileCloud/img/hyperledger_fabric_9_contract_and_sdk_application_example-2022-06-11-15-44-08.png “运行SDK客户端”)]

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]