关于fabric:Hyperledger-Fabric-智能合约开发及-fabricsdkgofabricgateway-使用示例

55次阅读

共计 28623 个字符,预计需要花费 72 分钟才能阅读完成。

前言

在上个试验 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.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 网络为:

根底环境

注册用户

间接运行根目录下的 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"]}'

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:%s\n", 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 下:

  4. 初始化模块

    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway
  5. 下载依赖

    go get

    此时试验目录构造为

  6. 运行客户端

    go run .

    因为本目录下同时有两个 packagemain 的 go 文件,所以要用 . 的形式运行,运行后果如下:

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.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 .

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: 作者. 文章题目. 发表地. [发表或更新日期] –>


  1. 1 ↩

正文完
 0