原文:http://hyperledger-fabric.readthedocs.io/en/latest/readwrite.html

翻译:梧桐树



交易模拟和read-write set

在背书节点上的交易模拟期间会产生一个交易的read-write set。read set包含在模拟期间交易读取到的唯一key及对应version。write set交易改写的唯一key(可能与read set中的key重叠)及对应的新value。如果交易的更新操作是删除一个key,则在write set为该key设置一个delete标记。

此外,如果交易中对一个key改写多次,则只保留最后的修改值。如果交易中读取一个key的值,即使交易在读取之前更新了该key的值,读取到的也会是之前提交过的而不是刚更新的。换句话说,不能读取到同一交易中修改的值。

如前所述,key的version只记录在read setwrite set只包含key及对应新value。

对于read set的version的实现有很多种方案,最基本要求就是为key生成一个非重复标识符。例如用递增的序号作为version。在目前的代码实现中我们使用了blockchain height作为version方案,就是用交易的height作为该交易所修改的key的version。交易height由一个结构表示(见下面Height struc),其中TxNum表示这个tx在block中的height(译注:也就是交易在区块中的顺序)。该方案相较于递增序号有很多优点–主要是这样的version可以很好地利用到诸如statedb、交易模拟和校验这些模块中。

此外,如果模拟交易中执行了批量查询(range query),批量查询结果会被放到read-write set中的query-info

// 译注

// read-write set 结构
type TxReadWriteSet struct {
	NsRWs []*NsReadWriteSet
}
type NsReadWriteSet struct {
	NameSpace        string
	Reads            []*KVRead
	Writes           []*KVWrite
	RangeQueriesInfo []*RangeQueryInfo
}
type RangeQueryInfo struct {
	StartKey     string
	EndKey       string
	ItrExhausted bool
	Results      []*KVRead
	ResultHash   *MerkleSummary
}
type MerkleSummary struct {
	MaxDegree      int
	MaxLevel       MerkleTreeLevel
	MaxLevelHashes []Hash
}
type MerkleTreeLevel int
type Hash []byte

// read set 结构
type KVRead struct {
	Key     string
	Version *Height
}
type Height struct {
	BlockNum uint64
	TxNum    uint64
}

// write set 结构
type KVWrite struct {
	Key      string
	IsDelete bool
	Value    []byte
}

下面是一个假设的交易模拟生成的read-write set示例,简单起见,示例中使用了递增序号作为version。

<TxReadWriteSet>
  <NsReadWriteSet name="chaincode1">
    <read-set>
      <read key="K1", version="1">
      <read key="K2", version="1">
    </read-set>
    <write-set>
      <write key="K1", value="V1">
      <write key="K3", value="V2">
      <write key="K4", isDelete="true">
    </write-set>
  </NsReadWriteSet>
<TxReadWriteSet>

使用read-write set 验证交易和更新worldState

提交节点(committer)利用read set部分校验交易的有效性;用write set部分更新key的version和value。

在验证阶段,如果read set中每个key的version都与stateDB中对应worldState(假设所有之前的有效交易,包括同一个block中的交易,都已经提交完成,即已更新ledger)的version相匹配,则认为此交易有效。

如果read-write set中包含query-info,则还要对此执行额外的校验。该校验确保在此批量查询的结果范围内没有key被增删改。换句话说,如果在验证阶段重新执行该批量查询(模拟期间执行的交易)应该产生与模拟交易期间相同的结果。此校验确保交易在提交时出现幻读会被认为无效。注意,这个幻读保护仅限于Chaincode的GetStateByRangeGetStateByPartialCompositeKey两个方法(译注:此处文档上提到的是GetStateByRangeGetQueryResult两个方法,但在代码里的注释却不是这样,此处以代码为准。详见fabric/examples/chaincode/go/marbles02/marbles_chaincode.go)。而其他批量查询方法(如:GetQueryResult)会有幻读风险,因此这种查询应该只用于不会被提交到ordering的只读交易,除非app能保证交易模拟和交易验证提交两阶段之间结果集稳定。

如果交易验证通过,committer就会用write set更新worldState。在更新阶段,write set中的每个key在worldState中对应的value都会被更新,然后worldState中这些key的version也会随着更新。

交易模拟与交易验证 示例

本节通过示例场景帮助理解read-write set。存在一个key设为k,在worldState中由元组(k,var,val)表示,其中verk的最新的version,valk的value。

现在有五个交易,分别是T1,T2,T3,T4,T5,这五个交易的模拟过程是针对相同的worldSate快照,下面的代码片段显示了模拟交易的worldState快照以及每个交易执行读写的顺序。

World state: (k1,1,v1), (k2,1,v2), (k3,1,v3), (k4,1,v4), (k5,1,v5)
T1 -> Write(k1, v1'), Write(k2, v2')
T2 -> Read(k1), Write(k3, v3')
T3 -> Write(k2, v2'')
T4 -> Write(k2, v2'''), read(k2)
T5 -> Write(k6, v6'), read(k5)

假设这些交易的顺序是T1,…T5(可能在同一个block或者不同block)

  1. T1验证成功,因为它没有read操作。之后在worldState中的k1k2会被更新成(k1,2,v1'), (k2,2,v2')
  2. T2验证失败,因为它读取的k1在之前的交易T1中被修改了(译注:需要特别注意一个前提,即这五个交易的模拟过程是对于相同的worldState快照,而且T2又有write操作,所以T2会进入commit阶段进行验证,这样T2的k1.ver=1,T1完成后实际的k1.ver=2了,然后T2在commit校验是就会失败。也就是上文提到的一个交易的模拟和提交期间,某key的值被修改。。。但是有个疑问,正常使用中应该会经常出现T1、T2这种顺序的情况,难道会经常发生交易校验失败??如果如此,那对于用户来说岂不很难用?暂有此疑,有待研究
  3. T3验证成功,因为它没有read操作。之后在worldState中的k2会被更新成(k2,3,v2'')
  4. T4验证失败,因为它读取的k2在之前的交易T1中被修改了
  5. T5验证成功,因为它读取的k5没有在之前的任何交易中修改

注意:交易不支持多read-write set