在上個實驗 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
根目錄下執行,在開始後面的實驗前按照以下命令啟動基礎實驗網路:
./setDNS.sh
source envpeer1soft
./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
的介面實現對合約操作。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"]}'
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 你可能會很疑惑,有些案例使用 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 組織的連線組態檔:
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
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
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 .
遇到錯誤:
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]