菜鳥系列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
}

上述流程為:

  1. 驗證區塊頭和區塊數據的有效性
  2. 驗證交易的合法性以及vscc驗證背書策略的有效性
  3. 處理私密數據
    1. 過濾該區塊存在的隱私數據讀寫集
    2. 計算本地缺失的私密數據信息
    3. 從其他節點獲取缺失的私密數據信息
  4. 寫區塊和私密數據

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特殊驗證
    1. 驗證輸入參數的合法性
    2. 驗證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校驗

  1. 驗證公共數據讀集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"
}
  1. 驗證范圍查詢
    針對公共數據范圍查詢的讀集合進行驗證,循環遍歷每個范圍查詢對象,驗證范圍查詢數據的讀數據版本是否一致。

  2. 驗證私密數據讀集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==",
            ""
        ]
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM