Read-Write set semantics(读写集)
本文讨论了关于读写集当前实现的细节。
Transaction simulation and read-write set(事务模拟和读写集)
客户端提交事务到peer,peer会执行背书验证并模拟该事务的请求结果,为该事务的请求准备一个读写集。读集包含了该事务在读取本地账本时的一列事务版本信息及该信息对应的的一列唯一键,写集包含了一个唯一键(可能也允许与读集中的键重复)列表和事务写入的最新值。如果事务的执行是删除操作,那么将为这一列唯一键设置一个delete标记(在新值的位置)。
此外,如果事务为键多次写入一个值,则只保留最后一个写入值。另外,即使在事务读取结果发出之前更新了键值,也会返回事务读取已提交状态的值。换句话说,必须保证读写一致性。
正如上面提到的,键的版本只在读取集中记录,而写集仅包含由事务设置的一列惟一键和它们的最新值。
可以有各种各样的执行版本的方案。版本控制方案的最低要求是为给定的键生成不重复的标识符。例如,使用单调递增的版本可以采用一个这样的方案。在当前的实现中,我们使用一个基于区块链顶点的版本控制方案,在这个方案中,提交事务的顶点被用作事务修改后的所有键的最新版本,事务的顶点由一个元组表示(txNumber是该块内事务的顶点)。该方案与增量编号方案相比有许多优点—主要是它支持其他组件,如statedb、事务模拟和验证,以实现有效的设计选择。
下面是模拟假想事务的一个示例读写集的例子。为了简单起见,我们使用增量数字表示版本。
<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>
此外,如果事务在模拟期间执行范围查询,那么范围查询及其结果将被添加到读写集作为查询信息。
Transaction validation and updating world state using read-write set(使用读-写集进行事务验证和更新世界状态)
提交者使用读-写集的读集部分来检查事务的有效性,并使用读写集的写集部分更新受影响的键的版本和值。
在验证阶段,如果在事务读集中每一个key的版本都能够与world state(假定之前所有的事务都是有效的,包括之前在同一区块中的已经被提交的事务)中的key版本一致,那么该条事务则被认为是有效的。如果读写集还包含一个或多个查询信息,则需要执行额外的验证。
这些额外的验证需要确保在本次事务查询信息返回结果的范围内的key没有被插入/删除/更新过,换句话说,如果我们在对提交状态的验证过程中重新执行任何一个范围查询(在模拟期间执行的事务),那么它应该会产生与在模拟时所观察到的结果相同的结果。此检查确保如果事务在提交期间观察到虚项,则该事务应被标记为无效。注意,这个虚项保护仅限于范围查询(例如:在chaincode中的GetStateByRange函数)并且还没有为其他查询(例如:在chaincode中的GetQueryResult函数)实现。其他查询有可能出现虚项,因此只能在没有提交到排序的只读事务中使用,除非应用程序能够保证模拟和验证/提交时间之间的结果集的稳定性。
如果一个事务通过了有效性检查,提交者将使用写集来更新世界状态。在更新阶段,对于写集中的每个键,相同键的值都设置为在写集中指定的值,进一步地,这个世界状态的键的版本会被改变,以反映最新的版本。
Example simulation and validation(模拟和验证案例)
本节通过一个示例场景帮助理解语义。对于本例的目的,在世界状态中,键k的存在是由元组(k、ver、val)表示的,其中,ver是键k的最新版本,它的值由val表示。
现在,考虑一组5个事务,T1、T2、T3、T4和T5,它们都在同一个快照上模拟世界状态。下面的代码片段显示了对事务进行模拟的世界状态的快照,以及由这些事务执行的读取和写入活动的顺序。
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(可以包含在一个区块或不同的区块中)
- T1通过验证,因为它不执行任何读取操作。此外,世界状态中的键k1和k2的元组被更新为(k1,2,v1‘)和(k2,2,v2‘)
- T2失败了,因为它读取了一个键k1,它被之前的事务修改为T1
- T3通过验证,因为它不执行读操作。进一步的,在这个世界状态下的键的元组被更新到(k2,3,v2‘‘)
- T4失败了,因为它读取了一个键k2,它被之前的事务T1修改过
- T5通过验证,因为它读取了一个键k5,它没有被前面的任何事务修改过
注意:目前还不支持具有多个读写集的事务。