1. 基本事务背书流程
下面我们从高层次的角度概述一个事务的请求流程。
备注:请注意,以下协议并不假定所有事务都是确定性的,即允许非确定性事务。
1.1. 客户端创建一个事务并将其发送给选择的背书peer节点
为了调用一个事务,客户端发送一个PROPOSE
消息给它所选择的一组背书peer节点(可能不是同一时间发送 - 见2.1.2节和2.3节)。一个给定的chaincodeID
的背书peer节点的集合经由peer节点被提供给客户端,该peer节点又从背书策略(参见第3节)知道背书peer节点的集合。例如,事务可以发送给给定chaincodeID
的所有背书节点。也就是说,一些背书节点可能会离线,其他节点可能会反对,并选择不对事务背书。提交客户端尝试满足可用的背书节点的背书策略表达式。
在下文中,我们首先详细介绍PROPOSE
消息格式,然后讨论提交客户端和背书节点之间可能的交互模式。
1.1.1. PROPOSE
消息格式
PROPOSE
消息的格式为<PROPOSE,tx,[anchor]>
,其中tx
是必要参数,anchor
是可选参数。
-
tx=<clientID,chaincodeID,txPayload,timestamp,clientSig>
,其中:-
clientID
是提交客户端的ID; -
chaincodeID
是指事务所属的链代码的ID; -
txPayload
是包含提交的事务本身的有效数据载体; -
timestamp
是客户端维护的单调递增(对于每个新的事务)整数; -
clientSig
是tx其他字段上客户端的签名,即clientSig=Signature(clientID,chaincodeID,txPayload,timestamp);
txPayload
的细节在调用事务和部署事务(如系统结构所述,部署事务其实是对系统链代码事务的调用)之间将有所不同。对于调用事务,txPayload
由两个字段组成:-
txPayload = <operation, metadata>
,其中-
operation
表示链代码操作(函数)和参数; -
metadata
表示与调用相关的属性;
-
对于部署事务来说,
txPayload
由三个字段组成:-
txPayload = <source, metadata, policies>
,其中-
source
表示链代码的源码; -
metadata
表示与链代码和应用程序相关的属性; -
policies
包含与所有peer节点可访问的链代码有关的策略,如背书策略。请注意,背书策略在deploy
事务不会与txPayload
一起提供,但deploy
的txPayload
包含背书策略ID以及参数(请参阅第3节)。
-
-
-
anchor
包含读版本依赖,或者更具体地说,key-version对(即,anchor
是KxN
的一个子集),它将PROPOSE
请求绑定或”anchors”到KVS指定版本的key。(请参阅1.2节)。如果客户端指定了anchor
参数,则一个背书者仅在读取它本地KVS匹配anchor
中相应key的版本号时,才会对一个事务进行背书(更多细节请参阅2.2节)。tx
的加密散列值被所有节点用作唯一事务标识符tid
(即,tid=HASH(tx)
)。客户端在内存中存储tid
,并等待来自背书peer节点的响应。1.1.2. 消息模式
客户端决定与背书peer节点的交互顺序。例如,客户端通常会发送<PROPOSE, tx>
(即没有anchor
参数)给一个单一背书peer节点,然后产生版本依赖(anchor
),客户端以后可以把所产生的anchor
作为其PROPOSE
消息的参数发送给其他背书节点。作为另外一个例子,客户端可以直接发送<PROPOSE, tx>
(没有anchor
)给所选的所有背书节点。不同模式的通讯都是可以的,并且客户端可以自由决定(见2.3节)。
1.2. 背书peer节点模拟事务并产生背书签名
在收到来自客户端的<PROPOSE,tx,[anchor]>
时,背书peer节点epID
首先验证客户端的签名clientSig
,然后模拟执行一个事务。如果客户端指定了anchor
,则只有在其本地KVS中的对应键的读取版本号(即,下面定义的readset
)匹配anchor
指定的版本号时,背书peer节点才会模拟执行事务。
模拟事务涉及通过背书peer节点调用事务引用的链代码(chaincodeID
)以及复制背书peer节点在本地持有的状态副,以此来临时执行事务(txPayload
)。
作为执行的结果,背书peer节点计算出读取版本依赖性(readset
)和状态更新(writeset
),也称为DB语言中的MVCC+postimage。MVCC是多版本并发控制Multiversion Concurrency control的缩写名词。MVCC一般用于数据库管理系统,为了解决存在多并发读写数据时可能出现数据不一致性问题。因为区块链系统中存在多个客户端请求执行事务,所以也会存在账本读写不一致问题。
回想一下,状态由key/value(k/v)对组成。所有k/v对都是版本化的,即每一个k/v对都包含了有序的版本化信息,每当某个key对应的value更新时,该版本信息就会增加。解释事务的peer节点会记录被链代码访问的所有k/v对,用于读取或写入,但peer节点尚未更新其状态。进一步来说:
-
给定一个在背书peer节点执行事务前,它的状态为
s
,对于事务读取的每个k
,(k,s(k).version)
pair被添加到到readset
中。 -
另外,对由事务修改为新值
v'
的每个k
,(k,v')
pair被添加到writeset
。或者,v'
可能是新值与先前值(s(k).value
)的增量。(使用增量可以减少writeset
的大小)
如果客户端在PROPOSE
消息中指定了anchor
,则客户端指定的anchor
必须等于模拟执行事务时由背书peer节点生成的readset
。
然后,peer节点向内部转发tran-proposal
(也可能是tx
)到其事务背书的(peer)逻辑部分,称为背书逻辑。默认情况下,peer节点的背书逻辑会接受tran-proposal
并简单地对tran-proposal
进行签名。然而,背书逻辑可以解释任意的功能,例如,以tran-proposal
和tx
作为输入来与遗留系统交互,以达成是否认可交易的决定。
如果背书逻辑决定认可一个事务,它发送<TRANSACTION-ENDORSED, tid, tran-proposal,epSig>
消息到提交客户端(tx.clientID
),其中:
-
tran-proposal := (epID,tid,chaincodeID,txContentBlob,readset,writeset)
,其中txContentBlob
是(链代码/事务)特定的信息。目的是将txContentBlob
用作tx
的一些表示(例如txContentBlob=tx.txPayload
)。 -
epSig
是背书peer节点对tran-proposal
的签名。
否则,如果背书逻辑拒绝认可事务,则背书peer节点可能向客户端发送消息(TRANSCATION-INVALID, tid, REJECTED)
。
请注意,背书peer节点在这一步不会改变其状态,在背书背景下由事务模拟产生的更新不会影响状态!
1.3. 提交的客户端收集事务的倍数并通过ordering service进行广播
提交的客户端会等待,直到它收到(TRANSACTION-ENDORSED, tid, *, *)
语句的“足够”的消息和签名才能断定该事务提案已经得到背书。正如2.1.2节所讨论的,这可能涉及与背书peer节点的一次或多次交互。
“足够”的确切数量取决于chaincode背书策略(另见第3节)。如果背书策略得到满足,事务已获得背书;注意它还没有提交。从背书peer节点收集签名的TRANSACTION-ENDORSED
消息,建立一个得到背书的事务,这个过程叫做背书,并用endorsement
来表示。
如果提交客户端没有设法收集事务提案的背书,它会放弃此事务并选择稍后重试。
对于有效背书的事务,我们现在开始使用ordering service。提交客户端使用broadcast(blob)
调用ordering service,其中blob=endorsement
。如果客户端没有直接调用ordering service的能力,可以通过自己选择一些peer节点作为代理它的广播。该客户端必须信任peer节点不会从endorsement
中删除任何消息,否则这个事务就会被视为无效。但是,请注意,一个代理peer节点可能不会编造有效的endorsement
。
1.4. ordering service向peer节点交付事务
当发生事件deliver(seqno, prevhash, blob)
并且peer节点已将所有状态更新应用于序列号低于seqno
的blob时,peer节点执行以下操作:
-
它根据链代码(
blob.tran-proposal.chaincodeID
)的背书策略检查blob.endorsement
是否有效; -
在典型情况下,它还验证依赖性(
blob.endorsement.tran-proposal.readset
)同时未被违反。在更复杂的情况下,背书的tran-proposal
字段可能有所不同,在这种情况下,背书策略(第3部分)规定了状态如何更新。
根据状态更新选择的一致性属性和”隔离保证”,可以以不同的方式验证依赖关系。除非链代码背书策略制定了不同的认证,否则可串行化是默认的隔离保证。可以通过要求与readset
中的每个key相关联的版本等于该key在状态中的版本以及拒绝不满足该要求的事务来提供可串行属性。
-
如果所有这些检查都通过,事务被视为valid或committed。在这种情况下,peer节点在
PeerLedger
的位掩码将事务标记为1,将blob.endorsement.tran-proposal.writeset
应用于区块链状态(如果tran-proposals
是相同的,否则背书策略逻辑定义了处理blob.endorsement
的函数)。 -
如果
blob.endorsement
的背书策略验证失败,则该事务无效,并且该peer在PeerLedger
的位掩码将事务标记为0。重要的是要注意无效事务不会改变区块链状态。
请注意,在处理具有给定序列号的deliver事件(区块)后,这足以让所有(正确)的peer节点具有相同的状态。也就是说,通过ordering service能够保证所有正确的peer节点都会收到相同的deliver(seqno, prevhash, blob)
事件序列。由于评估背书策略和对readset
中版本依赖性的评估是确定性的,所有正确的peer节点都会得到同样的结论。因此,所有peer节点都会以相同的方式提交和应用相同的事务序列并更新他们的状态。
图1. 一般情况下的事务背书流程图
1.5. 基本事务背书流程总结
在前面4小节内容中已经阐述了什么是事务背书以及背书的一般流程,图1也描述了client、peer节点、orderer节点在事务背书过程中起到的作用。但是为什么需要背书,还有为什么不是直接让客户端把事务提交给peer节点,让peer节点直接执行事务呢?
-
先回答为什么需要背书这个问题?因为在一个channel里面,只有特定的peer才能够读写channel范围内的账本数据。所以客户端提交的事务只有得到特定的peer节点的背书,才能够相信客户端提交的事务是合法有效的,保证数据安全。
-
再回答为什么不直接让peer节点执行事务,而是通过把事务广播给orderer节点,orderer节点再通过deliver方法把区块信息分发给peer节点?这是因为在v1.0版本后,fabric通过引入orderer节点来分离事务排序与事务执行这两项动作,让两项动作能够并行执行,来提高系统性能。具体请参见fabric系统架构介绍。