前言
在上個實驗 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
的介面實現對合約操作。 - 專案資訊結構體
專案資訊結構體主要定義了單個專案的基本資訊,類似於 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 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]: 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]