Hyperledger Fabric 智慧合約開發及 fabric-sdk-go/fabric-gateway 使用示例

丿風色幻想發表於2022-06-11

前言

在上個實驗 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/FabricLearn6_ContractGatewayAndSDK 下):

執行埠 說明
council.ifantasy.net 7050 council 組織的 CA 服務, 為聯盟鏈網路提供 TLS-CA 服務
orderer1.council.ifantasy.net 7051 council 組織的 orderer1 服務
orderer1.council.ifantasy.net 7052 council 組織的 orderer1 服務的 admin 服務
orderer2.council.ifantasy.net 7054 council 組織的 orderer2 服務
orderer2.council.ifantasy.net 7055 council 組織的 orderer2 服務的 admin 服務
orderer3.council.ifantasy.net 7057 council 組織的 orderer3 服務
orderer3.council.ifantasy.net 7058 council 組織的 orderer3 服務的 admin 服務
soft.ifantasy.net 7250 soft 組織的 CA 服務, 包含成員: peer1 、 admin1 、user1
peer1.soft.ifantasy.net 7251 soft 組織的 peer1 成員節點
web.ifantasy.net 7350 web 組織的 CA 服務, 包含成員: peer1 、 admin1 、user1
peer1.web.ifantasy.net 7351 web 組織的 peer1 成員節點
hard.ifantasy.net 7450 hard 組織的 CA 服務, 包含成員: peer1 、 admin1 、user1
peer1.hard.ifantasy.net 7451 hard 組織的 peer1 成員節點

實驗準備

本文網路結構直接將 Hyperledger Fabric無排序組織以Raft協議啟動多個Orderer服務、TLS組織執行維護Orderer服務 中建立的 4-2_RunOrdererByCouncil 複製為 6_ContractGatewayAndSDK 並修改(建議直接將本案例倉庫 FabricLearn 下的 6_ContractGatewayAndSDK 目錄拷貝到本地執行),文中大部分命令在 Hyperledger Fabric定製聯盟鏈網路工程實踐 中已有介紹因此不會詳細說明。預設情況下,所有命令皆在 6_ContractGatewayAndSDK 根目錄下執行,在開始後面的實驗前按照以下命令啟動基礎實驗網路:

  1. 設定DNS(如果未設定): ./setDNS.sh
  2. 設定環境變數: source envpeer1soft
  3. 啟動CA網路: ./0_Restart.sh

本實驗初始 docker 網路為:
初始 docker 網路

基礎環境

註冊使用者

直接執行根目錄下的 1_RegisterUser.sh 即可完成本實驗所需使用者的註冊。以往我們每個組織只有一個 peer 節點和一個 admin 節點,但這些節點都不適合為客戶端所用,因此基礎環境的改變主要包含了為每個組織新增一個 client 型別的使用者。以 soft 組織為例,其註冊使用者命令為:

echo "Working on soft"
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/ca/crypto/ca-cert.pem
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/ca/admin
fabric-ca-client enroll -d -u https://ca-admin:ca-adminpw@soft.ifantasy.net:7250
# client 型別使用者註冊
fabric-ca-client register -d --id.name user1 --id.secret user1 --id.type client -u https://soft.ifantasy.net:7250
fabric-ca-client register -d --id.name peer1 --id.secret peer1 --id.type peer -u https://soft.ifantasy.net:7250
fabric-ca-client register -d --id.name admin1 --id.secret admin1 --id.type admin -u https://soft.ifantasy.net:7250

組織證照構建

直接執行根目錄下的 2_EnrollUser.sh 即可完成本實驗所需證照的構建,每個組織主要增加了 client 型別使用者的證照構建每個註冊使用者單元配置檔案 config.yaml ,以 soft 組織為例,其生成組織證照的命令為:

echo "Start Soft============================="
# 新增
echo "Enroll User1"
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/user1
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem
export FABRIC_CA_CLIENT_MSPDIR=msp
fabric-ca-client enroll -d -u https://user1:user1@soft.ifantasy.net:7250

echo "Enroll Admin1"
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem
export FABRIC_CA_CLIENT_MSPDIR=msp
fabric-ca-client enroll -d -u https://admin1:admin1@soft.ifantasy.net:7250
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincerts
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/admincerts/cert.pem

