Michael.W谈hyperledger Fabric第25期-详细带读Fabric的源码10-peer节点交易的读写集合RWSet

Michael.W谈hyperledger Fabric第25期-详细带读Fabric的源码10-peer节点交易的读写集合RWSet

    • 1 读写集合与“双花”问题
    • 2 交易读写集合的生成
    • 3 交易读写集合的验证

1 读写集合与“双花”问题

个人认为这个是Fabric的重点,因为读写集合是Fabric中防止"双花"问题的根本手段。
现在的信息互联网只能做到信息的复制,当你浏览一个网站的时候,实际上是服务器将页面相关的信息通过网络复制一份到了你的计算机中。这就导致了信息互联网无法完成价值的转移。如果我给了你这个东西,必须保证这个东西到了我这时,你那边就没有了。

价值互联网上的资产是唯一的,无法被拷贝的,只能够被转移。

比特币中是通过最长链的共识机制来有效地解决"双花"问题,Fabric中即是通过对交易读写集合的校验来完成的。

简单总结Fabric对读写集合的验证:当前交易的读集状态必须与世界状态以及当前未被持久化的前序交易执行后的状态要保持一致。如果不一致,这笔交易就是无效的

2 交易读写集合的生成

交易读写集合的生成发生在模拟交易执行的时候。
交易模拟器的源代码位置:core/ledger/kvledger/txmgmt/txmgr/lockbasedtxmgr/lockbased_tx_simulator.go

	type lockBasedTxSimulator struct {
		lockBasedQueryExecutor
	 	 // 用于构建读写集合
		rwsetBuilder *rwsetutil.RWSetBuilder 
	}

RWSetBuilder类是用来构建读写集合的。看一下RWSetBuilder的定义:

	type RWSetBuilder struct {
	  	// 一个map,键为namespace
		rwMap map[string]*nsRWs 
	}

它的类方法如下所示:

	// 创建RWSetBuilder对象
	func NewRWSetBuilder() *RWSetBuilder {
		return &RWSetBuilder{make(map[string]*nsRWs)}
	}
	// 向读集中添加键和对应版本号
	func (rws *RWSetBuilder) AddToReadSet(ns string, key string, version *version.Height) {
		nsRWs := rws.getOrCreateNsRW(ns)
		nsRWs.readMap[key] = NewKVRead(key, version)
	}
	
	// 向写集中添加键和对应版本号
	func (rws *RWSetBuilder) AddToWriteSet(ns string, key string, value []byte) {
		nsRWs := rws.getOrCreateNsRW(ns)
		nsRWs.writeMap[key] = newKVWrite(key, value)
	}
	
	// 增添一个范围查询集合用来执行幻想读取有效性
	func (rws *RWSetBuilder) AddToRangeQuerySet(ns string, rqi *kvrwset.RangeQueryInfo) {
		nsRWs := rws.getOrCreateNsRW(ns)
		key := rangeQueryKey{rqi.StartKey, rqi.EndKey, rqi.ItrExhausted}
		_, ok := nsRWs.rangeQueriesMap[key]
		if !ok {
			nsRWs.rangeQueriesMap[key] = rqi
			nsRWs.rangeQueriesKeys = append(nsRWs.rangeQueriesKeys, key)
		}
	}
	
	// 以可以被序列化的格式返回读写集合
	func (rws *RWSetBuilder) GetTxReadWriteSet() *TxRwSet {
		txRWSet := &TxRwSet{}
	  	// 将rws.rwMap中的key(namespace)排序,然后返回这些排序后的key。因为map是无序的。
	  	// sortedNamespaces类型为[]string
		sortedNamespaces := util.GetSortedKeys(rws.rwMap)
	  	// 按排序好的键顺序,处理其对应的写集、读集和范围查询信息
		for _, ns := range sortedNamespaces {
			nsReadWriteMap := rws.rwMap[ns]
			// 添加写集
			var reads []*kvrwset.KVRead
			sortedReadKeys := util.GetSortedKeys(nsReadWriteMap.readMap)
			for _, key := range sortedReadKeys {
				reads = append(reads, nsReadWriteMap.readMap[key])
			}
			// 添加读集
			var writes []*kvrwset.KVWrite
			sortedWriteKeys := util.GetSortedKeys(nsReadWriteMap.writeMap)
			for _, key := range sortedWriteKeys {
				writes = append(writes, nsReadWriteMap.writeMap[key])
			}
			// 添加范围查询信息
			var rangeQueriesInfo []*kvrwset.RangeQueryInfo
			rangeQueriesMap := nsReadWriteMap.rangeQueriesMap
			for _, key := range nsReadWriteMap.rangeQueriesKeys {
				rangeQueriesInfo = append(rangeQueriesInfo, rangeQueriesMap[key])
			}
			kvRWs := &kvrwset.KVRWSet{Reads: reads, Writes: writes, RangeQueriesInfo: rangeQueriesInfo}
			nsRWs := &NsRwSet{ns, kvRWs}
			txRWSet.NsRwSets = append(txRWSet.NsRwSets, nsRWs)
		}
		return txRWSet
	}

