前言
在上個實驗 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:[email protected]: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:[email protected]: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:[email protected]: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:[email protected]: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:[email protected]: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的介面實作對合約操作, - 專案資訊結構體
專案資訊結構體主要定義了單個專案的基本資訊,類似于 Java 的 Entity 類、資料庫的單個表,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"` // 專案描述 } - 初始化智能合約資料
在 Fabric 某個舊版本之前必須提供智能合約初始化函式,但在本實驗所用的 Fabric 2.4 則是可選項,在此僅僅是為了寫入預設實驗資料,Fabric 底層使用默認鍵值對(key-value)狀態資料庫 LevelDB 儲存資料,在操作體驗上十分像 redis 資料庫,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) 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) } - 洗掉指定專案資訊
Fabric 聯盟鏈作為區塊鏈的一種特殊形式,同樣具有可追溯特性,因此任何對資料的增刪改操作都是軟操作——留下操作記錄,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) 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 1CHAINCODE_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]: 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]
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/498619.html
標籤:區塊鏈
