1、fabric V1.x的架构图
架构主要包括几个部分:
- 应用sdk:用于和区块链网络进行通信,提供了包括安全认证,交易申请等功能
- 节点:负责背书,验证,提交交易等功能,每个节点都维护了一个或是多个账本,同时通过gossip网络对其他节点保持通信。Fabric里每个节点都是无状态的,
- order服务,负责打包,排序和分发交易
上图也简要概括了fabric一个交易的完整周期。
- 客户端首先通过进行身份认证等安全操作,进入区块链网络
- 客户端之后创建一个交易申请,发送给背书节点进行背书操作。
- 背书节点执行对应的链码,基于应用的key操作生成读写操作集,并把背书结果返回给客户端
- 客户端收到背书返回后,把交易发送给节点,由节点转发到order服务
- order服务排序交易,把交易封装到区块里,并广播给每个节点(或是说每个提交节点)
- 节点对交易进行背书策略验证,身份认证,区块中所有交易的有效性认证。之后写入账本,并返回交易结果
2、节点内交易流程
在Fabric中,为了方便共识模块化和数据访问权限的控制,引入了ordering服务和通道(Channel)的概念。所以Fabric中我们说的链路,其实包含了三部分:节点,通道和ordering服务,也就是说ordering服务和通道决定了哪些节点是在同一条链路上。
上图包含的一条完整的链为:节点1.1,节点1.2,节点2.1,节点2.3。而节点1.3 和节点2.2不在这条链上。在节点 1.1,1.2,2.1,2.3中,都维护了同一份账本,这几个节点由于处在同一个通道和ordering服务中,所以他们之间的所有数据和消息都是共享和透明的,他们中的任何一个节点发送了状态变化,其他三个节点都能知道,但是这些信息对于节点1.3,2.2来说就是完全不透明的。同样的,节点1.3,2.2的状态变化或是账本信息变化对于处于链路上的节点(节点1.1,节点1.2,节点2.1,节点2.3)也是完全独立,不可见。比如,链路上有笔交易申请发生在节点1.1上,节点1.1会把这个交易申请发给ordering服务,ordering服务会根据发过来的请求判断这个请求是发生在哪个通道上,之后会把封装了这个交易的块发给这个通道上的其他所有节点。这个特性能让fabric在实际的应用更好的方便数据的访问控制。
接下来我们简单了解下链码(CC -- ChainCode)。链码是Fabric中的智能合约,相比于以太坊上用solidity写智能合约,链码可以直接采用java,go语言来编写,更容易上手。链码分为两种
- 系统链码(SCC)
- 用户链码
系统链码
用来实现系统层面的功能,包括系统的配置,用户链码的部署、升级,用户交易的签名和验证策略等,运行在节点进程中。Fabric里有很多的系统链码,比如,用于背书的背书系统链码(ESCC),验证交易合法性的验证系统链码(VSCC)
用户链码
用于实现用户的应用功能。开发者编写链码应用程序并将其部署到网络上。终端用户通过与网络节点交互的客户端应用程序调用链码,运行在独立的Docker容器中。
下面是一个节点内的交易流程图
这里的交易提交规则为:
1.如果一个节点收到的2f(f为可容忍的拜占庭节点数)个其它节点发来的摘要都和自己相等,就向全网广播一条提交消息。
2.如果一个节点收到2f+1条提交消息,即可提交新区块及其交易到本地的区块链和状态数据库。
3、交易背书
什么是背书
背书可以理解为一种签名,签署行为。在fabric的意思是:告诉其他节点或是客户端,我这个节点模拟执行这个交易后的结果是什么,并附上自己的签名。客户端拿到到这个背书签名,就知道哪个节点执行了这个交易后的结果是什么。
不要把fabric里的背书行为当成是共识算法。这是错误的。
本文将详细阐述交易背书的三个流程。
1.客户端创建一个交易背书申请并发送给背书节点
我们这里说的客户端是指上链后的客户端,也就是说这个客户端一定连接到区块链的某个节点上的。为了发起一个交易,客户端需要给一组背书节点(客户端自己选择哪些背书节点)发送PROPOSE消息。那客户端是怎么知道背书节点的呢?在一条链路上,每个背书节点都会对外暴露部署在自己节点上的链码ID(chaincodeID), 这样通过交易中指明的chaincodeID,就可以找到所有部署了这个chaincodeID的背书节点。
PROPOSE消息格式为
tx=
其中clientID 是发起交易的客户端ID。chaincodeID这个交易所用到的链码ID。txPayload 交易申请的payload。timestamp 单调递增的整形数值,由客户端维护,一个交易对应一个。clientSig 对上述字段值的签名
anchor 版本相关信息,包含了读集合,更明确的是说一组key-version对(version是有序的版本号)。该KV对必须是KVS(key/value 存储,用于表示状态的一种数据结构)中的值。
在fabric中,有两种交易:部署交易和调用交易。部署交易是用于部署一个新的链码到一个区块链上。而调用交易是对已经部署在区块链上的链码的一个操作或是链码上一个方法调用。不同类型的交易,上面的payload也不同。
对于调用交易:
txpayload =
其中operation 值链码上的方法和参数。metadata 调用的相关属性。
对于部署交易:
txpayload =
每个交易都有tid,是全局唯一的,通过对tx进行加密hash计算得到。客户端会把tid存储在内存中,等待背书签名的返回。
由于PROPOSE消息格式中anchor是可选的,所以有两种方式发送PROPOSE消息给背书节点。一种是先发送
2.背书节点模拟一个交易并产生一个背书签名
当背书节点(id为epID)接收到从客户端发来的一条背书请求消息(
状态信息包含键值对,并且所有的键值对都有版本控制。也就是说,每个键值对都包含了一个有序的版本信息,每当键值对的值发生变化时候,版本信息都会自动增加。背书节点通过链码把这个交易转换成所有的键值对,不管是读或是写,但是节点的状态信息在这个时候是没有更新的。更详细的为:
假定背书节点执行交易前的状态为s,对于交易的每个键k,读取这个键对应的值s(k).value,把 (k,s(k).value) 存于读集合(readset)中。
对每个被该交易更改后的键k所对应的新值v’, (k,v’)被加入到写集合(writeset). 当然也可以存储原来旧的值和当前新值的差值。
通过上述描述,我们知道背书节点会自己默默的计算并保存这个交易前后的状态集合变化,通过这种方式来模拟一次交易。这些操作对外是不可见的。
之后,节点会把内部交易请求(tran-proposal)转发到节点的背书逻辑,实现对交易的背书签名操作,默认情况下,节点的背书逻辑接收到交易请求后,只是简单的对这个交易请求进行签名。当然背书逻辑有可以进行额外的一些操作,比如,通过交易请求和tx 作为输入来判断是否要对这个交易进行背书。
如果背书逻辑决定了要对交易进行背书签名操作,背书逻辑会发送
消息到对应的客户端(tx.clientID):
tran-proposal =
epID节点ID。tid 交易ID。txContentBlob 指链码或是交易信息,txContentBlob =tx.txPayload。readset, writeset 模拟交易后的读写集合。 epSig 对tran-proposal 的背书节点签名。
如果背书节点拒绝对一个交易进行背书签名,背书节点会发送
注意,在整个背书过程,背书节点都是没有改变状态
3.客户端收到背书结果并通过****ordering****服务广播给其他节点
客户端发送背书请求后,一直等待,直到客户端接收到了足够多的消息和在(TRANSACTION-ENDORSED,tid,,)状态上的签名之后,才认为交易已经经过背书签名操作。客户端具体需要多少消息才是足够多的消息呢? 这个数字取决于链码的背书策略。如果收到的消息满足了背书策略,那么就认为交易进行过背书签名操作。注意,这里只是说交易被背书签名,而不是说交易被提交。从背书节点发过来的并且满足背书策略的签名 TRANSACTION-ENDORSED集合,就是 背书签名。如果客户端在这个过程中没有收到节点返回的有效背书签名,重试一定次数后还是失败就终止当前交易。
当收到节点返回的背书签名后,我们就开始使用ordering服务了。客户端通过使用broadcast(blob)的方式(blob为背书签名)来调用ordering服务。如果客户端没有直接调用ordering服务的能力,可以通过所连接的节点来代理广播背书签名给ordering服务。这个代理节点对于客户端来说是可信的,默认这个节点不会篡改背书签名或是伪造背书签名。否则这个交易就无效了。
当事件deliver(seqno,prevhash,blob)发生,节点已经对所有低于seqno信息已经处理完成(seqno是有序递增的),节点接下来:
- 根据该交易相关的链码(blob.tran-proposal.chaincodeID)上的背书策略,检查该交易背书签名(blob.endorsement)是否有效
- 同时验证背书签名相关性(blob.endordement.tran-proposal.readset)是否有被篡改。更复杂的验证方式也可以通过背书策略来验证 tran-proposal的字段是否有效
- 同时验证背书签名相关性(blob.endordement.tran-proposal.readset)是否有被篡改。更复杂的验证方式也可以通过背书策略来验证 tran-proposal的字段是否有效
验证背书签名相关性有多种实现方式,通过一致性或是状态更新的隔离保证都可以。串行化是默认的一种隔离保证方式,链码上的背书策略也可以指明了另外一种隔离保证方式。串行化可以通过要求在readset中每个key的版本号必须要等于状态中(KVS)的key的版本号来实现。如果不满足这个条件,就直接拒绝该交易。
需要注意,背书验证失败后,虽然会更新账本中失败交易的bitmask,但是不会更新区块链状态。
这些节点处理好一个给定序列号的广播事件后,结果是这些节点都拥有一样的状态。也就是说,通过ordering服务的保证,所有正确的节点都会接受到来自ordering服务转发的同一个序列号事件(deliver(deqno,prevhash,blob))。由于背书策略和readset中的版本相关性验证都是确定的,所有的节点对于同一个交易处理结果也是一样的,不管这个交易是否有效。因此,所有的节点提交交易和更新状态的行为是一致的。
下面用图来表示上面描述的一种通用交易流程。
说明:背书节点是提交节点的一个子集,这里为了表示有提交功能,所以图示上显示了一个提交节点。其实所有的背书节点也都是提交节点,具有提交功能。但是如果一个节点不部署背书链码,那么它就不具有背书功能。
4、账本
账本提供了一个可证实的历史记录,它记录了对系统操作期间发生的所有成功交易和所有失败交易。账本由ordering服务生成,是一个完全排序的交易块(失败交易和成功交易)的哈希链。链上的每个节点都持有一份账本,部分ordering服务也可以持有账本。如果是ordering服务持有的账本,我们称为这种账本为orderer服务账本,而节点持有的账本称为节点账本,节点账号和orderer账号的区别在于节点账本上维持了一个位掩码来区分哪些交易是有效的,哪些是无效的。同时账本也允许节点回放所有的交易并且重新构造状态。
随着系统运行时间越来越长,无效交易也会随之变多,导致节点账本上存放了一堆无效交易,额外的增加了存储空间,如果这个时间点有新的节点加入到系统,在同步有效交易的同时,也会同步一大堆无效的交易,这也会导致新节点的同步时间增长,同时也会导致验证这些交易时间变长。于是,为了减少存储空间和新节点加入系统的时间和成本,fabric引入了有效账本的概念。
所谓的有效账本是指除了状态和账本外,节点持有一个逻辑账本,只包含有有效并且是已提交的交易。这个哈希链是通过过滤账本上的所有有效交易得到。
生成一个有效账本上的有效交易块的过程如下:
当一个交易在变成有效交易块之前会判断该交易是否有效,如果是无效交易,就被剔除,如果是有效交易则加入进一个有效块(vBlock)中。所有的节点都会在本地进行这样的操作,比如通过使用节点账本的位掩码来过滤。一个有效交易块中不能包含无效交易块,所有无效交易都已经被剔除。这种有效交易块(vBlock)的长度是不固定的。每个节点上的有效交易块(vBlock)被连接起来成为一个哈希链,也就成了一个有效账本。有效账本的每个块包含:
- 前一个有效块的哈希
- 有效块的编号
- 从上一个有效交易块生成后到现在该节点提交的所有有效交易的有序列表
- 在该节点账本中,派生出当前有效交易块的交易块的哈希
所有上述信息都被节点进行哈希索引
账本包含有无效交易,虽然这些交易没有记录的必要,但是节点不会简单的就丢弃节点账本上的这些交易块。因此一旦节点账本生成了相关有效节点,就会对节点账本进行删减。也就是说,在这种情况下,如果有一个新节点计入到这个网络,其他节点就不会发送那些被剔除的交易块到这个新的节点上,也不需要新加入的节点去验证他们的有效交易块。那么节点什么时候生成有效账本呢?于是有了检查点机制,检查点机制通过检查点协议让节点知道什么时候生成有效交易块并去剔除无效的交易块。
检查点协议如下:
节点对每个CHK块周期性地执行检查点,CHK是一个可配置参数。为了初始化一个检查点,节点通过gossip网络广播检查点消息到其他节点,检查点消息为:
其中blockno 是当前交易块号,blocknohash为当前交易块的哈希,stateHash 为最近状态的哈希,peerSig 节点对检查点消息中其他字段的签名。
节点不停的接收检查点消息,直到有足够多的正确签名信息,可以通过这些信息中的blockno,blocknohash和stateHash建立一个有效的检查点。
对块号为blockno ,带有blocknohash的块创建了一个有效检查点后,节点首先检查blockno是否大于最新有效检查点的blockno,如果是,就把最新有效检查点的blockno改成blockno. 之后,存储由各自节点的签名组成了一个有效的检查点到latestValidCheckpointProof。同时存储stateHash响应的状态到latestValidCheckpointProof。最后可选的是否需要修剪节点账本。