还有一些都是读写集合数据结构相关的代码,这里不做详细介绍了。

3 交易读写集合的验证

读写集合验证的实现代码位置:core/ledger/kvledger/txmgmt/validator/statebasedval/state_based_validator.go

	// 验证类
	type Validator struct {
		db statedb.VersionedDB
	}
	// 创建一个验证类对象
	func NewValidator(db statedb.VersionedDB) *Validator {
		return &Validator{db}
	}
	...
	// 验证读写集合的入口,实现了一个Validator接口[1]
	func (v *Validator) ValidateAndPrepareBatch(block *common.Block, doMVCCValidation bool) (*statedb.UpdateBatch, error) {
		logger.Debugf("New block arrived for validation:%#v, doMVCCValidation=%t", block, doMVCCValidation)
		// 创建一个UpdateBatch对象,用于存放验证block后要对状态数据库进行的更改
		updates := statedb.NewUpdateBatch()
		logger.Debugf("Validating a block with [%d] transactions", len(block.Data.Data))
	
	  	// 获取交易状态。当commiter验证区块后(在验证交易读写集合之前进行的其他验证),将返回交易有效性的理由码切片。之前已经被验证无效的交易,在读写集合验证中将跳过。
		txsFilter := util.TxValidationFlags(block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER])
	
		// 防止区块在验证交易读写集合之前没有经过其他commiter的验证,一般不会出现这种情况
		if len(txsFilter) == 0 {
			txsFilter = util.NewTxValidationFlags(len(block.Data.Data))
			block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] = txsFilter
		}
	
	  	// 遍历验证交易读写集合
		for txIndex, envBytes := range block.Data.Data {
			if txsFilter.IsInvalid(txIndex) {
	      		// 如果是无效交易,跳过
				logger.Warningf("Block [%d] Transaction index [%d] marked as invalid by committer. Reason code [%d]",
					block.Header.Number, txIndex, txsFilter.Flag(txIndex))
				continue
			}
	    	// 从区块中获取封装的交易信息(*common.Envelope)
			env, err := putils.GetEnvelopeFromBlock(envBytes)
			if err != nil {
				return nil, err
			}
			// 从获取到的封装交易信息中获取Payload
			payload, err := putils.GetPayload(env)
			if err != nil {
				return nil, err
			}
			// 从获取到的Payload中获取ChannelHeader
			chdr, err := putils.UnmarshalChannelHeader(payload.Header.ChannelHeader)
			if err != nil {
				return nil, err
			}
			// 获取该交易类型[2]
			txType := common.HeaderType(chdr.Type)
			
	    	// 如果该交易类型不是模拟交易(背书交易),跳过
			if txType != common.HeaderType_ENDORSER_TRANSACTION {
				logger.Debugf("Skipping mvcc validation for Block [%d] Transaction index [%d] because, the transaction type is [%s]",
					block.Header.Number, txIndex, txType)
				continue
			}
	    	// 验证交易的读写集。txRWSet:验证后的交易读写集合,txResult:验证后的交易状态(有效或无效)
			txRWSet, txResult, err := v.validateEndorserTX(envBytes, doMVCCValidation, updates)
			if err != nil {
				return nil, err
			}
	
			// 根据上面获得的txResult,更新状态理由码
			txsFilter.SetFlag(txIndex, txResult)
	
			//txRWSet != nil => t is valid
			if txRWSet != nil {
	      	// 如果验证后的状态读写集合非空,即有效
	      		// 更新版本号
				committingTxHeight := version.NewHeight(block.Header.Number, uint64(txIndex))
	      		// 将该笔交易写入前面创建的UpdateBatch对象,该对象用于存放验证block后要对状态数据库进行的更改
				addWriteSetToBatch(txRWSet, committingTxHeight, updates)
				txsFilter.SetFlag(txIndex, peer.TxValidationCode_VALID)
			} 
			// 再打印一下该笔交易的有效性。估计就是用于调试,让调试的码农能看到验证的变化。
			if txsFilter.IsValid(txIndex) {
				logger.Debugf("Block [%d] Transaction index [%d] TxId [%s] marked as valid by state validator",
					block.Header.Number, txIndex, chdr.TxId)
			} else {
				logger.Warningf("Block [%d] Transaction index [%d] TxId [%s] marked as invalid by state validator. Reason code [%d]",
					block.Header.Number, txIndex, chdr.TxId, txsFilter.Flag(txIndex))
			}
		}
		// 更新交易状态到区块的元数据中[3]
		block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] = txsFilter
		// 返回待更新的状态集
	  return updates, nil
	}
  • [1] Validator是用来验证读写集合的接口,其源码位置:core/ledger/kvledger/txmgmt/validator/validator.go
    	type Validator interface {
    		ValidateAndPrepareBatch(block *common.Block, doMVCCValidation bool) (*statedb.UpdateBatch, error)
    	}
    
  • [2] 交易类型的定义所在的文件位置:protos/common/common.pb.go
    	type HeaderType int32
    	const (
    		HeaderType_MESSAGE              HeaderType = 0
    		HeaderType_CONFIG               HeaderType = 1
    		HeaderType_CONFIG_UPDATE        HeaderType = 2
    		HeaderType_ENDORSER_TRANSACTION HeaderType = 3
    		HeaderType_ORDERER_TRANSACTION  HeaderType = 4
    		HeaderType_DELIVER_SEEK_INFO    HeaderType = 5
    		HeaderType_CHAINCODE_PACKAGE    HeaderType = 6
    	)
    
  • [3] 这个block就是在orderer和peer之间交互的区块数据结构,结构如下:
    	type Block struct {
    	  	// 包含本区块的编号、本区块的Data的hash、前一个区块的Data的hash
    		Header   *BlockHeader   `protobuf:"bytes,1,opt,name=header" json:"header,omitempty"`
    	  	// 经过protobuf序列化后的区块数据
    		Data     *BlockData     `protobuf:"bytes,2,opt,name=data" json:"data,omitempty"`
    	  	// 经过protobuf序列化后的区块元数据
    		Metadata *BlockMetadata `protobuf:"bytes,3,opt,name=metadata" json:"metadata,omitempty"`
    	}
    
    可见,区块的hash值只是对Data取哈希,并没有将元数据计算在内。