echo "Enroll Peer1"
export FABRIC_CA_CLIENT_HOME=$LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem
export FABRIC_CA_CLIENT_MSPDIR=msp
fabric-ca-client enroll -d -u https://peer1:peer1@soft.ifantasy.net:7250
# for TLS
export FABRIC_CA_CLIENT_MSPDIR=tls-msp
export FABRIC_CA_CLIENT_TLS_CERTFILES=$LOCAL_CA_PATH/soft.ifantasy.net/assets/tls-ca-cert.pem
fabric-ca-client enroll -d -u https://peer1soft:peer1soft@council.ifantasy.net:7050 --enrollment.profile tls --csr.hosts peer1.soft.ifantasy.net
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/*_sk $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/tls-msp/keystore/key.pem
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincerts
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/admincerts/cert.pem

mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/admincerts
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/cacerts
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/tlscacerts
mkdir -p $LOCAL_CA_PATH/soft.ifantasy.net/msp/users
cp $LOCAL_CA_PATH/soft.ifantasy.net/assets/ca-cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/cacerts/
cp $LOCAL_CA_PATH/soft.ifantasy.net/assets/tls-ca-cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/tlscacerts/
cp $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/signcerts/cert.pem $LOCAL_CA_PATH/soft.ifantasy.net/msp/admincerts/cert.pem

cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/msp/config.yaml
# 新增
cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/user1/msp/config.yaml
cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/admin1/msp/config.yaml
cp $LOCAL_ROOT_PATH/config/config-msp.yaml $LOCAL_CA_PATH/soft.ifantasy.net/registers/peer1/msp/config.yaml
echo "End Soft============================="

為了配合使用每個使用者的單元配置檔案,需要將所有使用者 msp 目錄下的 cacerts/council-ifantasy-net-7050.pem 檔名修改為 cacerts/ca-cert.pem ,因此在 2_EnrollUser.sh 的末尾追加一行批量修改檔名的命令來實現此目的:

# 按正則匹配並批量修改符合要求的檔案
find orgs/ -regex ".+cacerts.+.pem" -not -regex ".+tlscacerts.+" | rename 's/cacerts\/.+\.pem/cacerts\/ca-cert\.pem/'

配置通道

直接執行根目錄下的 3_Configtxgen.sh 即可完成本實驗所需通道配置,需要注意的是,為了使通道組織架構更加清晰,將通道配置檔案 configtx.yaml 中各組織名稱從 orgnameMSP 改為了 orgname ,以 soft 組織為例,其組織通道配置如下:

- &soft
    Name: softMSP
    ID: softMSP
    MSPDir: ../orgs/soft.ifantasy.net/msp
    Policies:
        Readers:
            Type: Signature
            Rule: "OR('softMSP.admin', 'softMSP.peer', 'softMSP.client')"
        Writers:
            Type: Signature
            Rule: "OR('softMSP.admin', 'softMSP.client')"
        Admins:
            Type: Signature
            Rule: "OR('softMSP.admin')"
        Endorsement:
            Type: Signature
            Rule: "OR('softMSP.peer')"
    AnchorPeers:
        - Host: peer1.soft.ifantasy.net
            Port: 7251

智慧合約開發

本節將參考官方示例智慧合約 asset-transfer-basic 開發工作室聯盟鏈的 專案資源管理智慧合約 ,其在官方示例的基礎上進行了依賴和結構上的簡化。本示例是基於 Go 語言的智慧合約,因此建議先學習 Go 語言基礎概念和規範,不然自行定製可能會有一些 Bug 。

合約程式碼

  1. 初始化目錄/檔案
    在實驗根目錄 6_ContractGatewayAndSDK 下建立目錄 contract 作為智慧合約根目錄,並在其下建立智慧合約檔案 project_contract.go ,後續程式碼皆在 project_contract.go 中。
  2. 智慧合約結構體
    type ProjectContract struct {
        contractapi.Contract
    }
    
    智慧合約結構體一般是固定寫法,建立任意一個結構體然後繼承 contractapi.Contract 即可,當部署至鏈上後利用其繼承的 contractapi.Contract 的介面實現對合約操作。
  3. 專案資訊結構體
    type Project struct {
        ID           string `json:"ID"`             // 專案唯一ID
        Name         string `json:"Name"`           // 專案名稱
        Developer    string `json:"Developer"`      // 專案主要負責人
        Organization string `json:"Organization"`   // 專案所屬組織
        Category     string `json:"Category"`       // 專案所屬類別 
        Url          string `json:"Url"`            // 專案介紹地址
        Describes    string `json:"Describes"`      // 專案描述
    }
    
    專案資訊結構體主要定義了單個專案的基本資訊,類似於 Java 的 Entity 類、資料庫的單個表。
  4. 初始化智慧合約資料
    func (s *ProjectContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
        projects := []Project{
            {ID: "FA8B31A55CD59DB352BCBF4D2AE791AD", Name: "工作室聯盟鏈管理系統", Developer: "Fantasy", Organization: "Web", Category: "blockchain", Url: "https://github.com/wefantasy/FabricLearn", Describes: "本專案虛擬了一個工作室聯盟鏈需求並將逐步實現,致力於提供一個易理解、可復現的Fabric學習專案,其中專案部署步驟的各個環節都清晰可見,並且將所有實驗打包為指令碼使之能夠被快速復現在任何一臺主機上"},
        }
        for _, project := range projects {
            projectJSON, err := json.Marshal(project)
            if err != nil {
                return err
            }
            err = ctx.GetStub().PutState(project.ID, projectJSON)
            if err != nil {
                return fmt.Errorf("failed to put to world state. %v", err)
            }
        }
        return nil
    }
    
    在 Fabric 某個舊版本之前必須提供智慧合約初始化函式,但在本實驗所用的 Fabric 2.4 則是可選項,在此僅僅是為了寫入預設實驗資料。Fabric 底層使用預設鍵值對(key-value)狀態資料庫 LevelDB 儲存資料,在操作體驗上十分像 redis 資料庫。
  5. 判斷專案資訊是否已存在
    func (s *ProjectContract) ProjectExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
        projectJSON, err := ctx.GetStub().GetState(id)
        if err != nil {
            return false, fmt.Errorf("failed to read from world state: %v", err)
        }
    
        return projectJSON != nil, nil
    }
    
  6. 寫入新專案資訊
    func (s *ProjectContract) CreateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
        exists, err := s.ProjectExists(ctx, id)
        if err != nil {
            return err
        }
        if exists {
            return fmt.Errorf("the project %s already exists", id)
        }
        project := Project{
            ID:           id,
            Name:         name,
            Developer:    developer,
            Organization: organization,
            Category:     category,
            Url:          url,
            Describes:    describes,
        }
        projectJSON, err := json.Marshal(project)
        if err != nil {
            return err
        }
        return ctx.GetStub().PutState(id, projectJSON)
    }
    
  7. 刪除指定專案資訊
    func (s *ProjectContract) DeleteProject(ctx contractapi.TransactionContextInterface, id string) error {
        exists, err := s.ProjectExists(ctx, id)
        if err != nil {
            return err
        }
        if !exists {
            return fmt.Errorf("the project %s does not exist", id)
        }
    
        return ctx.GetStub().DelState(id)
    }
    
    Fabric 聯盟鏈作為區塊鏈的一種特殊形式,同樣具有可追溯特性,因此任何對資料的增刪改操作都是軟操作——留下操作記錄。
  8. 修改專案資訊
    func (s *ProjectContract) UpdateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
        exists, err := s.ProjectExists(ctx, id)
        if err != nil {
            return err
        }
        if !exists {
            return fmt.Errorf("the project %s does not exist", id)
        }
        project := Project{
            ID:           id,
            Name:         name,
            Developer:    developer,
            Organization: organization,
            Category:     category,
            Url:          url,
            Describes:    describes,
        }
        projectJSON, err := json.Marshal(project)
        if err != nil {
            return err
        }
        return ctx.GetStub().PutState(id, projectJSON)
    }
    
  9. 查詢專案資訊
    func (s *ProjectContract) ReadProject(ctx contractapi.TransactionContextInterface, id string) (*Project, error) {
        projectJSON, err := ctx.GetStub().GetState(id)
        if err != nil {
            return nil, fmt.Errorf("failed to read from world state: %v", err)
        }
        if projectJSON == nil {
            return nil, fmt.Errorf("the project %s does not exist", id)
        }
    
        var project Project
        err = json.Unmarshal(projectJSON, &project)
        if err != nil {
            return nil, err
        }
    
        return &project, nil
    }
    
  10. 查詢鏈上所有專案資訊
    func (s *ProjectContract) GetAllProjects(ctx contractapi.TransactionContextInterface) ([]*Project, error) {
        // GetStateByRange 查詢引數為兩個空字串時即查詢所有資料
        resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
        if err != nil {
            return nil, err
        }
        defer resultsIterator.Close()
    
        var projects []*Project
        for resultsIterator.HasNext() {
            queryResponse, err := resultsIterator.Next()
            if err != nil {
                return nil, err
            }
    
            var project Project
            err = json.Unmarshal(queryResponse.Value, &project)
            if err != nil {
                return nil, err
            }
            projects = append(projects, &project)
        }
    
        return projects, nil
    }
    
  11. 智慧合約入口函式/主函式
    func main() {
        chaincode, err := contractapi.NewChaincode(&ProjectContract{})
        if err != nil {
            log.Panicf("Error creating project-manage chaincode: %v", err)
        }
    
        if err := chaincode.Start(); err != nil {
            log.Panicf("Error starting project-manage chaincode: %v", err)
        }
    }
    

至此,專案資訊管理智慧合約核心程式碼以編寫完畢,完整 project_contract.go 檔案內容如下(需要注意的是合約入口必須屬於 main 包):

package main

import (
	"encoding/json"
	"fmt"
	"github.com/hyperledger/fabric-contract-api-go/contractapi"
	"log"
)

type ProjectContract struct {
	contractapi.Contract
}

type Project struct {
	ID           string `json:"ID"`             // 專案唯一ID
	Name         string `json:"Name"`           // 專案名稱
	Developer    string `json:"Developer"`      // 專案主要負責人
	Organization string `json:"Organization"`   // 專案所屬組織
	Category     string `json:"Category"`       // 專案所屬類別 
	Url          string `json:"Url"`            // 專案介紹地址
	Describes    string `json:"Describes"`      // 專案描述
}

// 初始化智慧合約資料
func (s *ProjectContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
	projects := []Project{
		{ID: "FA8B31A55CD59DB352BCBF4D2AE791AD", Name: "工作室聯盟鏈管理系統", Developer: "Fantasy", Organization: "Web", Category: "blockchain", Url: "https://github.com/wefantasy/FabricLearn", Describes: "本專案虛擬了一個工作室聯盟鏈需求並將逐步實現,致力於提供一個易理解、可復現的Fabric學習專案,其中專案部署步驟的各個環節都清晰可見,並且將所有實驗打包為指令碼使之能夠被快速復現在任何一臺主機上"},
	}
	for _, project := range projects {
		projectJSON, err := json.Marshal(project)
		if err != nil {
			return err
		}
		err = ctx.GetStub().PutState(project.ID, projectJSON)
		if err != nil {
			return fmt.Errorf("failed to put to world state. %v", err)
		}
	}
	return nil
}

// 寫入新專案
func (s *ProjectContract) CreateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
	exists, err := s.ProjectExists(ctx, id)
	if err != nil {
		return err
	}
	if exists {
		return fmt.Errorf("the project %s already exists", id)
	}

	project := Project{
		ID:           id,
		Name:         name,
		Developer:    developer,
		Organization: organization,
		Category:     category,
		Url:          url,
		Describes:    describes,
	}
	projectJSON, err := json.Marshal(project)
	if err != nil {
		return err
	}
	return ctx.GetStub().PutState(id, projectJSON)
}

// 讀取指定ID的專案資訊
func (s *ProjectContract) ReadProject(ctx contractapi.TransactionContextInterface, id string) (*Project, error) {
	projectJSON, err := ctx.GetStub().GetState(id)
	if err != nil {
		return nil, fmt.Errorf("failed to read from world state: %v", err)
	}
	if projectJSON == nil {
		return nil, fmt.Errorf("the project %s does not exist", id)
	}

	var project Project
	err = json.Unmarshal(projectJSON, &project)
	if err != nil {
		return nil, err
	}

	return &project, nil
}

// 更新專案資訊.
func (s *ProjectContract) UpdateProject(ctx contractapi.TransactionContextInterface, id string, name string, developer string, organization string, category string, url string, describes string) error {
	exists, err := s.ProjectExists(ctx, id)
	if err != nil {
		return err
	}
	if !exists {
		return fmt.Errorf("the project %s does not exist", id)
	}

	project := Project{
		ID:           id,
		Name:         name,
		Developer:    developer,
		Organization: organization,
		Category:     category,
		Url:          url,
		Describes:    describes,
	}
	projectJSON, err := json.Marshal(project)
	if err != nil {
		return err
	}

	return ctx.GetStub().PutState(id, projectJSON)
}

// 刪除指定ID的專案資訊
func (s *ProjectContract) DeleteProject(ctx contractapi.TransactionContextInterface, id string) error {
	exists, err := s.ProjectExists(ctx, id)
	if err != nil {
		return err
	}
	if !exists {
		return fmt.Errorf("the project %s does not exist", id)
	}

	return ctx.GetStub().DelState(id)
}

// 判斷某專案是否存在
func (s *ProjectContract) ProjectExists(ctx contractapi.TransactionContextInterface, id string) (bool, error) {
	projectJSON, err := ctx.GetStub().GetState(id)
	if err != nil {
		return false, fmt.Errorf("failed to read from world state: %v", err)
	}

	return projectJSON != nil, nil
}

// 讀取所有專案資訊
func (s *ProjectContract) GetAllProjects(ctx contractapi.TransactionContextInterface) ([]*Project, error) {
	// GetStateByRange 查詢引數為兩個空字串時即查詢所有資料
	resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
	if err != nil {
		return nil, err
	}
	defer resultsIterator.Close()

	var projects []*Project
	for resultsIterator.HasNext() {
		queryResponse, err := resultsIterator.Next()
		if err != nil {
			return nil, err
		}

		var project Project
		err = json.Unmarshal(queryResponse.Value, &project)
		if err != nil {
			return nil, err
		}
		projects = append(projects, &project)
	}

	return projects, nil
}

func main() {
	chaincode, err := contractapi.NewChaincode(&ProjectContract{})
	if err != nil {
		log.Panicf("Error creating project-manage chaincode: %v", err)
	}

	if err := chaincode.Start(); err != nil {
		log.Panicf("Error starting project-manage chaincode: %v", err)
	}
}

依賴下載

合約程式碼編寫完成後並不能直接部署到聯盟鏈上,需要將合約中 import 匯入的包下載到本地以供後面一起打包,本小節所有命令預設執行於 6_ContractGatewayAndSDK/contract 下。

  1. 初始化模組
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract
    
  2. 將所有依賴下載到本地
    go mod vendor
    

以上命令執行成功後,智慧合約開發工作基本結束,此時 contract 目錄結構如下:

6_ContractGatewayAndSDK/contract
├── go.mod
├── go.sum
├── project_contract.go
└── vendor
    ├── github.com
    ├── golang.org
    ├── google.golang.org
    ├── gopkg.in
    └── modules.tx

合約部署測試

如無特殊說明,以下命令預設執行於實驗根目錄 6_ContractGatewayAndSDK 下:

  1. 合約打包
     source envpeer1soft
     peer lifecycle chaincode package basic.tar.gz --path contract --lang golang --label basic_1
    
  2. 三組織安裝
     source envpeer1soft
     peer lifecycle chaincode install basic.tar.gz
     peer lifecycle chaincode queryinstalled
     source envpeer1web
     peer lifecycle chaincode install basic.tar.gz
     peer lifecycle chaincode queryinstalled
     source envpeer1hard
     peer lifecycle chaincode install basic.tar.gz
     peer lifecycle chaincode queryinstalled
    
  3. 三組織批准
     export CHAINCODE_ID=basic_1:0f1f1ffc8e3865a9179e70a3c56237482b3eb4dcecd30ab51ab01a6f5d3daeff
     source envpeer1soft
     peer lifecycle chaincode approveformyorg -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA  --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID
     peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1
     source envpeer1web
     peer lifecycle chaincode approveformyorg -o orderer3.council.ifantasy.net:7057 --tls --cafile $ORDERER_CA  --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID
     peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1
     source envpeer1hard
     peer lifecycle chaincode approveformyorg -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA  --channelID testchannel --name basic --version 1.0 --sequence 1 --waitForEvent --init-required --package-id $CHAINCODE_ID
     peer lifecycle chaincode queryapproved -C testchannel -n basic --sequence 1
    
    注意要將 CHAINCODE_ID 的值改為三組織安裝時輸出的連碼包 ID
  4. 提交併測試
     source envpeer1soft
     peer lifecycle chaincode commit -o orderer2.council.ifantasy.net:7054 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --init-required --version 1.0 --sequence 1 --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE
     peer chaincode invoke --isInit -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["InitLedger"]}'
     sleep 5
     peer chaincode invoke -o orderer1.council.ifantasy.net:7051 --tls --cafile $ORDERER_CA --channelID testchannel --name basic --peerAddresses peer1.soft.ifantasy.net:7251 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE --peerAddresses peer1.web.ifantasy.net:7351 --tlsRootCertFiles $CORE_PEER_TLS_ROOTCERT_FILE -c '{"Args":["GetAllProjects"]}'
    
    提交併測試

fabric-gateway 客戶端示例

客戶端程式碼

  1. 初始化目錄/檔案
    在實驗根目錄 6_ContractGatewayAndSDK 下建立目錄 contract-gateway 作為 fabric-gateway 客戶端的根目錄,並在其下建立聯盟鏈網路連線檔案 connect.go 和 客戶端主程式 app.go 。實驗最終目錄結構為:
    contract-gateway
    ├── app.go
    ├── connect.go
    ├── go.mod
    └── go.sum
    
  2. connect.go 寫入以下內容
    package main
    
    import (
        "crypto/x509"
        "fmt"
        "io/ioutil"
        "path"
        "github.com/hyperledger/fabric-gateway/pkg/identity"
        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials"
    )
    
    const (
        mspID         = "softMSP"				// 所屬組織的MSPID
        cryptoPath    = "/root/FabricLearn/6_ContractGatewayAndSDK/orgs/soft.ifantasy.net"	// 中間變數
        certPath      = cryptoPath + "/registers/user1/msp/signcerts/cert.pem"		// client 使用者的簽名證照
        keyPath       = cryptoPath + "/registers/user1/msp/keystore/"		// client 使用者的私鑰路徑
        tlsCertPath   = cryptoPath + "/assets/tls-ca-cert.pem"			// client 使用者的 tls 通訊證照
        peerEndpoint  = "peer1.soft.ifantasy.net:7251"			// 所連 peer 節點的地址
        gatewayPeer   = "peer1.soft.ifantasy.net"		// 閘道器 peer 節點名稱
    )
    
    // 建立指向聯盟鏈網路的 gRPC 連線.
    func newGrpcConnection() *grpc.ClientConn {
        certificate, err := loadCertificate(tlsCertPath)
        if err != nil {
            panic(err)
        }
    
        certPool := x509.NewCertPool()
        certPool.AddCert(certificate)
        transportCredentials := credentials.NewClientTLSFromCert(certPool, gatewayPeer)
    
        connection, err := grpc.Dial(peerEndpoint, grpc.WithTransportCredentials(transportCredentials))
        if err != nil {
            panic(fmt.Errorf("failed to create gRPC connection: %w", err))
        }
    
        return connection
    }
    
    // 根據使用者指定的X.509證照為這個閘道器連線建立一個客戶端標識。
    func newIdentity() *identity.X509Identity {
        certificate, err := loadCertificate(certPath)
        if err != nil {
            panic(err)
        }
    
        id, err := identity.NewX509Identity(mspID, certificate)
        if err != nil {
            panic(err)
        }
        return id
    }
    
    // 載入證照檔案
    func loadCertificate(filename string) (*x509.Certificate, error) {
        certificatePEM, err := ioutil.ReadFile(filename)
        if err != nil {
            return nil, fmt.Errorf("failed to read certificate file: %w", err)
        }
        return identity.CertificateFromPEM(certificatePEM)
    }
    
    // 使用私鑰從訊息摘要生成數字簽名
    func newSign() identity.Sign {
        files, err := ioutil.ReadDir(keyPath)
        if err != nil {
            panic(fmt.Errorf("failed to read private key directory: %w", err))
        }
        privateKeyPEM, err := ioutil.ReadFile(path.Join(keyPath, files[0].Name()))
    
        if err != nil {
            panic(fmt.Errorf("failed to read private key file: %w", err))
        }
    
        privateKey, err := identity.PrivateKeyFromPEM(privateKeyPEM)
        if err != nil {
            panic(err)
        }
    
        sign, err := identity.NewPrivateKeySign(privateKey)
        if err != nil {
            panic(err)
        }
    
        return sign
    }
    

    值得說明的是,不論是 gateway 客戶端還是 fabric-sdk 客戶端,一般都可以通過 client 、 admin 型別的使用者連線聯盟鏈網路,只是建立單獨的 client 型別的專用使用者連線網路更符合開發理念。

  3. app.go 寫入以下內容
    package main
    
    import (
        "bytes"
        "encoding/json"
        "fmt"
        "time"
        "github.com/hyperledger/fabric-gateway/pkg/client"
    )
    
    const (
        channelName   = "testchannel"	// 連線的通道
        chaincodeName = "basic"			// 連線的鏈碼
    )
    
    func main() {
        clientConnection := newGrpcConnection()
        defer clientConnection.Close()
    
        id := newIdentity()
        sign := newSign()
    
        gateway, err := client.Connect(
            id,
            client.WithSign(sign),
            client.WithClientConnection(clientConnection),
            client.WithEvaluateTimeout(5*time.Second),
            client.WithEndorseTimeout(15*time.Second),
            client.WithSubmitTimeout(5*time.Second),
            client.WithCommitStatusTimeout(1*time.Minute),
        )
        if err != nil {
            panic(err)
        }
        defer gateway.Close()
    
        network := gateway.GetNetwork(channelName)
        contract := network.GetContract(chaincodeName)
    
        fmt.Println("getAllAssets:")
        getAllAssets(contract)
    }
    func getAllAssets(contract *client.Contract) {
        fmt.Println("Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger")
    
        evaluateResult, err := contract.EvaluateTransaction("GetAllProjects")
        if err != nil {
            panic(fmt.Errorf("failed to evaluate transaction: %w", err))
        }
        result := formatJSON(evaluateResult)
    
        fmt.Printf("*** Result:%s\n", result)
    }
    
    func formatJSON(data []byte) string {
        var prettyJSON bytes.Buffer
        if err := json.Indent(&prettyJSON, data, " ", ""); err != nil {
            panic(fmt.Errorf("failed to parse JSON: %w", err))
        }
        return prettyJSON.String()
    }
    

客戶端演示

如無特殊說明,以下命令預設執行於實驗根目錄 contract-gateway 下:

  1. 初始化模組
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway
    
  2. 下載依賴
    go get
    
    此時實驗目錄結構為
  3. 執行客戶端
    go run .
    
    因為本目錄下同時有兩個 packagemain 的 go 檔案,所以要用 . 的方式執行,執行結果如下:
    執行gateway客戶端

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.shccp-template.yaml ,後者為連線配置檔案的基準模板,前者使用 bash 命令將基準模板替換為具體連線配置檔案。連線配置檔案有 json 和 yaml 兩種格式,我覺得 yaml 語法更為簡潔,後續實驗以此為例。將 ccp-generate.sh 檔案中的函式展開後,可以很容易的得生成連線配置檔案的過程,本節所有命令預設執行於 6_ContractGatewayAndSDK 目錄下,通過如下命令生成 soft 組織的連線配置檔案:

  1. 建立模板檔案
    將官方模板 ccp-template.yaml 複製一份至我們專案的 6_ContractGatewayAndSDK/config/ccp-template.yaml 中,由於我們的命名規範與官方不同,且該模板通用性不高,因此將其內容改為如下:
    ---
    name: test-network-${ORG}
    version: 1.0.0
    client:
    organization: ${ORG}
    connection:
        timeout:
        peer:
            endorser: '300'
    organizations:
    ${ORG}:
        mspid: ${ORG}MSP
        peers:
        - peer1.${ORG}.ifantasy.net
        certificateAuthorities:
        - ${ORG}.ifantasy.net
    peers:
    peer1.${ORG}.ifantasy.net:
        url: grpcs://peer1.${ORG}.ifantasy.net:${P0PORT}
        tlsCACerts:
        pem: |
            ${PEERPEM}
        grpcOptions:
        ssl-target-name-override: peer1.${ORG}.ifantasy.net
        hostnameOverride: peer1.${ORG}.ifantasy.net
    certificateAuthorities:
    ${ORG}.ifantasy.net:
        url: https://${ORG}.ifantasy.net:${CAPORT}
        caName: ${ORG}.ifantasy.net
        tlsCACerts:
        pem: 
            - |
            ${CAPEM}
        httpOptions:
        verify: false
    

    這個模板可以跟我們專案很好的契合,需要特別注意的是其中組織名和組織ID必須與 configtx.yaml 檔案中相匹配,這是前面修改 configtx.yaml 的原因,不然很容易出錯,其中各個引數的含義可以對照下面的模板引數理解。

  2. 設定模板引數
    ORG=soft
    P0PORT=7251
    CAPORT=7250
    cryptoPath=$LOCAL_CA_PATH/soft.ifantasy.net
    PEERPEM=$cryptoPath/assets/tls-ca-cert.pem
    CAPEM=$cryptoPath/assets/ca-cert.pem
    
  3. 獲取 tls 證照和 ca 證照
    PP="`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $PEERPEM`"
    CP="`awk 'NF {sub(/\\n/, ""); printf "%s\\\\\\\n",$0;}' $CAPEM`"
    
  4. 生成模板檔案
    sed -e "s/\${ORG}/$ORG/" \
            -e "s/\${P0PORT}/$P0PORT/" \
            -e "s/\${CAPORT}/$CAPORT/" \
            -e "s#\${PEERPEM}#$PP#" \
            -e "s#\${CAPEM}#$CP#" \
            config/ccp-template.yaml | sed -e $'s/\\\\n/\\\n          /g'  > connection-soft.yaml
    

依次執行上述命令,最後會將連線配置檔案 connection-soft.yaml 輸出到實驗根目錄中,本例中其內容如下:

---
name: test-network-soft
version: 1.0.0
client:
  organization: soft
  connection:
    timeout:
      peer:
        endorser: '300'
organizations:
  soft:
    mspid: softMSP
    peers:
    - peer1.soft.ifantasy.net
    certificateAuthorities:
    - soft.ifantasy.net
peers:
  peer1.soft.ifantasy.net:
    url: grpcs://peer1.soft.ifantasy.net:7251
    tlsCACerts:
      pem: |
          -----BEGIN CERTIFICATE-----
          MIICHzCCAcWgAwIBAgIUbO4XSCy2KbQQN/E63zvkhUJfMzwwCgYIKoZIzj0EAwIw
          bDELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK
          EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMR0wGwYDVQQDExRjb3VuY2ls
          LmlmYW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGwx
          CzAJBgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChML
          SHlwZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEdMBsGA1UEAxMUY291bmNpbC5p
          ZmFudGFzeS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQecDRTwml7bcaD
          nZdPiEYiTxFwHa+g2nw+mq+6KeMPW98WT3BPNErb1gw9BQa6GRcTypJ7Ga1lSqLS
          IFD+aypYo0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAd
          BgNVHQ4EFgQUq3Q80AlYM9lGKHWVupCEjpyBb1kwCgYIKoZIzj0EAwIDSAAwRQIh
          AJashZ+Sob7DoOpYII22wDOPSV8updo1W9LNEAaxzMyTAiAokfgCVjtlX3EJnV+m
          qc5EBQCjA0AaX1HPNBTUII7T+Q==
          -----END CERTIFICATE-----
          
    grpcOptions:
      ssl-target-name-override: peer1.soft.ifantasy.net
      hostnameOverride: peer1.soft.ifantasy.net
certificateAuthorities:
  soft.ifantasy.net:
    url: https://soft.ifantasy.net:7250
    caName: soft.ifantasy.net
    tlsCACerts:
      pem: 
        - |
          -----BEGIN CERTIFICATE-----
          MIICGDCCAb+gAwIBAgIUXF3f1cgHiAMO03c/61iyFWAD/0AwCgYIKoZIzj0EAwIw
          aTELMAkGA1UEBhMCVVMxFzAVBgNVBAgTDk5vcnRoIENhcm9saW5hMRQwEgYDVQQK
          EwtIeXBlcmxlZGdlcjEPMA0GA1UECxMGRmFicmljMRowGAYDVQQDExFzb2Z0Lmlm
          YW50YXN5Lm5ldDAeFw0yMjA2MTEwNTU3MDBaFw0zNzA2MDcwNTU3MDBaMGkxCzAJ
          BgNVBAYTAlVTMRcwFQYDVQQIEw5Ob3J0aCBDYXJvbGluYTEUMBIGA1UEChMLSHlw
          ZXJsZWRnZXIxDzANBgNVBAsTBkZhYnJpYzEaMBgGA1UEAxMRc29mdC5pZmFudGFz
          eS5uZXQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASP0Vs5wUaRzIyiXx2ygH6A
          IQyCLe6VhTxnNPmJhMUVOmO+iyLJqMUuQRRHIcCgiNGPR9cqd4ygcRJBvsG+sooY
          o0UwQzAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBATAdBgNVHQ4E
          FgQUkPhZPSjyHVdL5NkQED1Rdif7GdowCgYIKoZIzj0EAwIDRwAwRAIgfOt69wD8
          HEqroGm/zVFf/NiqivluaK5Yf3Ryn0C7p5ECID/KNGjbt5b53ivuL5slK5B+8eA2
          KGUN7ysBzX8hTzPj
          -----END CERTIFICATE-----
          
    httpOptions:
      verify: false

上述操作已打包至 5_GenConnectYaml.sh 中,也可以直接在根目錄下執行 5_GenConnectYaml.sh 來了生成連線配置檔案。

客戶端程式碼

  1. 初始化目錄/檔案
    在實驗根目錄 6_ContractGatewayAndSDK 下建立目錄 contract-sdk 作為 fabric-sdk 客戶端的根目錄,並在其下建立主程式 app.go 。將上節生成的 connection-soft.yaml 複製到該目錄下,最終目錄結構為:
     contract-sdk
     ├── app.go
     ├── connection-soft.yaml
     ├── go.mod
     ├── go.sum
     ├── keystore
     └── wallet
         └── appUser.id
    
  2. 向 app.go 寫入以下內容
     package main
    
     import (
         "fmt"
         "io/ioutil"
         "log"
         "os"
         "path/filepath"
    
         "github.com/hyperledger/fabric-sdk-go/pkg/core/config"
         "github.com/hyperledger/fabric-sdk-go/pkg/gateway"
     )
    
     func main() {
         log.Println("============ application-golang starts ============")
    
         err := os.Setenv("DISCOVERY_AS_LOCALHOST", "true")
         if err != nil {
             log.Fatalf("Error setting DISCOVERY_AS_LOCALHOST environemnt variable: %v", err)
         }
    
         wallet, err := gateway.NewFileSystemWallet("wallet")
         if err != nil {
             log.Fatalf("Failed to create wallet: %v", err)
         }
    
         err = populateWallet(wallet)
         // 除錯建議註釋這裡
         // if !wallet.Exists("appUser") {
         // 	err = populateWallet(wallet)
         // 	if err != nil {
         // 		log.Fatalf("Failed to populate wallet contents: %v", err)
         // 	}
         // }
    
         ccpPath := filepath.Join(
             "connection-soft.yaml",
         )
    
         gw, err := gateway.Connect(
             gateway.WithConfig(config.FromFile(filepath.Clean(ccpPath))),
             gateway.WithIdentity(wallet, "appUser"),
         )
         if err != nil {
             log.Fatalf("Failed to connect to gateway: %v", err)
         }
         defer gw.Close()
         
         network, err := gw.GetNetwork("testchannel")
         if err != nil {
             log.Fatalf("Failed to get network: %v", err)
         }
         
         contract := network.GetContract("basic")
    
         log.Println("--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger")
         result, err := contract.EvaluateTransaction("GetAllProjects")
         if err != nil {
             log.Fatalf("Failed to evaluate transaction: %v", err)
         }
         log.Println(string(result))
    
         log.Println("--> Submit Transaction: DeleteProject, delete new project info with ID arguments")
         result, err = contract.SubmitTransaction("DeleteProject", "FA8B31A55CD59DB352BCBF4D2AE791AD")
         if err != nil {
             log.Fatalf("Failed to Submit transaction: %v", err)
         }
         log.Println(string(result))
     }
    
     func populateWallet(wallet *gateway.Wallet) error {
         log.Println("============ Populating wallet ============")
         credPath := filepath.Join(
             "..",
             "orgs",
             "soft.ifantasy.net",
             "registers",
             "user1",
             "msp",
         )
    
         certPath := filepath.Join(credPath, "signcerts", "cert.pem")
         // read the certificate pem
         cert, err := ioutil.ReadFile(filepath.Clean(certPath))
         if err != nil {
             return err
         }
    
         keyDir := filepath.Join(credPath, "keystore")
         // there's a single file in this dir containing the private key
         files, err := ioutil.ReadDir(keyDir)
         if err != nil {
             return err
         }
         if len(files) != 1 {
             return fmt.Errorf("keystore folder should have contain one file")
         }
         keyPath := filepath.Join(keyDir, files[0].Name())
         key, err := ioutil.ReadFile(filepath.Clean(keyPath))
         if err != nil {
             return err
         }
    
         identity := gateway.NewX509Identity("softMSP", string(cert), string(key))
    
         return wallet.Put("appUser", identity)
     }
    

客戶端演示

如無特殊說明,以下命令預設執行於實驗根目錄 contract-sdk 下:

  1. 初始化模組
    go mod init github.com/wefantasy/FabricLearn/6_ContractGatewayAndSDK/contract-gateway
    
  2. 下載依賴
    go get
    
  3. 執行客戶端
    go run .
    
    執行SDK客戶端

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]

相關文章