菜鳥系列Fabric原始碼學習 — committer記賬節點
Fabric 1.4 原始碼分析 committer記賬節點
本文件主要介紹committer記賬節點如何初始化的以及committer記賬節點的功能及其實現。
1. 簡介
記賬節點負責驗證交易和提交賬本,包括公有資料(即區塊資料,包括公共資料和私密資料hash值)與私密資料。在提交賬本前需要驗證交易資料的有效性,包括交易訊息的格式、簽名有效性以及呼叫VSCC驗證訊息的合法性及指定背書策略的有效性,接著通過MVCC檢查讀寫集衝突並標記交易的有效性,最後提交區塊資料到區塊檔案系統,建立索引資訊並儲存到區塊索引資料庫,更新有效交易和私密資料到狀態資料庫,將經過背書節點到有效交易同步到歷史資料庫,並更新隱私資料庫。
2. 記賬節點初始化
首先,每個通道里面的組織的peer節點都是committer記賬節點(則commiter記賬節點初始化肯定和通道操作相關),因此記賬節點初始化肯定是在peer加入通道或者peer啟動時已存在通道的初始化過程中。首先commiter節點主要負責驗證交易和提交賬本。因此實現了以下介面:
// 提交賬本 type Committer interface { // CommitWithPvtData block and private data into the ledger CommitWithPvtData(blockAndPvtData *ledger.BlockAndPvtData) error // GetPvtDataAndBlockByNum retrieves block with private data with given // sequence number GetPvtDataAndBlockByNum(seqNum uint64) (*ledger.BlockAndPvtData, error) // GetPvtDataByNum returns a slice of the private data from the ledger // for given block and based on the filter which indicates a map of // collections and namespaces of private data to retrieve GetPvtDataByNum(blockNum uint64, filter ledger.PvtNsCollFilter) ([]*ledger.TxPvtData, error) // Get recent block sequence number LedgerHeight() (uint64, error) // Gets blocks with sequence numbers provided in the slice GetBlocks(blockSeqs []uint64) []*common.Block // GetConfigHistoryRetriever returns the ConfigHistoryRetriever GetConfigHistoryRetriever() (ledger.ConfigHistoryRetriever, error) // CommitPvtDataOfOldBlocks commits the private data corresponding to already committed block // If hashes for some of the private data supplied in this function does not match // the corresponding hash present in the block, the unmatched private data is not // committed and instead the mismatch inforation is returned back CommitPvtDataOfOldBlocks(blockPvtData []*ledger.BlockPvtData) ([]*ledger.PvtdataHashMismatch, error) // GetMissingPvtDataTracker return the MissingPvtDataTracker GetMissingPvtDataTracker() (ledger.MissingPvtDataTracker, error) // Closes committing service Close() }
// 驗證交易到合法性,包括交易格式的合法性、背書策略的有效性(vscc) type Validator interface { Validate(block *common.Block) error } // private interface to decouple tx validator // and vscc execution, in order to increase // testability of TxValidator type vsccValidator interface { VSCCValidateTx(seq int, payload *common.Payload, envBytes []byte, block *common.Block) (error, peer.TxValidationCode) }
那麼commiter模組功能是何時初始化的呢?core/peer/peer.go檔案中的createChain()函式。(peer建立通道和peer啟動時都會呼叫改函式)
func createChain(cid string, ledger ledger.PeerLedger, cb *common.Block, ccp ccprovider.ChaincodeProvider, sccp sysccprovider.SystemChaincodeProvider, pm txvalidator.PluginMapper) error {
...
// 構建新的驗證鏈碼支援物件
vcs := struct {
*chainSupport
*semaphore.Weighted
}{cs, validationWorkersSemaphore}
// 建立交易驗證器
validator := txvalidator.NewTxValidator(cid, vcs, sccp, pm)
// 建立賬本提交器
c := committer.NewLedgerCommitterReactive(ledger, func(block *common.Block) error {
chainID, err := utils.GetChainIDFromBlock(block)
if err != nil {
return err
}
return SetCurrConfigBlock(block, chainID)
})
ordererAddresses := bundle.ChannelConfig().OrdererAddresses()
if len(ordererAddresses) == 0 {
return errors.New("no ordering service endpoint provided in configuration block")
}
// TODO: does someone need to call Close() on the transientStoreFactory at shutdown of the peer?
// 建立Transient隱私資料儲存物件
store, err := TransientStoreFactory.OpenStore(bundle.ConfigtxValidator().ChainID())
if err != nil {
return errors.Wrapf(err, "[channel %s] failed opening transient store", bundle.ConfigtxValidator().ChainID())
}
csStoreSupport := &CollectionSupport{
PeerLedger: ledger,
}
simpleCollectionStore := privdata.NewSimpleCollectionStore(csStoreSupport)
// 初始化指定通道的gossip模組
service.GetGossipService().InitializeChannel(bundle.ConfigtxValidator().ChainID(), ordererAddresses, service.Support{
Validator: validator,
Committer: c,
Store: store,
Cs: simpleCollectionStore,
IdDeserializeFactory: csStoreSupport,
})
chains.Lock()
defer chains.Unlock()
//放入chain map中
chains.list[cid] = &chain{
cs: cs,
cb: cb,
committer: c,
}
return nil
}
3. 呼叫committer模組
本節主要介紹交易如何呼叫committer模組,即寫區塊流程。根據區塊同步可知,最後區塊傳輸流程為通過addPayload()函式將區塊寫入gossip.payloadbuff中,然後觸發協程go deliverPayloads(),在裡面呼叫了commitBlock()方法實現寫區塊過程。
func (s *GossipStateProviderImpl) commitBlock(block *common.Block, pvtData util.PvtDataCollections) error {
// 1、儲存區塊
if err := s.ledger.StoreBlock(block, pvtData); err != nil {
logger.Errorf("Got error while committing(%+v)", errors.WithStack(err))
return err
}
// 2、更新區塊高度
s.mediator.UpdateLedgerHeight(block.Header.Number+1, common2.ChainID(s.chainID))
return nil
}
其中PvtDataCollections:
type PvtDataCollections []*ledger.TxPvtData
type TxPvtData struct {
// 在區塊的序號
SeqInBlock uint64
// 寫集
WriteSet *rwset.TxPvtReadWriteSet
}
在同步區塊中,介紹到leader和orderer同步區塊,peer pull區塊以及leader push區塊。但是leader和orderer同步區塊時私密資料集PvtDataCollections=nil。
StoreBlock()函式
主要完成區塊和私密資料的儲存
// StoreBlock stores block with private data into the ledger
func (c *coordinator) StoreBlock(block *common.Block, privateDataSets util.PvtDataCollections) error {
// 對data和header驗證
if block.Data == nil {
return errors.New("Block data is empty")
}
if block.Header == nil {
return errors.New("Block header is nil")
}
// 對交易進行驗證,包括呼叫vscc鏈碼
err := c.Validator.Validate(block)
c.reportValidationDuration(time.Since(validationStart))
blockAndPvtData := &ledger.BlockAndPvtData{
Block: block,
PvtData: make(ledger.TxPvtDataMap),
MissingPvtData: make(ledger.TxMissingPvtDataMap),
}
// 獲取該區塊上交易相關的私密資料集
ownedRWsets, err := computeOwnedRWsets(block, privateDataSets)
// 標識丟失的私密資料讀寫集,並嘗試從本地瞬時資料庫中檢索它們
privateInfo, err := c.listMissingPrivateData(block, ownedRWsets)
for len(privateInfo.missingKeys) > 0 && time.Now().Before(limit) {
// 從其他peer節點獲取缺失的私密資料
c.fetchFromPeers(block.Header.Number, ownedRWsets, privateInfo)
}
// populate the private RWSets passed to the ledger
// 填充私密資料讀寫集
for seqInBlock, nsRWS := range ownedRWsets.bySeqsInBlock() {
rwsets := nsRWS.toRWSet()
// 構造blockAndPvtData結構中的私密資料
blockAndPvtData.PvtData[seqInBlock] = &ledger.TxPvtData{
SeqInBlock: seqInBlock,
WriteSet: rwsets,
}
}
// populate missing RWSets to be passed to the ledger
// 構造缺失的私密資料
for missingRWS := range privateInfo.missingKeys {
blockAndPvtData.MissingPvtData.Add(missingRWS.seqInBlock, missingRWS.namespace, missingRWS.collection, true)
}
// populate missing RWSets for ineligible collections to be passed to the ledger
for _, missingRWS := range privateInfo.missingRWSButIneligible {
blockAndPvtData.MissingPvtData.Add(missingRWS.seqInBlock, missingRWS.namespace, missingRWS.collection, false)
}
// commit block and private data
// 寫賬本
err = c.CommitWithPvtData(blockAndPvtData)
if len(blockAndPvtData.PvtData) > 0 {
// Finally, purge all transactions in block - valid or not valid.
if err := c.PurgeByTxids(privateInfo.txns); err != nil {
logger.Error("Purging transactions", privateInfo.txns, "failed:", err)
}
}
seq := block.Header.Number
if seq%c.transientBlockRetention == 0 && seq > c.transientBlockRetention {
err := c.PurgeByHeight(seq - c.transientBlockRetention)
if err != nil {
logger.Error("Failed purging data from transient store at block", seq, ":", err)
}
}
return nil
}
上述流程為:
- 驗證區塊頭和區塊資料的有效性
- 驗證交易的合法性以及vscc驗證背書策略的有效性
- 處理私密資料
- 過濾該區塊存在的隱私資料讀寫集
- 計算本地缺失的私密資料資訊
- 從其他節點獲取缺失的私密資料資訊
- 寫區塊和私密資料
4. 驗證交易的合法性以及vscc驗證背書策略的有效性
- Validate(block *common.Block)
主要是該方法實現驗證過程。
func (v *TxValidator) Validate(block *common.Block) error {
.....
// 額外開啟一個協程,針對區塊裡面每一個交易進行驗證
results := make(chan *blockValidationResult)
go func() {
for tIdx, d := range block.Data.Data {
// ensure that we don't have too many concurrent validation workers
v.Support.Acquire(context.Background(), 1)
go func(index int, data []byte) {
defer v.Support.Release(1)
// 驗證交易
v.validateTx(&blockValidationRequest{
d: data,
block: block,
tIdx: index,
}, results)
}(tIdx, d)
}
}()
// 對驗證結果進行處理
for i := 0; i < len(block.Data.Data); i++ {
res := <-results
if res.err != nil {
...
} else {
// 設定交易狀態碼
txsfltr.SetFlag(res.tIdx, res.validationCode)
// 如果交易是有效的
if res.validationCode == peer.TxValidationCode_VALID {
// 設定鏈碼名
if res.txsChaincodeName != nil {
txsChaincodeNames[res.tIdx] = res.txsChaincodeName
}
// 設定升級鏈碼名
if res.txsUpgradedChaincode != nil {
txsUpgradedChaincodes[res.tIdx] = res.txsUpgradedChaincode
}
// 設定交易id
txidArray[res.tIdx] = res.txid
}
}
}
// 如果存在重複交易,則設定該交易無效TxValidationCode_DUPLICATE_TXID,防止雙花攻擊
if v.Support.Capabilities().ForbidDuplicateTXIdInBlock() {
markTXIdDuplicates(txidArray, txsfltr)
}
// 防止多次重複升級鏈碼
v.invalidTXsForUpgradeCC(txsChaincodeNames, txsUpgradedChaincodes, txsfltr)
utils.InitBlockMetadata(block)
// 設定區塊交易索引
block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] = txsfltr
return nil
}
總結以下流程:
此處主要是完成交易驗證及背書策略合法性驗證。
1. 開啟一個協程驗證區塊裡面的交易,並且在該協程為每個交易開啟一個協程進行交易驗證
2. 對驗證結果進行處理,即設定每個交易的交易碼以及新增鏈碼名、添加升級鏈碼名
3. 判斷是否存在重複交易,將重複交易交易碼設定為TxValidationCode_DUPLICATE_TXID
4. 對多次鏈碼升級的無效交易進行處理,此處將交易碼設定為TxValidationCode_CHAINCODE_VERSION_CONFLICT
5. 在區塊的Metadata.Metadata設定交易索引
- validateTx(req blockValidationRequest, results chan<- blockValidationResult)
該函式主要是驗證每個交易的有效性以及背書策略的合法性,傳入的引數為blockValidationRequest以及results,經過該方法驗證後,將驗證結果寫入results通道
type blockValidationRequest struct {
// 區塊
block *common.Block
// 交易資料
d []byte
// 交易在區塊的序號
tIdx int
}
主要流程包括如下:
1. 首先呼叫validation.ValidateTransaction()驗證交易格式、簽名以及是否被篡改
2. 通過交易的payload.header獲取通道id,判斷該通道是否存在。
3. 根據交易型別進行分類處理
+ HeaderType_ENDORSER_TRANSACTION:經過背書節點背書的交易
1. 通過交易id判斷交易的唯一性,檢查賬本是否存在相同的交易id(重放攻擊)
2. 接著通過呼叫VSCCValidateTx驗證交易背書籤名是否符合對應的背書策略
3. 呼叫v.getTxCCInstance(payload)獲取該交易呼叫的鏈碼
+ HeaderType_CONFIG:通道配置交易
1. 呼叫介面configtx.UnmarshalConfigEnvelope(payload.Data)獲取配置交易資訊configEnvelope
2. 呼叫介面v.Support.Apply(configEnvelope)更新配置,具體實現fabric/core/peer/peer.go
+ 未知的訊息型別
4. 將交易寫入results通道中返回,其中合法和不合法的交易構造的blockValidationResult,不合法的只包含(只包含tIdx以及validationCode):
// invalid:
results <- &blockValidationResult{
tIdx: tIdx,
validationCode: peer.TxValidationCode_UNKNOWN_TX_TYPE,
}
// valid:
results <- &blockValidationResult{
tIdx: tIdx,
txsChaincodeName: txsChaincodeName,
txsUpgradedChaincode: txsUpgradedChaincode,
validationCode: peer.TxValidationCode_VALID,
txid: txID,
}
綜上,交易驗證基本流程可以確定,可以分為驗證交易格式、簽名以及是否被篡改以及驗證交易背書籤名是否符合對應的背書策略(HeaderType_ENDORSER_TRANSACTION交易需要驗證)這兩個方面。接下來將分別介紹為驗證交易格式、簽名以及是否被篡改、雙花攻擊以及驗證交易背書籤名是否符合對應的背書策略這兩個介面。
4.1 驗證交易格式、交易真實性與完整性
- ValidateTransaction(e *common.Envelope, c channelconfig.ApplicationCapabilities)
該函式主要功能為驗證交易格式、簽名以及是否被篡改。
主要流程如下:
1. 驗證Envelope交易的格式,其中包括(Envelope是否為nil,Envelope.Payload是否為nil,Envelope.Payload.Header)
2. 驗證簽名是否有效(驗證該訊息的建立者及其簽名是否有效)
3. 根據不同訊息型別進行處理
+ HeaderType_ENDORSER_TRANSACTION
1. 驗證交易id
2. 驗證背書交易是否被篡改
1. 反序列payload.data生成Transaction
2. 驗證Actions.Header的格式(是否為nil,長度是否為0)
3. 反序列化ProposalResponsePayload,驗證proposal hash
+ HeaderType_CONFIG
主要驗證payload.Data, payload.Header是否為nil
+ HeaderType_TOKEN_TRANSACTION
驗證交易id是否一致
4.2 VSCC驗證
- VSCCValidateTx
該函式主要實現對交易vscc驗證
主要流程如下:
1. 解析訊息頭拓展hdrExt以及通道頭chdr,然後通過這兩個資訊驗證鏈碼id和版本是否一致
2. 建立一個名稱空間集合,遍歷交易讀寫集,儲存namespace,例如lscc、mycc,並進行判斷
1. 檢查是否存在lscc名稱空間
2. 檢查是否是不可被其他鏈碼呼叫的系統鏈碼
3. 檢查是否是不可以被外部鏈碼呼叫的系統鏈碼
3. 根據鏈碼 型別進行驗證(應用鏈碼和系統鏈碼)
1. 應用鏈碼
1. 判斷名稱空間是否存在lscc以及不可呼叫系統鏈碼
2. 迴圈遍歷當前寫集合的名稱空間
0. 構造請求從lscc獲取鏈碼id、版本以及背書策略
1. 驗證鏈碼版本
2. vscc背書策略驗證
2. 系統鏈碼
1. 判斷名稱空間是否是不可呼叫系統鏈碼
2. vscc背書策略驗證
- VSCCValidateTxForCC()
該函式主要實現背書策略驗證
VSCCValidateTxForCC()裡面會呼叫ValidateWithPlugin(),呼叫Validate(),預設實現為core/handlers/validation/builtin/default_validation.go/Validate(),首先會對block、txPosition進行校驗。然後根據不同的版本呼叫不同的介面。
switch {
case v.Capabilities.V1_3Validation():
err = v.TxValidatorV1_3.Validate(block, namespace, txPosition, actionPosition, serializedPolicy.Bytes())
case v.Capabilities.V1_2Validation():
fallthrough
default:
err = v.TxValidatorV1_2.Validate(block, namespace, txPosition, actionPosition, serializedPolicy.Bytes())
}
這裡以v1.2版本為例。
func (vscc *Validator) Validate(
block *common.Block,
namespace string,
txPosition int,
actionPosition int,
policyBytes []byte,
) commonerrors.TxValidationError {
// get the envelope
// and the payload...
// validate the payload type
// ...and the transaction...
// 返回去掉重複背書節點身份的簽名集合
signatureSet, err := vscc.deduplicateIdentity(cap)
// evaluate the signature set against the policy
// 背書策略驗證
err = vscc.policyEvaluator.Evaluate(policyBytes, signatureSet)
//如果是lscc,則繼續驗證lscc
// do some extra validation that is specific to lscc
if namespace == "lscc" {
err := vscc.ValidateLSCCInvocation(chdr.ChannelId, env, cap, payl, vscc.capabilities)
}
return nil
}
- 背書策略驗證
// Evaluate takes a set of SignedData and evaluates whether this set of signatures satisfies the policy
func (id *PolicyEvaluator) Evaluate(policyBytes []byte, signatureSet []*common.SignedData) error {
pp := cauthdsl.NewPolicyProvider(id.IdentityDeserializer)
policy, _, err := pp.NewPolicy(policyBytes)
if err != nil {
return err
}
return policy.Evaluate(signatureSet)
}
最後會呼叫compile()返回的驗證方法進行驗證。
此處根據策略型別進行驗證
+ SignaturePolicy_NOutOf_型別策略。
遞迴構造自策略驗證方法compiledPolicy,並放入策略驗證方法集合policies中。然後返回一個方法。在該方法中,會遍歷policys,進行驗證,如果子策略是SignaturePolicy_NOutOf_型別策略,會繼續遞迴呼叫驗證方法,最後直到最底層子策略為SignaturePolicy_SignedBy。如果通過驗證,則verified自增,然後返回驗證通過的個數是否滿足策略要求。
+ SignaturePolicy_SignedBy型別策略
首先驗證簽名索引signedby的合法性。再返回一個方法。該方法遍歷簽名資料列表進行判斷。
1. 跳過已經匹配的身份實體
2. 解析簽名身份實體的identity
3. 驗證identity是否滿足指定簽名策略identity.SatisfiesPrincipal(signedByID)
4. 再驗證identity簽名的真實性
其中,SatisfiesPrincipal會最終呼叫satisfiesPrincipalInternalPreV13()。其中存在多種驗證方式。
1. MSPPrincipal_ROLE 基於角色的驗證
1. 驗證是否為相同的MSP;
2. 驗證是否是有效的證書;
如果是admin,會遍歷MSP裡面的admin身份證書,按位元組比對。如果是peer/client,會驗證組織部門資訊是否匹配
2. MSPPrincipal_IDENTITY 基於身份的驗證
此處主要驗證身份證書是否一致
3. MSPPrincipal_ORGANIZATION_UNIT 基於部門單元的驗證
1. 驗證是否為相同的MSP;
2. 驗證是否是有效的證書;
3. 驗證組織部門資訊是否匹配
- lscc特殊驗證
- 驗證輸入引數的合法性
- 驗證deploy和upgrade的結果讀寫集以及背書策略
5. 寫區塊和私密資料
CommitWithPvtData(blockAndPvtData *ledger.BlockAndPvtData)主要實現寫區塊和私密資料功能。
// CommitWithPvtData commits blocks atomically with private data
func (lc *LedgerCommitter) CommitWithPvtData(blockAndPvtData *ledger.BlockAndPvtData) error {
// Do validation and whatever needed before
// committing new block
if err := lc.preCommit(blockAndPvtData.Block); err != nil {
return err
}
// Committing new block
if err := lc.PeerLedgerSupport.CommitWithPvtData(blockAndPvtData); err != nil {
return err
}
return nil
}
該方法首先會呼叫lc.preCommit(blockAndPvtData.Block)方法對需要提交的區塊資料進行預處理,如果是配置區塊則執行lc.eventer(block),其實現為core/peer/peer.go createChain()方法中:其主要功能為從區塊中解析出通道id,然後呼叫SetCurrConfigBlock()方法,設定本地map[string]*chain,更新該chain最新配置塊。接著會呼叫kvLedger.CommitWithPvtData()方法提交區塊到賬本中。
c := committer.NewLedgerCommitterReactive(ledger, func(block *common.Block) error {
chainID, err := utils.GetChainIDFromBlock(block)
if err != nil {
return err
}
return SetCurrConfigBlock(block, chainID)
})
// SetCurrConfigBlock sets the current config block of the specified channel
func SetCurrConfigBlock(block *common.Block, cid string) error {
chains.Lock()
defer chains.Unlock()
if c, ok := chains.list[cid]; ok {
c.cb = block
return nil
}
return errors.Errorf("[channel %s] channel not associated with this peer", cid)
}
kvLedger.CommitWithPvtData()為提交區塊寫入賬本核心方法,在該流程中,會對交易執行MVCC檢查,判斷讀資料讀有效性、標記交易讀有效性再更新賬本。因此主要分為驗證和準備資料以及提交賬本資料兩個步驟。
5.1 驗證和準備資料
5.1.1 預處理構造內部區塊
kvLedger.CommitWithPvtData()會呼叫l.txtmgmt.ValidateAndPrepare(),最終會呼叫preprocessProtoBlock()進行預處理操作。該方法會將common.Block預處理成internal.Block。internal.Block以及internal.Transaction資料結果如下:
type Block struct {
Num uint64
Txs []*Transaction
}
type Transaction struct {
IndexInBlock int
ID string
RWSet *rwsetutil.TxRwSet
ValidationCode peer.TxValidationCode
}
preprocessProtoBlock():
- 處理Endorser交易:只保留有效的 Endorser 交易;
- 處理配置交易:獲取配置更新的模擬結果,放入讀寫集;
- 檢查讀寫集是否符合資料庫要求格式
5.1.2 執行MVCC檢查與準備公有資料
對交易資料進行MVCC檢查用於驗證交易結果讀寫集的讀集的key版本是否在該交易前是否改變、RangeQuery 的結果未變、私密資料的key的版本是否改變,並標記無效的交易,最後將有效交易的公共資料與私密資料寫集合新增到資料更新批量操作中。
- ValidateAndPrepareBatch()
updates := internal.NewPubAndHashUpdates() // 建立公共資料和私密資料hash值批處理更新操作
for _, tx := range block.Txs { // 遍歷區塊所有交易
var validationCode peer.TxValidationCode
var err error
// 背書交易mvcc驗證
if validationCode, err = v.validateEndorserTX(tx.RWSet, doMVCCValidation, updates); err != nil {
return nil, err
}
tx.ValidationCode = validationCode
// 檢查交易的有效性
if validationCode == peer.TxValidationCode_VALID {
logger.Debugf("Block [%d] Transaction index [%d] TxId [%s] marked as valid by state validator", block.Num, tx.IndexInBlock, tx.ID)
committingTxHeight := version.NewHeight(block.Num, uint64(tx.IndexInBlock))
updates.ApplyWriteSet(tx.RWSet, committingTxHeight, v.db) // 更新寫集合到PubAndHashUpdates結構中
} else {
logger.Warningf("Block [%d] Transaction index [%d] TxId [%s] marked as invalid by state validator. Reason code [%s]",
block.Num, tx.IndexInBlock, tx.ID, validationCode.String())
}
}
MVCC校驗
- 驗證公共資料讀集key
func (v *Validator) validateKVRead(ns string, kvRead *kvrwset.KVRead, updates *privacyenabledstate.PubUpdateBatch) (bool, error) {
if updates.Exists(ns, kvRead.Key) { // 檢視更新批處理,如果存在,則標示該交易使用了同一個區塊上一個交易讀讀集,無效
return false, nil
}
committedVersion, err := v.db.GetVersion(ns, kvRead.Key) // 檢視狀態資料庫已提交的版本
if err != nil {
return false, err
}
if !version.AreSame(committedVersion, rwsetutil.NewVersion(kvRead.Version)) { // 構造單個key讀資料版本,並與已提交版本比較,不一致則返回false
logger.Debugf("Version mismatch for key [%s:%s]. Committed version = [%#v], Version in readSet [%#v]",
ns, kvRead.Key, committedVersion, kvRead.Version)
return false, nil
}
return true, nil
}
其中版本資料結構
type Height struct {
BlockNum uint64
TxNum uint64
}
-- example
"key": "marblesp",
"version": {
"block_num": "5",
"tx_num": "0"
}
驗證範圍查詢
針對公共資料範圍查詢的讀集合進行驗證,迴圈遍歷每個範圍查詢物件,驗證範圍查詢資料的讀資料版本是否一致。- 驗證私密資料讀集key hash
遍歷所有的collHashedRWSets,再遍歷collHashedRWSet.HashedRwSet.HashedReads,驗證每個kvReadHash版本是否一致(類似於key驗證)
func (v *Validator) validateNsHashedReadSets(ns string, collHashedRWSets []*rwsetutil.CollHashedRwSet,
updates *privacyenabledstate.HashedUpdateBatch) (bool, error) {
for _, collHashedRWSet := range collHashedRWSets {
if valid, err := v.validateCollHashedReadSet(ns, collHashedRWSet.CollectionName, collHashedRWSet.HashedRwSet.HashedReads, updates); !valid || err != nil {
return valid, err
}
}
return true, nil
}
func (v *Validator) validateCollHashedReadSet(ns, coll string, kvReadHashes []*kvrwset.KVReadHash,
updates *privacyenabledstate.HashedUpdateBatch) (bool, error) {
for _, kvReadHash := range kvReadHashes {
if valid, err := v.validateKVReadHash(ns, coll, kvReadHash, updates); !valid || err != nil {
return valid, err
}
}
return true, nil
}
5.1.3 驗證與準備私密資料
對私密資料hash值進行校驗,再將更新操作寫入新增到資料更新批量操作中。
func validatePvtdata(tx *internal.Transaction, pvtdata *ledger.TxPvtData) error {
if pvtdata.WriteSet == nil {
return nil
}
for _, nsPvtdata := range pvtdata.WriteSet.NsPvtRwset {
for _, collPvtdata := range nsPvtdata.CollectionPvtRwset {
collPvtdataHash := util.ComputeHash(collPvtdata.Rwset)
hashInPubdata := tx.RetrieveHash(nsPvtdata.Namespace, collPvtdata.CollectionName)
if !bytes.Equal(collPvtdataHash, hashInPubdata) {
return &validator.ErrPvtdataHashMissmatch{
Msg: fmt.Sprintf(`Hash of pvt data for collection [%s:%s] does not match with the corresponding hash in the public data.
public hash = [%#v], pvt data hash = [%#v]`, nsPvtdata.Namespace, collPvtdata.CollectionName, hashInPubdata, collPvtdataHash),
}
}
}
}
return nil
}
5.1.4 更新區塊元資料
更新區塊元資料交易驗證碼列表,本來更新了一次,參見上文,但是在MVCC驗證中還存在驗證不通過的情況,因此再次重新整理交易驗證碼。
func postprocessProtoBlock(block *common.Block, validatedBlock *internal.Block) {
txsFilter := util.TxValidationFlags(block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER])
for _, tx := range validatedBlock.Txs {
txsFilter.SetFlag(tx.IndexInBlock, tx.ValidationCode)
}
block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] = txsFilter
}
5.2 提交賬本資料
提交賬本資料包括以下步驟
1、將區塊資料寫入賬本、更新私密資料庫以及更新區塊索引資料庫
2、更新狀態資料庫
3、更新歷史資料庫
5.2.1 提交區塊和私密資料
- 準備提交私密資料
CommitWithPvtData()會呼叫pvtdataStore.Prepare()介面對私密資料進行處理,再處理過程中,首先會將私密資料轉化為storeEntries結構,再將storeEntries結構的三個欄位分別轉化成KV鍵值對形式,並放入批處理更新操作UpdateBatch中。最後s.db.WriteBatch(batch, true)進行更新私密資料庫操作。
type storeEntries struct {
dataEntries []*dataEntry
expiryEntries []*expiryEntry
missingDataEntries map[missingDataKey]*bitset.BitSet
}
type UpdateBatch struct {
KVs map[string][]byte
}
func (h *DBHandle) WriteBatch(batch *UpdateBatch, sync bool) error {
if len(batch.KVs) == 0 {
return nil
}
levelBatch := &leveldb.Batch{}
for k, v := range batch.KVs {
// key為h.dbName+[]byte{0x00}+[]byte(k)
key := constructLevelKey(h.dbName, []byte(k))
if v == nil {
levelBatch.Delete(key)
} else {
levelBatch.Put(key, v)
}
}
if err := h.db.WriteBatch(levelBatch, sync); err != nil {
return err
}
return nil
}
- 提交區塊資料
本質上是通過(mgr blockfileMgr) addBlock(block common.Block)將區塊寫入區塊檔案系統中,接著呼叫mgr.index.indexBlock(*blockIdxInfo)更新當前區塊資訊到區塊索引資料庫。最後執行mgr.updateCheckpoint(newCPInfo)更新檢查點資訊以及執行mgr.updateBlockchainInfo(blockHash, block)更新區塊鏈資訊。
type blockIdxInfo struct {
blockNum uint64
blockHash []byte
flp *fileLocPointer
txOffsets []*txindexInfo
metadata *common.BlockMetadata
}
- 確認提交私密資料操作
當提交區塊到區塊檔案系統時報錯,則私密資料寫資料庫執行回滾操作,如果沒有問題,執行真正到確認提交操作。
if err := s.AddBlock(blockAndPvtdata.Block); err != nil {
s.pvtdataStore.Rollback()
return err
}
if writtenToPvtStore {
return s.pvtdataStore.Commit()
}
return nil
5.2.2 更新狀態資料庫
if err = l.txtmgmt.Commit(); err != nil {
panic(errors.WithMessage(err, "error during commit to txmgr"))
}
主要實現方法為l.txtmgmt.Commit()
- l.txtmgmt.Commit()
// 準備清理過期到私密資料
if !txmgr.pvtdataPurgeMgr.usedOnce {
txmgr.pvtdataPurgeMgr.PrepareForExpiringKeys(txmgr.current.blockNum())
txmgr.pvtdataPurgeMgr.usedOnce = true
}
defer func() {
txmgr.pvtdataPurgeMgr.PrepareForExpiringKeys(txmgr.current.blockNum() + 1)
logger.Debugf("launched the background routine for preparing keys to purge with the next block")
txmgr.reset()
}()
// 更新私密資料生命週期記錄資料庫,這裡記錄了每個私密鍵值的存活期限
if err := txmgr.pvtdataPurgeMgr.DeleteExpiredAndUpdateBookkeeping(
txmgr.current.batch.PvtUpdates, txmgr.current.batch.HashUpdates); err != nil {
return err
}
// 更新狀態資料庫裡面的公共資料和私密資料
if err := txmgr.db.ApplyPrivacyAwareUpdates(txmgr.current.batch, commitHeight); err != nil {
txmgr.commitRWLock.Unlock()
return err
}
5.2.3 更新歷史資料庫
if ledgerconfig.IsHistoryDBEnabled() {
logger.Debugf("[%s] Committing block [%d] transactions to history database", l.ledgerID, blockNo)
if err := l.historyDB.Commit(block); err != nil {
panic(errors.WithMessage(err, "Error during commit to history db"))
}
}
主要實現方法為l.historyDB.Commit(block)
- l.historyDB.Commit(block)
5.2.4 清理工作
if len(blockAndPvtData.PvtData) > 0 {
// Finally, purge all transactions in block - valid or not valid.
if err := c.PurgeByTxids(privateInfo.txns); err != nil {
logger.Error("Purging transactions", privateInfo.txns, "failed:", err)
}
}
seq := block.Header.Number
if seq%c.transientBlockRetention == 0 && seq > c.transientBlockRetention {
err := c.PurgeByHeight(seq - c.transientBlockRetention)
if err != nil {
logger.Error("Failed purging data from transient store at block", seq, ":", err)
}
}
PurgeByTxids從瞬態儲存中刪除給定交易的私有讀寫集,PurgeByHeight會刪除小於給定maxBlockNumToRetain的塊高度處的私有讀寫集。
6. 附錄
blkrouter提供的一個區塊資訊
{
"data":{
"data":[
{
"payload":{
"data":{
"actions":[
{
"header":{
"creator":{
"id_bytes":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLekNDQWRHZ0F3SUJBZ0lSQVB1TWdsMkJZbS9WMEhCVW1NMFRibVF3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekl1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGt4TWpNd01ETXhOVEF3V2hjTk1qa3hNakkzTURNeE5UQXcKV2pCc01Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFUE1BMEdBMVVFQ3hNR1kyeHBaVzUwTVI4d0hRWURWUVFEREJaQlpHMXBia0J2CmNtY3lMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFT2EzS3hBVVAKQzJIWUlrTlB6akpKbDViY21CbEt6cnVFT2h3VmViWVBYcVdNMFVJRlR0all3U09XNGpiTDNDWUVuSzVBNGJNZwpOcHROTjEvWlJ2elRxNk5OTUVzd0RnWURWUjBQQVFIL0JBUURBZ2VBTUF3R0ExVWRFd0VCL3dRQ01BQXdLd1lEClZSMGpCQ1F3SW9BZyt6QTM3SnhCYzZVbXJtTk5UTG1nb09RRFFaMnFOSVNJQWpxMmp5MUJKSlF3Q2dZSUtvWkkKemowRUF3SURTQUF3UlFJaEFNMW42SDlFY01qb09hYkQ3WVJwQXUwOWp4NWcrMEd2c05qNmFZTElWQ2FUQWlBcApQYlJ6MFo1NFo1a0NDOHhpS0t3d3BNK05jNjhPanRBSWRsQmRmZkM2QlE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"mspid":"Org2MSP"
},
"nonce":"d82/MXTjQoG1RPdyuPxM16TjThX1bJks"
},
"payload":{
"action":{
"endorsements":[
{
"endorser":"CgdPcmcxTVNQEqoGLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLRENDQWMrZ0F3SUJBZ0lSQUpNRFJ4TG5FbUhSVEVKZXowcTVjT293Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpFdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekV1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGt4TWpNd01ETXhOVEF3V2hjTk1qa3hNakkzTURNeE5UQXcKV2pCcU1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFTk1Bc0dBMVVFQ3hNRWNHVmxjakVmTUIwR0ExVUVBeE1XY0dWbGNqQXViM0puCk1TNWxlR0Z0Y0d4bExtTnZiVEJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCR3pYU2pSUUMxZGYKNGFlMHAvSloxNjBPamY2VmZiVHh6RlFOdklSdndKTS9ETnB2UG9qTkVNRGF1V2JPRkFhUjcxK2FMQnhZRkpLbAp0aVVhRGJFcFJ4S2pUVEJMTUE0R0ExVWREd0VCL3dRRUF3SUhnREFNQmdOVkhSTUJBZjhFQWpBQU1Dc0dBMVVkCkl3UWtNQ0tBSUlMaXJ6YzlhdlJ4dW96c3VLSFU2TmJsLzVROGN3alBoTmtxb0QzSTRmc1dNQW9HQ0NxR1NNNDkKQkFNQ0EwY0FNRVFDSUJKcmhNNmZSMXVod3VYbnJPeFVHSXNlVFBoSDZlY0lHbXhGcGRIM2ZhQmxBaUJ5MC9ydQp2NmliMWdqWjdVUzJOdi9tL2dySENCc0gwSEU4Mk5KSm12bnE4dz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
"signature":"MEQCIAzbyxlzFDyEy3y26mqFpQjUfUO+Bsn6nBYxKY2yMvs9AiAObJZgBGuc7LjQcX1o8QArdmLM90XMOJ5t9Id6bYFnDg=="
},
{
"endorser":"CgdPcmcyTVNQEqYGLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNKekNDQWM2Z0F3SUJBZ0lRU3FDL1p5U0lyalNkOW9mL2FlNVNsekFLQmdncWhrak9QUVFEQWpCek1Rc3cKQ1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0JNS1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ4TU5VMkZ1SUVaeQpZVzVqYVhOamJ6RVpNQmNHQTFVRUNoTVFiM0puTWk1bGVHRnRjR3hsTG1OdmJURWNNQm9HQTFVRUF4TVRZMkV1CmIzSm5NaTVsZUdGdGNHeGxMbU52YlRBZUZ3MHhPVEV5TXpBd016RTFNREJhRncweU9URXlNamN3TXpFMU1EQmEKTUdveEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVApZVzRnUm5KaGJtTnBjMk52TVEwd0N3WURWUVFMRXdSd1pXVnlNUjh3SFFZRFZRUURFeFp3WldWeU1DNXZjbWN5CkxtVjRZVzF3YkdVdVkyOXRNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVoZ2tsWlVZbFZKN08KLzlIUXBZSXcvaTdodVBOTU95ejdpT0dzaWFLYTg0K3lyOHo2TzBFdk53Q1p5MjFNOEVENnVUWDdCeHFRL3NDRgo1Z2x5QlgvTG02Tk5NRXN3RGdZRFZSMFBBUUgvQkFRREFnZUFNQXdHQTFVZEV3RUIvd1FDTUFBd0t3WURWUjBqCkJDUXdJb0FnK3pBMzdKeEJjNlVtcm1OTlRMbWdvT1FEUVoycU5JU0lBanEyankxQkpKUXdDZ1lJS29aSXpqMEUKQXdJRFJ3QXdSQUlnUk5FMEZQUTdmM243dWswRUUzQmlEbVE4c1BwdDVNV0taWWlUclJlRkdud0NJRExkVGxXMQptbU5SdkVkdGpIM0xiR0h3UGZndk9vRlBkTzBQU2FOU2haQnEKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
"signature":"MEQCIAOSCpKv3DWb0eWSxwzIt4Y0D9U2dwpgaHDmO6jfyUO4AiApnBZi2kn+z/B0/2S8IuoAJIYGJp+8zG8qwxHKm2/ypQ=="
}
],
"proposal_response_payload":{
"extension":{
"chaincode_id":{
"name":"mycc",
"path":"",
"version":"1.0"
},
"events":null,
"response":{
"message":"",
"payload":null,
"status":200
},
"results":{
"data_model":"KV",
"ns_rwset":[
{
"collection_hashed_rwset":[
],
"namespace":"lscc",
"rwset":{
"metadata_writes":[
],
"range_queries_info":[
],
"reads":[
{
"key":"mycc",
"version":{
"block_num":"3",
"tx_num":"0"
}
}
],
"writes":[
]
}
},
{
"collection_hashed_rwset":[
],
"namespace":"mycc",
"rwset":{
"metadata_writes":[
],
"range_queries_info":[
],
"reads":[
{
"key":"a",
"version":{
"block_num":"3",
"tx_num":"0"
}
},
{
"key":"b",
"version":{
"block_num":"3",
"tx_num":"0"
}
}
],
"writes":[
{
"is_delete":false,
"key":"a",
"value":"OTA="
},
{
"is_delete":false,
"key":"b",
"value":"MjEw"
}
]
}
}
]
},
"token_expectation":null
},
"proposal_hash":"VstSrCFTRwBOoJodpbhtQsJUIFDz5UYKZPq34BmY+lg="
}
},
"chaincode_proposal_payload":{
"TransientMap":{
},
"input":{
"chaincode_spec":{
"chaincode_id":{
"name":"mycc",
"path":"",
"version":""
},
"input":{
"args":[
"aW52b2tl",
"YQ==",
"Yg==",
"MTA="
],
"decorations":{
}
},
"timeout":0,
"type":"GOLANG"
}
}
}
}
}
]
},
"header":{
"channel_header":{
"channel_id":"mychannel",
"epoch":"0",
"extension":"EgYSBG15Y2M=",
"timestamp":"2019-12-30T03:21:19.734584800Z",
"tls_cert_hash":null,
"tx_id":"13eafcea37a6adfdfd2ac6522b35f32697a0334f8c8a74d11df73bbb9f9dc5b5",
"type":3,
"version":0
},
"signature_header":{
"creator":{
"id_bytes":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLekNDQWRHZ0F3SUJBZ0lSQVB1TWdsMkJZbS9WMEhCVW1NMFRibVF3Q2dZSUtvWkl6ajBFQXdJd2N6RUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhHVEFYQmdOVkJBb1RFRzl5WnpJdVpYaGhiWEJzWlM1amIyMHhIREFhQmdOVkJBTVRFMk5oCkxtOXlaekl1WlhoaGJYQnNaUzVqYjIwd0hoY05NVGt4TWpNd01ETXhOVEF3V2hjTk1qa3hNakkzTURNeE5UQXcKV2pCc01Rc3dDUVlEVlFRR0V3SlZVekVUTUJFR0ExVUVDQk1LUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnhNTgpVMkZ1SUVaeVlXNWphWE5qYnpFUE1BMEdBMVVFQ3hNR1kyeHBaVzUwTVI4d0hRWURWUVFEREJaQlpHMXBia0J2CmNtY3lMbVY0WVcxd2JHVXVZMjl0TUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFT2EzS3hBVVAKQzJIWUlrTlB6akpKbDViY21CbEt6cnVFT2h3VmViWVBYcVdNMFVJRlR0all3U09XNGpiTDNDWUVuSzVBNGJNZwpOcHROTjEvWlJ2elRxNk5OTUVzd0RnWURWUjBQQVFIL0JBUURBZ2VBTUF3R0ExVWRFd0VCL3dRQ01BQXdLd1lEClZSMGpCQ1F3SW9BZyt6QTM3SnhCYzZVbXJtTk5UTG1nb09RRFFaMnFOSVNJQWpxMmp5MUJKSlF3Q2dZSUtvWkkKemowRUF3SURTQUF3UlFJaEFNMW42SDlFY01qb09hYkQ3WVJwQXUwOWp4NWcrMEd2c05qNmFZTElWQ2FUQWlBcApQYlJ6MFo1NFo1a0NDOHhpS0t3d3BNK05jNjhPanRBSWRsQmRmZkM2QlE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==",
"mspid":"Org2MSP"
},
"nonce":"d82/MXTjQoG1RPdyuPxM16TjThX1bJks"
}
}
},
"signature":"MEUCIQDXH7HH1+++Fw9Y/MLRHj4smpxBJpMlM8ZuIGAHK0kmXgIgaLFa9R8ajOnZUZDTGmLpxTs4sVwOiyjD5BZJB6JLBBY="
}
]
},
"header":{
"data_hash":"VN26ozBNLgcSnB16dBhtCRjW0MOYD1sLNCGBOBg9da0=",
"number":"4",
"previous_hash":"HyT2nn+22vfSmZILRLspLimV9ENLempiKRfdAhl0/q4="
},
"metadata":{
"metadata":[
"CgQKAggCEv0GCrIGCpUGCgpPcmRlcmVyTVNQEoYGLS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNEVENDQWJPZ0F3SUJBZ0lSQUsvbnFuTHJYbTN5ODJvaWdHQUpKWlF3Q2dZSUtvWkl6ajBFQXdJd2FURUwKTUFrR0ExVUVCaE1DVlZNeEV6QVJCZ05WQkFnVENrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY1REVk5oYmlCRwpjbUZ1WTJselkyOHhGREFTQmdOVkJBb1RDMlY0WVcxd2JHVXVZMjl0TVJjd0ZRWURWUVFERXc1allTNWxlR0Z0CmNHeGxMbU52YlRBZUZ3MHhPVEV5TXpBd016RTFNREJhRncweU9URXlNamN3TXpFMU1EQmFNRmd4Q3pBSkJnTlYKQkFZVEFsVlRNUk13RVFZRFZRUUlFd3BEWVd4cFptOXlibWxoTVJZd0ZBWURWUVFIRXcxVFlXNGdSbkpoYm1OcApjMk52TVJ3d0dnWURWUVFERXhOdmNtUmxjbVZ5TG1WNFlXMXdiR1V1WTI5dE1Ga3dFd1lIS29aSXpqMENBUVlJCktvWkl6ajBEQVFjRFFnQUVyVHJiMjNjTXAzMlExTDV6UXR3d29lQk1Ia1lLOGN6bVdya2lFZUhveWVWNjM4aWkKQ3JEUGt4U1BoMDR3Z3RXOTV5d3oxT1hDSG5DYWw2VThoWm1odGFOTk1Fc3dEZ1lEVlIwUEFRSC9CQVFEQWdlQQpNQXdHQTFVZEV3RUIvd1FDTUFBd0t3WURWUjBqQkNRd0lvQWdYZXZxK3lld2p4dUhEWk10eVZEckNQMXNlTmxjCk0wSmFzSE5BZ3JBcUQvUXdDZ1lJS29aSXpqMEVBd0lEU0FBd1JRSWhBSVBDQjdJNThrZzJJNkJiaHVpU3FHbkYKVjFRZC9wZ2RGT1JiWUU3MSt3cGNBaUFTejhMdWpzU1l3d0FLb2lRRmF4a0dQNTJmOTBhTGtnTFdKRk1UMWs1eApGUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0KEhj98u3PfmVFt5+7jIBwQeOJsXhb280QIQ8SRjBEAiAl5Q7dLotTv2/kmn3JXubtdJU52Ti4WJKynmNPgIpEpQIgeb499fxau3mYtPtMiwrsnbJxpSFqogz1zdDIHiZmcOg=",
"CgIIAg==",
"AA==",
""
]
}
}