关于Block中的Header字段的结构图:
Michael.W谈hyperledger Fabric第25期-详细带读Fabric的源码10-peer节点交易的读写集合RWSet_第1张图片
接下来看一下对交易读写集验证的方法validateEndorserTX的源码:

	func (v *Validator) validateEndorserTX(envBytes []byte, doMVCCValidation bool, updates *statedb.UpdateBatch) (*rwsetutil.TxRwSet, peer.TxValidationCode, error) {
	  	// 从序列化的区块Data中,还原出链码的执行时产生的事件(events)
		respPayload, err := putils.GetActionFromEnvelope(envBytes)
		if err != nil {
			return nil, peer.TxValidationCode_NIL_TXACTION, nil
		}
	  	// 定义一个用于存放交易读写集合的对象
		txRWSet := &rwsetutil.TxRwSet{}
		// 反序列化,得到交易读写集
		if err = txRWSet.FromProtoBytes(respPayload.Results); err != nil {
			return nil, peer.TxValidationCode_INVALID_OTHER_REASON, nil
		}
	  	// 先将交易状态理由码定义成有效
		txResult := peer.TxValidationCode_VALID
	 	// doMVCCValidation为参数传递进来的一个bool值
		if doMVCCValidation {
	    // 做交易读写集合的验证,调用validateTx()方法
			if txResult, err = v.validateTx(txRWSet, updates); err != nil {
	      	// 如果验证过程出现问题,返回nil和error
				return nil, txResult, err
			} else if txResult != peer.TxValidationCode_VALID {
	      		// 如果验证过程没问题,只是验证结果为交易无效,读写集合清空
				txRWSet = nil
			}
		}
		return txRWSet, txResult, err
	}

