在這個教程中,我們將介紹如何使用fabric-config庫進行通道配置的更新。我們將提供一個基於fabric-config的示例程序,該程序可以更改HyperledgerFabric的塊切割參數。此外,教程還包括如何開始使用fabric-config庫和函數的指南,以便你可以在項目中快速增加通道配置功能。
用熟悉的語言和OS學習Hyperledger Fabric區塊鏈開發:
Node.js | Java | Golang | Python | BYFN Windows版 | WIZ快速開發工具箱 | 鏈碼Python開發包
1、通道配置概述
Hyperledger Fabric網絡由一些數據結構和過程構成,它們以及定義瞭如何與區塊鏈網絡進行交互。其中,數據結構包括組織、對等節點、身份憑證、排序節點和CA等。
標識數據結構及其相應過程(即用於網絡交互的管理指令)的數據包含在通道配置中。這些配置又可以在已提交給通道帳本的區塊中找到。因此,用於修改通道配置的過程稱為配置更新事務。下面列舉了有關通道配置更新的一些常見需求:
- 更新一個區塊中可以包含的最大交易數量。
- 更新在第一個交易到達之後切割區塊之前需要繼續等待交易的時間。
- 更新Raft排序服務參數。
- 更新區塊簽名的有效性要求。
- 將新組織添加到已有的通道。
- 將新組織添加到已經建立的聯盟。
2、更新通道配置
迄今為止,官方推薦的更新通道配置的方法還是使用configtxlator 和jq工具。使用這些工具更新通道配置時的步驟可以概括如下:
- 獲取通道的最新配置區塊。
- 將最新的配置區塊從protobuf格式解碼為JSON。
- 使用jq從JSON配置區塊中刪除不必要的元數據。
- 創建JSON配置區塊的副本。
- 對複製的JSON配置區塊進行相應的更新(例如,更新一個塊允許的最大交易數)。
- 將JSON更新的配置區塊重新編碼為protobuf格式。
- 計算兩個protobuf配置(即原始配置塊和更新的配置塊)之間的差異。這會生成包含增量配置的protobuf數據。
- 將增量數據解碼回JSON。
- 將必要的頭數據添加到JSON增量中,以便使用jq將其包裝在信封消息中。
- 將JSON增量編碼為protobuf。
- 簽名配置更新交易。
- 通過對等節點將配置更新交易提交給排序服務。
儘管上述方法可行,但它非常繁瑣且容易出錯。例如,很容易忘記在解碼最新的配置塊後剝離頭數據(步驟3)或將頭數據添加到增量塊(步驟9)。另外,第5步很容易搞砸。在使用文本編輯器(例如Visual Studio或Atom)或使用jq手動編輯JSON塊時,可能會在JSON文檔的錯誤部分進行修改從而引入錯誤。另外,理想情況下,在對JSON塊進行任何更改之前,你應該對JSON模式有透徹的瞭解,但現實情況是,並不是每個人都擁有這一知識。因此,我們希望有一種不易出錯並且更加簡單的更新信道配置的機制。更好的方案井蓋使用類型安全且經過編譯的語言,例如Go。在下一節中,我們將介紹這種新機制。
值得一提的是,我們鼓勵用戶使用下面詳細介紹的config更新過程來開發自己的工具,並最終棄用configtxlator工具。這也是為什麼你應該開始熟悉用於更新通道配置的最新機制的另一個原因。
注意:提供使用configtxlator和jq工具的底層詳細信息和說明超出了本文的範圍。有關此操作的完整詳細信息,請參見更新通道配置。
3、使用fabric-config庫編輯通道配置
3.1 引入fabric-config庫
Hyperledger fabric-config庫引入了一種用於生成配置交易更新的替代方法,該方法消除了前面描述的手動JSON解析過程中所需的許多繁瑣且容易出錯的步驟。fabric-config庫被設計為獨立的庫,它支持生成諸如應用程序和系統信道創建、通道配置更新操作等,並將背書籤名附加到交易信封消息。
fabric-config庫是用Go編寫的,並提供了豐富的API 用於修改所提供的配置交易,以及計算現有配置和所需更新之間的增量(類似於configtxlator工具的功能)。
3.2 使用fabric-config庫更新通道配置
請注意,fabric-config庫不包含獲取配置塊的功能,你可以自己決定如何獲取配置塊以及如何向網絡提交配置更新交易。例如,你可以選擇使用Fabric Peer CLI(Hyperledger Fabric二進制文件的一部分)
從通道中獲取最新的配置塊。如果你正在利用IBM區塊鏈平臺,那麼還可以利用Ansible的IBM區塊鏈
平臺集合從通道中獲取最新的配置塊。
從相應的通道中獲取最新的配置塊後,可以使用Go語言編寫如下代碼,將該配置塊讀入內存:
import (
...
cb "github.com/hyperledger/fabric-protos-go/common"
...
)
func getConfigFromBlock(blockPath string) *cb.Config {
blockBin, err := ioutil.ReadFile(blockPath)
if err != nil {
panic(err)
}
block := &cb.Block {}
err = proto.Unmarshal(blockBin, block)
if err != nil {
panic(err)
}
blockDataEnvelope := &cb.Envelope {}
err = proto.Unmarshal(block.Data.Data[0], blockDataEnvelope)
if err != nil {
panic(err)
}
blockDataPayload := &cb.Payload {}
err = proto.Unmarshal(blockDataEnvelope.Payload, blockDataPayload)
if err != nil {
panic(err)
}
config := &cb.ConfigEnvelope {}
err = proto.Unmarshal(blockDataPayload.Data, config)
if err != nil {
panic(err)
} return config.Config
}
getConfigFromBlock()
函數從指定的路徑讀取先前獲取的塊,並返回指向該Config結構實例的指針。上面的函數是完全通用的,這意味著無論你打算進行什麼配置更新,都可以在代碼中使用此函數來讀取配置塊。注意,Config實例封裝了配置塊中包含的數據,格式為配置交易protobuf類型。另外,請注意,這個Config不是fabric-config庫中定義的結構。Configprotobuf是在fabric-protos-go模塊中定義的。
將配置塊讀入內存後,就可以創建ConfigTx實例了,如下所示:
import (
...
"github.com/hyperledger/fabric-config/configtx"
...
)
...
baseConfig := getConfigFromBlock(blockPath)
configTx := configtx.New(baseConfig)
...
configtx.New()
函數返回ConfigTx結構的實例,該實例在fabric-config庫中定義。ConfigTx實例是應用程序代碼用於對配置塊進行必要更新的主要入口點。請注意,要獲取ConfigTx實例,你需要提供使用getConfigFromBlock()
方法讀取的配置交易protobuf結構作為參數。現在讓我們展示如何利用ConfigTx結構來對配置塊進行一些更新。具體來說,我們將更改以下塊切割參數:
- absolute_max_bytes:塊最大字節數,即任何區塊都不應大於absolute_max_bytes。
- max_message_count:區塊可以包含的最大交易數,即一個區塊的交易數不應超過max_message_count。
- preferred_max_bytes:塊的首選大小,即如果可以在preferred_max_bytes下構造一個塊,則將盡早切割一個塊,大於該尺寸的交易將出現在另一個塊中。
- batch_timeout:在第一個交易到達之後,在切割區塊之前需要等待其他交易的時間。
注意:如果需要有關上述塊切割參數的更多詳細信息,請參閱Hyperledger Fabric官方文檔中的更新通道配置部分。
現在讓我們定義一組變量來捕獲上述參數:
var (
batchSizeMaxMessage uint32
batchSizeAbsoluteMax uint32
batchSizePreferredMax uint32
batchTimeout uint32
)
在代碼中,你可以為這些變量分別賦值。例如,可以從屬性文件中讀取它們,也可以將這些值作為運行時參數傳遞給程序。無論如何嚮應用程序提供此類值,讀取後就可以將它們分配給以下變量:
batchSizeMaxMessage = ...
batchSizeAbsoluteMax = ...
batchSizePreferredMax = ...
batchTimeout = ...
完成此操作後,就可以繼續使用fabric-config庫中的以下API方法來更新配置塊:
// Obtain OrdererGroup instance from ConfigTx instance
ordererGrp := configTx.Orderer()
// Use setter methods in the OrdererGroup instance to make configuration changes
ordererGrp.SetBatchTimeout(time.Second * time.Duration(batchTimeout))
ordererGrp.BatchSize().SetAbsoluteMaxBytes(batchSizeAbsoluteMax)
ordererGrp.BatchSize().SetMaxMessageCount(batchSizeMaxMessage)
ordererGrp.BatchSize().SetPreferredMaxBytes(batchSizePreferredMax)
對塊進行配置更新後,即可計算這些更改的增量:
var (
channelName string
)
...
configUpdateBytes, err := configTx.ComputeMarshaledUpdate(channelName)
...
在計算完增量之後,下一個可選的步驟是簽名要進行的區塊更新。在這樣做之前,讓我們介紹下如何使用getSigningIdentity()
函數來解析從本地MSP圖區的身份信息:
func getSigningIdentity(sigIDPath string) *configtx.SigningIdentity {
// Read certificate, private key and MSP ID from sigIDPath
var (
certificate *x509.Certificate
privKey crypto.PrivateKey
mspID string
err error
)
mspUser := filepath.Base(sigIDPath)
certificate, err = readCertificate(filepath.Join(sigIDPath, "msp", "signcerts", fmt.Sprintf("%s-cert.pem", mspUser)))
if err != nil {
panic(err)
}
privKey, err = readPrivKey(filepath.Join(sigIDPath, "msp", "keystore", "priv_sk"))
if err != nil {
panic(err)
}
mspID = strings.Split(mspUser, "@")[1] return &configtx.SigningIdentity{
Certificate: certificate,
PrivateKey: privKey,
MSPID: mspID,
}
}
以下是上述功能中使用的輔助方法。首先,讓我們定義readCertificate()
方法:
func readCertificate(certPath string) (*x509.Certificate, error) {
certBytes, err := ioutil.ReadFile(certPath)
if err != nil {
return nil, err
}
pemBlock, _ := pem.Decode(certBytes)
if pemBlock == nil {
return nil, fmt.Errorf("no PEM data found in cert[% x]", certBytes)
}
return x509.ParseCertificate(pemBlock.Bytes)
}
然後定義readPrivKey()
方法:
func readPrivKey(keyPath string) (crypto.PrivateKey, error) {
privKeyBytes, err := ioutil.ReadFile(keyPath)
if err != nil {
return nil, err
}
pemBlock, _ := pem.Decode(privKeyBytes)
if pemBlock == nil {
return nil, fmt.Errorf("no PEM data found in private key[% x]", privKeyBytes)
}
return x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
}
就像getConfigFromBlock()
方法一樣,getSigningIdentity()
也可用於任何類型的方案。因此,你可以將這個getSigningIdentity()
功能添加到你的應用程序代碼中並加以利用,而無需進行任何配置更新。getSigningIdentity()
方法的唯一參數是指向MSP根文件夾的路徑,該文件夾應具有一組子文件夾,其中包含MSP組織用戶或管理員的相應證書和密鑰。MSP根文件夾的名稱應遵循以下命名約定:@。例如,由OrdererMSP標識的Orderer組織的Admin用戶的身份材料應位於名為Admin@OrdererMSP
的文件夾下。在子文件夾下找到的證書和密鑰的名稱應如下所示:
<enrollment_id>@<MSP ID> // sigIDPath
└── msp
├── admincerts
│ │ // The public cert for the org administrator
│ └── admin-cert.pem
├── cacerts
│ │ // The public cert for the root CA
│ └── ca-cert.pem
├── tlscacerts
│ │ // The public cert for the root TLS CA
│ └── tlsca-cert.pem
├── keystore
│ │ // The private key for the identity
│ └── priv_sk
└── signcerts
│ // The public cert for the identity
└── <enrollment_id>@<MSP ID>-cert.pem
如果你使用過cryptogen工具,那麼上面顯示的文件夾結構應該看起來很熟悉。
請注意,getSigningIdentity()
方法返回指向configtx.SigningIdentity結構實例的指針,該實例也在fabric-config庫中定義。
對於每個應該簽名通道配置更新的身份,都應該調用getSigningIdentity()
方法。可以將調用此方法返回的身份標識存儲在數組中。一旦擁有用於簽名配置更新的所有必需身份,就可以使用configtx.SigningIdentity結構的CreateConfigSignature()
方法來創建相應的簽名:
configSignatures := []*cb.ConfigSignature{}
...
signingIdentity := getSigningIdentity(pathToSigningIdentity)
...
configSignature, err := signingIdentity.CreateConfigSignature(configUpdateBytes)
...
configSignatures = append(configSignatures, configSignature)
CreateConfigSignature
方法將我們之前通過調用ComputeMarshaledUpdate()
函數計算出的增量作為參數,即configUpdateBytes。
生成必要的簽名後,可以使用以下方法創建信封消息,其中包含配置更新以及簽名:
env, err := configtx.NewEnvelope(configUpdateBytes, configSignatures...)
就像CreateConfigSignature, configUpdateBytes從ComputeMarshaledUpdate()函數調用中返回的配置增量一樣,configSignatures數組則包含所有必要的簽名(即,指向ConfigSignature結構實例的指針)。
對於我們在本文中討論的示例情況(即,切割參數的更改),只需要訂購服務組織的管理員的簽名。
你可能還希望使用將交易提交到排序節點的身份對從NewEvelope()函數返回的信封消息進行簽名。在我們的示例中,此身份也是排序服務機構的管理員。你還可以通過調用getSigningIdentity()方法來獲得此標識實例,正如我們已經提到的,該方法返回該configtx.SigningIdentity結構的實例:
envelopeSigningIdentity := getSigningIdentity(pathToEnvelopeSigningIdentity)
err = envelopeSigningIdentity.SignEnvelope(env)
最後,我們將簽名的信封寫入文件系統:
envelopeBytes, err := proto.Marshal(env)
if err != nil {
panic(err)
}
err = ioutil.WriteFile(outputPath, envelopeBytes, 0640)
現在,你有了一個配置更新交易,其中包含更改和[簽名],可以將其提交給網絡進行處理!作為參考,下面是示例程序的完整源代碼:
package main
import (
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"time"
"github.com/golang/protobuf/proto"
"github.com/hyperledger/fabric-config/configtx"
cb "github.com/hyperledger/fabric-protos-go/common"
)
func main() {
var (
batchSizeMaxMessage uint32
batchSizeAbsoluteMax uint32
batchSizePreferredMax uint32
batchTimeout uint32
blockPath string
channelName string
pathToSigningIdentity string
pathToEnvelopeSigningIdentity string
outputPath string
)
// Update variables as needed for your use case
batchSizeMaxMessage = 10
batchSizeAbsoluteMax = 103809024
batchSizePreferredMax = 524288
batchTimeout = 4
blockPath = "<blockPath>"
channelName = "<channelName>"
pathToSigningIdentity = "<pathToSigningIdentity>"
pathToEnvelopeSigningIdentity = "<pathToEnvelopeSigningIdentity>"
outputPath = "<outputPath>"
// Read configuration block into memory
baseConfig := getConfigFromBlock(blockPath)
configTx := configtx.New(baseConfig)
// Obtain OrdererGroup instance from ConfigTx instance
ordererGrp := configTx.Orderer()
// Use setter methods in the OrdererGroup instance to make configuration changes
ordererGrp.SetBatchTimeout(time.Second * time.Duration(batchTimeout))
ordererGrp.BatchSize().SetAbsoluteMaxBytes(batchSizeAbsoluteMax)
ordererGrp.BatchSize().SetMaxMessageCount(batchSizeMaxMessage)
ordererGrp.BatchSize().SetPreferredMaxBytes(batchSizePreferredMax)
// Compute delta
configUpdateBytes, err := configTx.ComputeMarshaledUpdate(channelName)
if err != nil {
panic(err)
}
// Attach signature
signingIdentity := getSigningIdentity(pathToSigningIdentity)
configSignature, err := signingIdentity.CreateConfigSignature(configUpdateBytes)
if err != nil {
panic(err)
}
// Create envelope
env, err := configtx.NewEnvelope(configUpdateBytes, configSignature)
// Sign envelope
envelopeSigningIdentity := getSigningIdentity(pathToEnvelopeSigningIdentity)
err = envelopeSigningIdentity.SignEnvelope(env)
envelopeBytes, err := proto.Marshal(env)
if err != nil {
panic(err)
}
// Write envelope to file system
err = ioutil.WriteFile(outputPath, envelopeBytes, 0640)
}
func getConfigFromBlock(blockPath string) *cb.Config {
blockBin, err := ioutil.ReadFile(blockPath)
if err != nil {
panic(err)
}
block := &cb.Block{}
err = proto.Unmarshal(blockBin, block)
if err != nil {
panic(err)
}
blockDataEnvelope := &cb.Envelope{}
err = proto.Unmarshal(block.Data.Data[0], blockDataEnvelope)
if err != nil {
panic(err)
}
blockDataPayload := &cb.Payload{}
err = proto.Unmarshal(blockDataEnvelope.Payload, blockDataPayload)
if err != nil {
panic(err)
}
config := &cb.ConfigEnvelope{}
err = proto.Unmarshal(blockDataPayload.Data, config)
if err != nil {
panic(err)
}
return config.Config
}
func getSigningIdentity(sigIDPath string) *configtx.SigningIdentity {
// Read certificate, private key and MSP ID from sigIDPath
var (
certificate *x509.Certificate
privKey crypto.PrivateKey
mspID string
err error
)
mspUser := filepath.Base(sigIDPath)
certificate, err = readCertificate(filepath.Join(sigIDPath, "msp", "signcerts", fmt.Sprintf("%s-cert.pem", mspUser)))
if err != nil {
panic(err)
}
privKey, err = readPrivKey(filepath.Join(sigIDPath, "msp", "keystore", "priv_sk"))
if err != nil {
panic(err)
}
mspID = strings.Split(mspUser, "@")[1]
return &configtx.SigningIdentity{
Certificate: certificate,
PrivateKey: privKey,
MSPID: mspID,
}
}
func readCertificate(certPath string) (*x509.Certificate, error) {
certBytes, err := ioutil.ReadFile(certPath)
if err != nil {
return nil, err
}
pemBlock, _ := pem.Decode(certBytes)
if pemBlock == nil {
return nil, fmt.Errorf("no PEM data found in cert[% x]", certBytes)
}
return x509.ParseCertificate(pemBlock.Bytes)
}
func readPrivKey(keyPath string) (crypto.PrivateKey, error) {
privKeyBytes, err := ioutil.ReadFile(keyPath)
if err != nil {
return nil, err
}
pemBlock, _ := pem.Decode(privKeyBytes)
if pemBlock == nil {
return nil, fmt.Errorf("no PEM data found in private key[% x]", privKeyBytes)
}
return x509.ParsePKCS8PrivateKey(pemBlock.Bytes)
}
4、注意事項
儘管fabric-config庫極大地改進了更新通道配置的過程,但是仍然需要考慮一些注意事項。
儘管您可以使用這個庫來更新任何版本的Hyperledger Fabric的通道配置,但由於不支持某些被棄用的配置項,因此強烈建議你將其用於更新Hyperledger Fabric v2通道或遷移到Hyperledger Fabric v2。
由於fabric-config庫是用Go語言編寫的,因此該庫只能由使用Go語言編寫工具的開發者使用。可以通過創建通用工具(例如命令行界面)來避免這種情況,這些通用工具不直接嵌入目標應用程序中。這種方法將允許在外部使用該庫來生成配置更新交易。或者,可以設置用Go語言編寫的服務器,該服務器用於可從輸入配置塊生成配置交易更新的端點。
Hyperledger Fabric的現有用戶可能已經有穩定的方法來更新配置交易。因此,他們可能不願意修改現有的自動化過程。但是,隨著在通道配置周圍添加新功能,最終將難以維護當前的通道配置方法。無需手動更新零散的解決方法代碼,使用此庫將成為通過簡單擴展來採用新功能的一致方法。
5、結論
如本文所示,Hyperledger fabric-config庫提供了一種可靠的機制來生成配置更新交易,同時消除了手動進行此類更改時出現的機械步驟和易於出錯的步驟。因此,fabric-config庫使你能夠以可靠且一致的方式自動執行配置更新交易。
儘管未在本教程中顯示,fabric-config庫還支持生成用於應用程序和系統通道創建的配置包絡以及修改應用程序和通道功能。用Go語言編寫的fabric-config庫為此類操作提供了類型安全且經過編譯的選項。
我們鼓勵你查看fabric-config庫的官方GoDoc 文檔,以便熟悉其直觀且易於使用的API,並查看其他示例和代碼段。利用本文中共享的指導和功能,你可以立即在下一個項目中利用fabric-config庫!