架构起源¶
注解
本文档描述了 Hyperledger Fabric v1.0的最初架构提案。虽然从概念上讲,Hyperledger Fabric 实现是从架构方案开始的,但是在实现过程中还是修改了一些细节。最初的架构方案是按照最初的准备提出的。有关架构的更准确的技术描述,请参阅 Hyperledger Fabric: A Distributed Operating System for Permissioned Blockchains 。
Hyperledger Fabric 架构有以下优势:
- 链码信任的灵活性。该架构将链码(区块链应用程序)的*信任假设*与排序的信任假设分开。换句话说,排序服务可能由一组节点(排序节点)提供,并可以容忍其中一些节点出现故障或不当行为,而且每个链码的背书者可能不同。
- 可扩展性。由于负责特定链码的背书节点与排序节点是无关的,因此系统的 扩展性 会比由相同的节点完成这些功能更好。特别是当不同的链码指定不同的背书节点时,这会让背书节点的链码互相隔离,并允许并行执行链码(背书)。因此,代价高昂的链码执行从排序服务的关键路径中删除了。
- 机密性。本架构有助于部署对其交易的内容和状态更新具有“机密性”要求的链码。
- 共识模块化。该架构是 模块化的,允许可插拔的共识算法(即排序服务)实现。
第一部分:与 Hyperledger Fabric v1相关的架构元素
- 系统架构
- 交易背书的基本流程
- 背书策略
第二部分:v1之后架构版本的元素
- 账本检查点(裁剪)
1. 系统架构¶
区块链是一个分布式系统,由许多相互通信的节点组成。区块链运行链码来保存状态和账本数据,并执行交易。链码是核心元素,因为交易是调用链码的操作。交易必须“背书”,只有背书的交易才可以提交并对状态产生影响。可能存在一个或多个用于管理方法和参数的特殊链码,称之为 系统链码。
1.1. 交易¶
交易可以分为两类:
- 部署交易,创建新的链码并将程序作为参数。当部署交易成功执行时,链码会被安装在区块链上。
- 调用交易,在部署的链码环境中执行操作。调用交易引用链码和它的方法。当成功时,链码执行指定的方法(这可能涉及修改相应的状态),并返回输出。
后边会讲到,部署交易是调用交易的特殊情况,创建新链码的部署交易是系统链码上的调用交易。
备注:本文档目前假设交易要么创建新的链码,要么调用一个已部署链码提供的操作。这个文档还没有描述:a)查询(只读)交易的优化(包含在v1中),b)对跨链码交易的支持(v1之后版本的特性)。
1.2. 区块链数据结构¶
1.2.1. 状态¶
区块链的最新状态(简称 状态)被建模为一个带有版本的键值存储(KVS),其中键是名称,值是任意的。这些条目由区块链上运行的链码(应用程序)通过 put 和 get KVS 的方式维护。状态被持久地存储,并记录对状态的更新。注意,带有版本的 KVS 被用作状态模型,实现可以使用已有的 KVS,也可以使用 RDBMS 或任何其他解决方案。
更正式地说,状态 s 被映射为 K -> (V X N) 的一个元素,其中:
K是键的集合V是值的集合N是一个带有版本号的有序集合。单映射函数next: N -> N接收N并返回下一个版本号。
V 和 N 都包含一个特殊的元素 ⊥ (空类型),以防 N 是最小值。初始时所有键都映射为(⊥, ⊥)。对于 s(k)=(v,ver) ,我们用 s(k).value 表示 v, 用 s(k).version 表示 ver。
KVS 操作模型如下:
put(k,v)其中k∈K,v∈V,获取区块链状态s并改变为s'就是s'(k)=(v,next(s(k).version)),当k'!=k时s'(k')=s(k')。get(k)返回s(k)。
状态由 Peer 节点维护,而不是排序节点和客户端。
状态隔离。KVS 中的键可以从它们的名称中识别出属于某个特定链码,因为只有特定链码的交易才可以修改属于这个链码的键。原则上,任何链码都可以读取属于其他链码的键。支持跨链码交易,可以修改属于两个或多个链码的状态,这是一个v1后续版本的特性。
1.2.2 账本¶
账本提供了一个可验证的历史记录,记录了在系统运行期间发生的所有成功的(有效的)和失败的(无效的)状态更改。
账本是由排序服务(参见第1.3.3节)构造的,它是(有效或无效的)交易的 区块 的完全有序哈希链。哈希链强制账本中的区块的顺序,每个区块包含一个完全有序的交易数组。这强制为所有交易指定了顺序。
账本保存在所有的 Peer 节点上,也可以选择保存在部分排序节点上。在排序节点的上下文中,我们将账本称为“排序节点账本”,而在 Peer 节点的上下文中,我们将账本称为“节点账本”。Peer 节点账本 与 排序节点账本 的不同之处在于,Peer 节点在本地维护一个位掩码,该位掩码将有效的交易与无效的交易区分开来(有关详细信息,请参阅XX节)。
如第 XX 节(v1后续版本特性)所述的,Peer 节点可以裁剪 节点账本。排序节点维护“排序节点账本”以获得容错性和(“Peer 节点账本”的)可用性,并可以决定随时对其进行裁剪,前提是排序服务的属性得到了维护(参见第1.3.3节)。
账本允许节点重放所有交易的历史并重建状态。因此,1.2.1节中描述的状态是一个可选的数据结构。
1.3. 节点¶
节点是区块链的通信实体。“节点”只是一个逻辑功能,因为不同类型的多个节点可以运行在同一个物理服务器上。重要的是节点如何在“信任域”中分组并与控制它们的逻辑实体相关联。
有三种类型的节点:
- 客户端 或 提交客户端:是一个向背书节点提交实际交易调用并向排序服务广播交易提案的客户端。
- Peer 节点:是一个提交交易并维护状态和账本(参见第1.2节)副本的节点。此外,Peer 节点可以扮演一个特殊的 背书人 角色。
- 排序服务节点 或 排序节点:是一个运行实现交付担保的通信服务节点,例如原子性或总顺序广播。
接下来将更详细地解释节点的类型。
1.3.1. 客户端¶
客户端扮演了代表最终用户的实体。它必须连接到与区块链通信的 Peer 节点。客户端可以连接到它所选择的任何 Peer 节点。客户端创建并调用交易。
如第2节所详细介绍的,客户端同时与 Peer 节点和排序服务通信。
1.3.2. Peer 节点¶
Peer 节点接收来自排序服务 区块 形式的有序状态更新,并维护状态和账本。
Peer 节点还可以承担 背书节点 或 背书人 的特殊角色。背书节点 发生在一个特定的链码上,包括在提交事务之前 背书 交易。每个链码都可以指定一个 背书策略,该策略可以引用一组背书节点。策略为有效的交易背书定义了必要条件和充分条件(通常是一组背书人的签名),后面的第2和3节将对此进行讲解。在安装新链码的特殊部署交易的情况下,背书策略指定为系统链码的背书策略。
1.3.3. 排序服务节点(排序节点)¶
排序节点 来自于 排序服务 ,即提供交付担保的通信结构。排序服务可以以不同的方式实现:从集中式服务(例如,在开发和测试中使用)到针对不同网络和节点故障模型的分布式协议。
排序服务为客户端和 Peer 节点提供共享的 通信通道,为包含交易的消息提供广播服务。客户端连接到通道,并可以在通道上向所有 Peer 节点广播消息。该通道支持所有消息以 原子 方式传递,即消息通信是全顺序传递和(特定实现)可靠的。换句话说,通道将消息以相同的逻辑顺序输出给所有与之相连的 Peer 节点。这种原子通信保证在分布式系统中也称为 全顺序广播、原子广播 或 共识。所通信的消息是要包含在区块链状态中的候选交易。
分区(排序服务通道)。排序服务可能支持多个 通道,类似于发布者-订阅者消息系统的 主题。客户端可以连接到给定的通道,然后可以发送消息并获取到达的消息。通道可以看作是分区,连接到一个通道的客户端不知道其他通道的存在,但是客户端可以连接到多个通道。尽管 Hyperledger Fabric 中实现的一些排序服务支持多个通道,但为了简化表示,在本文档的其余部分中,我们假设排序服务由一个通道(主题)组成。
排序服务 API。Peer 节点通过排序服务提供的接口连接到通道。排序服务 API 由两个基本操作(通常称 异步事件)组成:
TODO 添加用于在客户端或 Peer 节点获取指定序列号的区块的 API 部分,。
broadcast(blob):客户端调用它来在通道上广播任意消息blob。在 BFT 中,当向服务发送请求时,也称为request(blob)。deliver(seqno, prevhash, blob):排序服务调用这个来向 Peer 节点发送消息blob,该消息中包含非负整数序列号(seqno)和上一个发送的blob的哈希(prevhash)。欢句话说,它是排序服务的输出事件。deliver()在发布者-订阅者系统中称为notify(),在 BFT 系统中称为commit()。
账本和区块格式。账本(参见第1.2.2节)中包含了所有排序服务输出的数据。简单来说,它是一个 deliver(seqno, prevhash, blob) 事件的序列,而事件就是根据前面所说的 prevhash 计算的哈希链。
大多数时候,出于效率的考虑,排序服务不会输出单个交易(blob),而是在单个 deliver 事件中将交易分组并输出到 区块 中。在这种情况下,排序服务必须限定每个区块中交易的排序。区块中的交易数可以由排序服务动态选择。
为了便于讲解,下边我们定义了排序服务属性(本节剩余部分)并解释了交易背书工作流(第二节),其中我们假设每个 deliver 事件中只有一个交易。这些很容易扩展到区块上,根据上面提到的区块中交易的确定性顺序,假设一个区块的 deliver 事件对应于一个区块中的每个交易单独的 deliver 事件序列。
排序服务属性
排序服务(或原子广播通道)的保证规定了广播了什么消息,以及传递的消息之间存在什么关系。这些保证如下:
安全(一致性保证):只要 Peer 节点连接到通道的时间足够长(它们可以断开连接或崩溃,但会重新启动和重新连接),它们将看到一个 相同的 已交付的
(seqno, prevhash, blob)消息序列。这意味着所有 Peer 节点都可以收到 相同顺序 的输出(deliver()事件),并且相同的序列号都有 相同的内容 (blob和prevhash)。注意,这只是一个 逻辑顺序,一个 Peer 节点上的``deliver(seqno, prevhash, blob)`` 不需要与另一个 Peer 节点上输出相同的deliver(seqno, prevhash, blob)的消息发生实时关联。换句话说,给定一个特定的seqno,没有 两个正确的 Peer 节点会提供 不同的prevhash或blob值。此外,除非某个客户端(Peer 节点)实际调用了broadcast(blob),否则不会传递任何blob,即每个广播过的 blob 只分发 一次。此外,
deliver()事件包含前一个deliver()事件中的数据的哈希(prevhash)。当排序服务实现原子广播保证时,prevhash是deliver()事件的参数和序号seqno-1的哈希。这将在deliver()事件之间建立一个哈希链,用于帮助验证排序服务输出的完整性,第4和第5节将讨论这个。在第一个deliver()事件中prevhash有一个默认值。存活性(交付保证):排序服务的存活性保证由排序服务决定。准确的保证取决于网络和节点故障模型。
原则上,如果提交的客户端没有失败,那么排序服务应该确保连接到排序服务的每个正确的 Peer 节点最终会发送每个提交的交易。
总而言之,排序服务确保以下特性:
- 协议。对于任何两个正确的 Peer 节点上有相同
seqno的事件deliver(seqno, prevhash0, blob0)和deliver(seqno, prevhash1, blob1),prevhash0==prevhash1并且blob0==blob1; - 哈希链完整性。对于任何两个正确的 Peer 节点上的事件
deliver(seqno, prevhash0, blob0)和deliver(seqno, prevhash1, blob1),prevhash = HASH(seqno-1||prevhash0||blob0) - 不能跳跃。如果排序服务向正确的 Peer 节点 p 输出了
deliver(seqno, prevhash, blob),其中seqno>0。那么 p 肯定已经接收到了deliver(seqno-1, prevhash0, blob0)。 - 不能创建。正确的Peer 节点上的
deliver(seqno, prevhash, blob)事件必须在一些(可能不是同一个) Peer 节点上通过broadcast(blob)事件处理。 - 没有重复(可选,但最好存在)。对于两个事件
broadcast(blob)和broadcast(blob'),当两个事件deliver(seqno0, prevhash0, blob)和deliver(seqno1, prevhash1, blob')在正确的 Peer 上发生时,并且blob == blob',那么seqno0==seqno1而且prevhash0==prevhash1。 - 存活性。如果一个正确的客户端调用一个事件
broadcast(blob),那么每个正确的 Peer 节点“最终”都会发出一个事件deliver(*, *, blob),其中*表示一个任意值。
2. 交易背书的基本流程¶
在下面的文章中,我们将概述交易请求的整体流程。
注: 注意以下协议并不假设所有交易都是确定性的,即它允许非确定性交易。
2.1. 客户端创建一个交易并将其发送给它所指定的背书节点¶
要执行一个交易,客户端需要向其指定的背书节点发送一个 PROPOSE (提案)消息(可能不是同时。请参见2.1.2和2.3节)。客户端可以通过 Peer 节点根据背书策略(参阅第三章)得到给定 chaincodeID 的背书节点。例如,交易可以发送给指定 chaincodeID 的 所有 背书节点。也就是说,一些背书节点可以离线,其他的可能不同意或者不背书该交易。提交客户端可以尝试可用的背书节点来满足背书策略。
下边我们将首先详细介绍 PROPOSE 消息格式,然后讨论提交客户端和背书节点之间可能的交互模式。
2.1.1. PROPOSE 消息格式¶
PROPOSE 消息的格式是 <PROPOSE,tx,[anchor]>,tx 是必选参数而 anchor 是可选参数,解释如下。
tx=<clientID,chaincodeID,txPayload,timestamp,clientSig>,其中 -clientID是提交客户端的 ID, -chaincodeID是提交的交易所引用的链码, -txPayload是提交的交易所包含的内容, -timestamp是由客户端维护的单调递增(对于每一个新交易)的整数, -clientSig是客户端对tx其他字段的签名。在执行交易和部署交易中
txPayload的细节所有不同,对于 执行交易,txPayload包含两个字段:txPayload = <operation, metadata>,其中 -operation定义了链码方法和参数, -metadata定义了执行相关的参数。
对于 部署交易,``txPayload`` 包含三个字段: -
txPayload = <source, metadata, policies>, 其中source定义了链码的源码,metadata定义了相关的链码和应用程序,policies包含了链码相关的策略,比如背书策略,它可以被所有 Peer 节点访问。 注意,在部署交易中txPayload不包含背书策略,但是包含背书策略 ID 和它的参数(参见第三章)。
anchor包含了 读版本依赖项,具体来说就是“键值-版本”对(即anchor是KxN的子集),它将PROPOSE请求绑定或者“锚定”在 KVS(参见 1.2)中指定的键的版本上。如果客户端指定了anchor参数,背书节点仅在其本地 KVS 和anchor对应键的 读 版本号相匹配时才背书交易(更多的细节参见第2.2节)。
所有节点都是用 tx 的哈希作为交易标识符 tid,即 tid=HASH(tx)。客户端将 tid 保存在内存中,等待背书节点的响应。
2.1.2. 消息模式¶
客户端决定和背书节点交互的顺序。例如,客户端通常会把 <PROPOSE, tx>``(即没有 ``anchor 参数)发送到一个节点,客户端稍后会将生成的版本依赖(anchor)作为 提案 消息的参数发送到其他背书节点。另外一个例子就是,客户端也可以直接将 <PROPOSE, tx>``(没有 ``anchor 参数)直接发送给背书节点。客户端可以自由选择不同的通信模式(参看2.3节)。
2.2. 背书节点模拟交易并生成背书签名¶
当收到客户端发来的 <PROPOSE,tx,[anchor]> 消息后,背书节点 epID 首先验证客户端的签名 clientSig 然后再模拟交易。如果客户端指定了 anchor,那么背书节点只会在它读取到本地 KVS 中 anchor 所指定键值的版本号(即后边会介绍的 readset )后才会模拟交易。
模拟交易是背书节点调用交易中引用的链码(chaincodeID)和背书节点本地的状态副本来 执行 交易 (txPayload)的过程。
执行的结果是背书节点计算出来的 读版本依赖*(``readset``,读集)和 *状态更新 (writeset,写集 ),在数据库语言中也称为 MVCC + postimage info。
回想一下,状态由键值对组成。所有键值条目都是带有版本的;也就是说,每个条目都含有有序的版本信息,每次更新键对应的值时,版本号都会递增。执行交易的节点保存着链码用于读取或写入的所有键值对,但节点还没有更新状态。具体来说:
- 在背书节点执行交易前给出一个状态
s,其中保存着交易读取的所有键k。(k,s(k).version)被添加到readset中。 - 另外,交易将所有键
k的值改变为新值v',(k,v')被添加到writeset中。
如果客户端在 PROPOSE 消息中指定了 anchor,那么客户端指定的 anchor 必须和背书节点模拟交易时的 readset 一致。
然后节点在内部根据 背书逻辑 向其他背书节点转发 tran-proposal (或者叫做 tx)。默认情况下节点的背书逻辑只接收并背书 tran-proposal。然而背书逻辑可以解释任何功能,比如,以 tran-proposal 和 tx 作为输入和系统交互来判断是否能够背书一笔交易。
If endorsing logic decides to endorse a transaction, it sends
<TRANSACTION-ENDORSED, tid, tran-proposal,epSig> message to the
submitting client(tx.clientID), where:
如果背书逻辑决定背书一笔交易,它会发送 <TRANSACTION-ENDORSED, tid, tran-proposal,epSig> 消息给提交客户端 (tx.clientID),其中:
tran-proposal := (epID,tid,chaincodeID,txContentBlob,readset,writeset)`, ``txContentBlob是链码(交易)指定的信息。目的是让txContentBlob和tx有相同的表达方式(例如txContentBlob=tx.txPayload)。epSig是背书节点在tran-proposal上的签名。
另外,当背书逻辑拒绝为交易背书时,背书节点 可能 会给提交客户端发送一个 (TRANSACTION-INVALID, tid, REJECTED) 消息。
注意,背书节点在这一步不会改变状态,在背书环境中模拟执行交易产生的结果不会影响状态!
2.3. 提交客户端收集交易背书并向排序服务广播¶
提交客户端一直等待接收到“足够多”的消息和 (TRANSACTION-ENDORSED, tid, *, *) 的签名后才可以确认交易提案背书完成了。就像在2.1.2节中所讨论的,这一步可能会和背书节点有多次交互。
“足够多”的含义取决于背书策略(参见第三章)。如果满足了背书策略,就表明交易被 背书 了。注意,还没有提交。从背书节点收集的 TRANSACTION-ENDORSED 消息的签名就称为 背书。
如果提交客户端没有收到交易提案的背书,它就会放弃该交易,并且可以选择稍后重试。
对于成功背书的交易,我们现在就要开始使用排序服务了。提交客户端通过 broadcast(blob) 调用排序服务,其中 blob=endorsement。如果客户端不能直接调用排序服务,它可以通过其他节点代理它的广播。这个节点必须是客户端信任的节点,确保节点不会从 endorsement 中删除任何信息,否则交易会被验证失败。需要提醒的是,代理节点无法伪造有效的 背书。
2.4. 排序服务将交易发送给节点¶
当发生 deliver(seqno, prevhash, blob) 事件,并且节点上的状态已经更新到比 seqno 小的序号时,节点会有如下操作:
- 它会根据链码(
blob.tran-proposal.chaincodeID)的背书策略来检查blob.endorsement的有效性。 - 一般情况下,它还会验证依赖项(
blob.endorsement.tran-proposal.readset)没有被改变。在更复杂的用例中,背书中的tran-proposal字段可能会不一样,这时背书策略(参见第三章)会决定对状态的操作。
依赖项的验证根据状态更新选择的一致性属性或者“隔离保证”可以有多种不同实现。有序性 是默认的隔离保证,除非背书策略指定了一个。当所要求的 readset 中 每一个 键的版本和状态中键的版本一致的时候就提供了有序性,并将拒绝不符合要求的交易。
- 如果通过了所有检查,就认为交易是 有效的 或者是 已提交的。这时,节点会在
节点账本的位掩码中将该交易标记为1,将blob.endorsement.tran-proposal.writeset应用到区块链账本(如果tran-proposals是一致的,否则背书策略逻辑会让函数验证blob.endorsement)。 - 如果
blob.endorsement的背书策略验证失败,交易就是无效的并且节点会在节点账本的位掩码中将该交易标记为0。有必要提醒一下,无效交易不会改变状态。
注意,这足够使所有(正确的)节点在处理完给定序号的区块后得到相同的状态。也就是说,通过排序节点的保证,所有正确的节点都将收到相同顺序的 deliver(seqno, prevhash, blob) 事件。无论交易是否有效,通过背书策略和 读集 中的版本依赖节点都将得到一样的结果。因此,所有节点以同样的方式提交和应用相同顺序的交易来更新它们的状态。
Figure 1. 一般的交易流程示意图。
3. 背书策略¶
3.1. 背书策略说明¶
背书策略 是 背书 一笔交易的条件。区块链节点预置了一些背书策略,用来处理安装特定链码的 部署 交易。背书策略可以通过 部署 交易来指定。
为了保证安全性,背书策略 应该是一组被证实过的策略,其中包含一组有限的方法,以此确保执行时间可控,可以出现确定性结果,有良好的性能以及拥有安全保证。
动态添加背书策略(例如,在安装链码时的 部署 交易)会影响其安全性。目前不允许动态添加背书策略,以后会增加这项功能。
3.2. 根据背书策略的交易评估¶
只有当交易的背书满足背书策略时交易才是有效的。链码的执行交易会首先获得符合链码策略的 背书,否则不会被提交。这个过程发生在提交客户端和背书节点之间,详细过程参见第二章。
从形式上来讲,背书策略是背书的依据,并且背书策略更进一步的评估状态是正确的还是错误的。对于部署交易,背书包含在系统层面的策略中(例如,来自系统链码)。
背书策略的依据是引用特定的变量。它可能引用:
- 和链码相关的键或标示(可以在链码的元数据中找到),例如,背书者集合;
- 链码更进一步的元数据;
endorsement和endorsement.tran-proposal中的元素;- 其他元素。
上边列出的内容是根据易读性和负责性排序递增的顺序排序的,也就是说,只引用键和节点标示的策略会相对简单。
背书策略的评估标准必须是确定的。背书的评估可能在每一个本地节点上执行,这些节点 不 必要和其他节点交互,但所有正确的节点仍以相同的方式评估背书。
3.3. 背书策略示例¶
背书策略的条件可能会包含用来判断正确和错误的逻辑语句。一般来说,判断条件会使用交易中的签名,该签名由链码的背书节点签发。
假设链码指定了背书者集合 E = {Alice, Bob, Charlie, Dave, Eve, Frank, George}。以下是一些示例策略:
- 同一个
tran-proposal上有效签名的条件是:E 中所有成员都签名。 - 有效签名的条件是:E 中任何一个成员签名。
- 同一个
tran-proposal上有效签名的条件是:(Alice OR Bob) AND (any two of: Charlie, Dave, Eve, Frank, George)。 - 同一个
tran-proposal上有效签名的条件是:包含七个背书节点中的任意五个。(一般来说,对于一个n > 3f个背书者的链码来说,n个节点中有2f+1个节点签名就算有效,或者 多于(n+f)/2个背书节点。) - 假设背书者有一个
权重,比如{Alice=49, Bob=15, Charlie=15, Dave=10, Eve=7, Frank=3, George=1}总权重是100,有效签名的条件是权重中的大多数(比如,多于50的权重),例如{Alice, X}和 George 之外的任何X,或者{everyone together except Alice}。等等。 - 上边所提到的权重可以是静态的(固定在链码元数据中)也可以是动态的(例如,根据在执行过程中链码的状态)。
- 有效签名的条件是:
tran-proposal1满足Alice OR Bob并且tran-proposal2满足(any two of: Charlie, Dave, Eve, Frank, George),其中tran-proposal1和``tran-proposal2`` 的区别在于背书节点和状态更新。
这些策略的用处取决于应用程序,关系到当背书节点故障、作恶或者出现其他状况时系统的弹性。
4 (v1之后)。 已验证账本和节点账本检查点(裁剪)¶
4.1. 已验证账本(Validated ledger,VLedger)¶
为了维护只包含了有效的和已提交的交易的账本的抽象,Peer 节点在状态和账本之外维护了一个 已验证账本。这是从账本中过滤掉无效交易之后的哈希链。
已验证账本区块(VLedger blocks, vBlocks)处理过程如下。因为 节点账本 可能包含无效交易(例如,交易的背书无效或者依赖版本无效),这些交易在加入到 vBlock 之前就别过滤掉了。每个节点自己完成这一步(例如,根据 节点账本 中相关的掩码)。已验证账本区块的定义是,不包含无效交易的区块。这些区块的大小是动态的并且可能为空。vBlock 的结构定义如下图:
图2。已验证账本区块(vBlock)和账本(PeerLedger)区块结构的区别。
vBlock 是通过每一个 Peer 节点连接在一起的哈希链。确切地说,每一个 vBlock 包含:
- 前一个 vBlock 的哈希。
- vBlock 序号。
- 上一个 vBlock 生成之后所有已提交交易的有序列表。
- 生成当前 vBlock 的相关区块(在
PeerLedger中)哈希。
所有这些信息连接在一起并由 Peer 节点计算哈希,从而得到已验证账本中 vBlock 的哈希。
4.2. PeerLedger 检查点(Checkpointing)¶
包含无效交易的账本没有必要永久保存。但是 Peer 节点不能简单地丢弃 PeerLedger 区块,因此当构造完相应 vBlock 之后会对 PeerLedger 进行裁剪。也就是说,在这种情况下,如果一个新节点加入到了网络中,其他节点不能向新节点发送将会丢弃的区块,也不能向新节点证明它们的 vBlock 的有效性。
针对裁剪 PeerLedger,本文档简介了 检查点 机制。这个机制建立了跨节点网络的 vBlock 验证并允许带检查点的 vBlock 替换丢弃的 PeerLedger 。这样就不用存储无效交易,减少了存储空间。同样也减小了新加入的节点重新构建状态的工作量(它们在重新执行 PeerLedger 中的交易来重构状态的过程中,不用验证每笔独立交易的有效性,但是可能需要需要重新执行已验证账本中的状态更新)。
4.2.1. 检查点协议¶
每一个 CHK 区块,Peer 节点都会定期执行检查点,CHK 是一个可配置参数。要初始化一个检查点,Peer 节点要想其他节点广播消息 <CHECKPOINT,blocknohash,blockno,stateHash,peerSig>,其中 blockno 是当前区块号,blocknohash 是区块哈希,stateHash 是根据区块 blockno 的验证得到的最新状态(比如,Merkle 哈希)的哈希,peerSig 是 Peer 节点在 (CHECKPOINT,blocknohash,blockno,stateHash) 上的签名,表明这是已验证的账本。
节点收集 验证点 消息,直到它收集到了足够多正确的和 blockno、 blocknohash 、 stateHash 相匹配的签名信息,然后它就会创建一个 有效的检查点 (参见 4.2.2 节)。
一个节点要对包含 blocknohash 的区块号 blockno 创建一个有效的检查点,它就要:
- 如果
blockno>latestValidCheckpoint.blockno,节点要指定latestValidCheckpoint=(blocknohash,blockno), - 将构成一个有效检查点的相关节点的签名集合保存在
latestValidCheckpointProof中, - 将和
stateHash相关的状态保存在latestValidCheckpointedState中, - (可选)修剪到区块号为
blockno(包含) 的PeerLedger。
4.2.2. 有效检查点¶
显然,检查点协提出了以下问题:什么时候节点可以裁剪它的 ``PeerLedger`` ?多少 ``CHECKPOINT`` 信息是 “足够多”? 。这些定义在 检查点有效性策略 中,其中包含(至少)两种方案,可以是组合使用:
- 本地(特定节点)检查点有效性策略(Local checkpoint validity policy,LCVP)。*在一个给定节点 *p 的本地策略中,可以指定节点 p 新人的节点集合和谁的
CHECKPOINT消息可以满足构件有效检查点。例如,Alice 节点的 LCVP 定义为 Alice 需要接收到 Bob 的或者 Charlie 和 Dave 两个人的CHECKPOINT消息。 - *全局检查点有效性策略(Global checkpoint validity policy,GCVP)。*检查点有效性策略可以指定为全局的。这和本地节点策略类似,只是这个是规定在系统(区块链)粒度,而不是节点粒度。GCVP可以像这样定义:
- 所有节点会信任经过 11 个不同节点的确认的检查点。
- 在一些特定的部署场景下,在同一个机制(例如,信任域)中,每一个排序节点都搭配了一个 Peer 节点,其中 f 个节点可能会发生(拜占庭)错误,所有节点会信任经过 f+1 个和排序节点搭配的不同节点确认的检查点。