再进到交易读写集合验证方法validateTx中看一下:

	func (v *Validator) validateTx(txRWSet *rwsetutil.TxRwSet, updates *statedb.UpdateBatch) (peer.TxValidationCode, error) {
	  	// 遍历读写集合
		for _, nsRWSet := range txRWSet.NsRwSets {
	    	// 通过namespace找到KV键值对表示的对应读写集对象[1](kvrwset.KVRWSet)
			ns := nsRWSet.NameSpace
			// 验证读集
			if valid, err := v.validateReadSet(ns, nsRWSet.KvRwSet.Reads, updates); !valid || err != nil {
				if err != nil {
					return peer.TxValidationCode(-1), err
				}
				return peer.TxValidationCode_MVCC_READ_CONFLICT, nil
			}
	    	// 验证范围查询信息
			if valid, err := v.validateRangeQueries(ns, nsRWSet.KvRwSet.RangeQueriesInfo, updates); !valid || err != nil {
				if err != nil {
					return peer.TxValidationCode(-1), err
				}
				return peer.TxValidationCode_PHANTOM_READ_CONFLICT, nil
			}
		}
		return peer.TxValidationCode_VALID, nil
	}
  • [1] KV类型的读写集合中的数据结构组成:
    	type KVRWSet struct {
    	  	// 读集。逻辑上针对一个key
    		Reads            []*KVRead         `protobuf:"bytes,1,rep,name=reads" json:"reads,omitempty"`
    	  	// 范围查询信息。其为读集的一个拓展。逻辑上针对多个key
    		RangeQueriesInfo []*RangeQueryInfo `protobuf:"bytes,2,rep,name=range_queries_info,json=rangeQueriesInfo" json:"range_queries_info,omitempty"`
    	  	// 写集
    		Writes           []*KVWrite        `protobuf:"bytes,3,rep,name=writes" json:"writes,omitempty"`
    	}
    

再深入到验证读集的方法validateReadSet中:

	func (v *Validator) validateReadSet(ns string, kvReads []*kvrwset.KVRead, updates *statedb.UpdateBatch) (bool, error) {
	  	// 循环遍历读集,因为从上面的数据结构中可以看出,读集的类型为[]*KVRead,是个切片
		for _, kvRead := range kvReads {
	    	// 对单个KVRead对象进行验证
			if valid, err := v.validateKVRead(ns, kvRead, updates); !valid || err != nil {
				return valid, err
			}
		}
		return true, nil
	}
	
	func (v *Validator) validateKVRead(ns string, kvRead *kvrwset.KVRead, updates *statedb.UpdateBatch) (bool, error) {
	  	// 在状态更新集合中查看是否已经有了这个KVRead。如果存在,说明在这个区块里,之前的交易中已经对该KVRead的Key所指的交易进行了修改。这样该Key所对应的版本号(状态)一定跟KVRead的版本号不同。所以这次KVRead所代表的交易是无效交易。这也印证了我前面在讲读写集验证的时候所说的逻辑。
		if updates.Exists(ns, kvRead.Key) {
			return false, nil
		}
		
	  	// 读取KVRead.Key在世界状态中对应的值。目的是为了获取世界状态中的版本号。
		versionedValue, err := v.db.GetState(ns, kvRead.Key)
		if err != nil {
			return false, err
		}
		var committedVersion *version.Height
		if versionedValue != nil {
	    	// 获取到世界状态中的版本号
			committedVersion = versionedValue.Version
		}
	
		// 验证世界状态中读出的版本号和kvRead中的版本号是否一致[1]
		if !version.AreSame(committedVersion, rwsetutil.NewVersion(kvRead.Version)) {
			logger.Debugf("Version mismatch for key [%s:%s]. Committed version = [%s], Version in readSet [%s]",
				ns, kvRead.Key, committedVersion, kvRead.Version)
	    	// 不一致返回false
			return false, nil
		}
	  	// 一致返回true
		return true, nil
	}
  • [1] 版本号其实就是区块高度

ps:
本人热爱图灵,热爱中本聪,热爱V神,热爱一切被梨花照过的姑娘。
以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。
同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!
如果需要转发,麻烦注明作者。十分感谢!
后现代泼痞浪漫主义奠基人
公众号名称:后现代泼痞浪漫主义奠基人

你可能感兴趣的:(Fabric,Fabric)