前言
在上个试验 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
根目录下执行,在开始前面的试验前依照以下命令启动根底试验网络:
- 设置 DNS(如果未设置):
./setDNS.sh
- 设置环境变量:
source envpeer1soft
- 启动 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。
合约代码
- 初始化目录 / 文件
在试验根目录6_ContractGatewayAndSDK
下创立目录contract
作为智能合约根目录,并在其下创立智能合约文件project_contract.go
,后续代码皆在project_contract.go
中。 -
智能合约构造体
type ProjectContract struct {contractapi.Contract}
智能合约构造体个别是固定写法,创立任意一个构造体而后继承
contractapi.Contract
即可,当部署至链上后利用其继承的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"` // 我的项目形容 }
我的项目信息结构体次要定义了单个我的项目的根本信息,相似于 Java 的 Entity 类、数据库的单个表。
-
初始化智能合约数据
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 数据库。
-
判断我的项目信息是否已存在
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) 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) }
-
删除指定我的项目信息
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 联盟链作为区块链的一种非凡模式,同样具备可追溯个性,因而任何对数据的增删改操作都是软操作——留下操作记录。
-
批改我的项目信息
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) }
-
查问我的项目信息
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) 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) } }
至此,我的项目信息管理智能合约外围代码以编写结束,残缺 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
下。
-
初始化模块
go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract
-
将所有依赖下载到本地
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
下:
-
合约打包
source envpeer1soft peer lifecycle chaincode package basic.tar.gz --path contract --lang golang --label basic_1
-
三组织装置
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
-
三组织批准
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
。 -
提交并测试
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 客户端示例
客户端代码
-
初始化目录 / 文件
在试验根目录6_ContractGatewayAndSDK
下创立目录contract-gateway
作为 fabric-gateway 客户端的根目录,并在其下创立联盟链网络连接文件connect.go
和 客户端主程序app.go
。试验最终目录构造为:contract-gateway ├── app.go ├── connect.go ├── go.mod └── go.sum
-
向
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 类型的专用用户连贯网络更合乎开发理念。
-
向
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
下: -
初始化模块
go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway
-
下载依赖
go get
此时试验目录构造为
-
运行客户端
go run .
因为本目录下同时有两个
package
为main
的 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 组织的连贯配置文件:
-
创立模板文件
将官网模板 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
的起因,不然很容易出错,其中各个参数的含意能够对照上面的模板参数了解。 -
设置模板参数
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
-
获取 tls 证书和 ca 证书
PP="`awk'NF {sub(/\\n/, ""); printf"%s\\\\\\\n",$0;}' $PEERPEM`"CP="`awk 'NF {sub(/\\n/,""); printf "%s\\\\\\\n",$0;}'$CAPEM`"
-
生成模板文件
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
来了生成连贯配置文件。
客户端代码
-
初始化目录 / 文件
在试验根目录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
-
向 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
下:
-
初始化模块
go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway
-
下载依赖
go get
-
运行客户端
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 ↩