《Ethanos: Efficient Bootstrapping for Full Nodes on Account-based Blockchain》是EuroSys 2021一篇关于优化以太坊全节点同步问题的文章,下面我将整理这篇论文的要点以及加上我自己的一些理解。
1 引言
传统以太坊中,一个节点通常需要通过运行从创世块到当前块的所有事务来构建状态树及以太坊中所有的更新历史,从而成为与区块链完全同步的全节点,这需要很长的运行时间以及很大的存储空间。 (Geth) 客户端采用了一种更高效的同步模式,称为快速同步,仅下载枢轴块状态树的快照,当前块后面 64 个块,并且只重放从枢轴块到当前块的交易以重现当前状态树。截止到 2020 年 1 月将存储大小大幅减少到约 235GB,这对于普通客户端来说还是太重了,无法同步。
但是以太坊中超过 95% 的帐户处于休眠状态(不活跃),因此这篇论文提出了一种状态优化技术——Ethanos ,Ethanos定期扫描休眠帐户以减少状态树的大小,将每个周期的状态树大小减少到 220MB 左右,这与 70GB 的完整状态树大小相比很小。
2 以太坊和状态树回顾
2.1 交易状态
以太坊是一个基于账户的交易模型,这个图是一个交易流程的一个简化图。
假设第i个区块以太坊上有5个普通账户,其地址分别为 0xa7d391(a1)、0xa7d397(a2)、0xa7f936(a3)、0xd81355(a4)、0xd815fc(a5),分别模拟两笔交易,第一笔交易如下图所示,区块i+1的交易列表只包含一笔交易:账户a2向a4转账5ETH,要多付0.01交易费,账户a2、a4、a5的余额发生变化,a5为第i+1个区块的矿工,其账户余额增加2.01,其中2ETH是出块奖励、0.01是交易费。 区块i+2的交易列表只包含一笔交易(如下图所示):a4向新账户a6转账1ETH 账户a1、a4、a6的余额发生变化,a1为第i+2个区块的矿工,账户余额增加2.02,其中2ETH是出块奖励、0.02是交易费
2.2 交易树
以太坊使用 Merkle Patricia Trie (MPT) 将帐户状态存储在每个块中,这是一种结合 Patricia trie 和 Merkle 树的数据结构。Patricia trie 是一个保存账户(地址、数据)的搜索树。账户地址的字符中具有公共前缀的帐户共享字典树中相同的子路径,这使得地址搜索比字典树高效 。Merkle 树对每个叶节点进行哈希标记,并用其子节点标签的哈希递归标记每个中间节点,直到标记根节点。
左边的图就是一个MPT,根节点(hashA)是一个分支节点,它将五个地址分为两组:以𝑎开头的地址和以𝑑开头的地址,分支节点的子节点是扩展节点,用于在分支之前添加其后代共有的前缀;例如,通过𝑑(标记为?𝑎𝑠?𝐶)分支后的节点为其后代帐户添加公共前缀81,𝑎4和𝑎5。一个扩展节点的子节点是另一个分支节点,所以这两种节点的交替重复,直到可以区分每个账户的地址,将该账户添加为叶子节点。
右图是这个区块中MPT具体的键值对存储,key存储的是节点的哈希值,value则根据节点的类别存储的不同内容,比如叶子节点存储的是账户的余额和nonce。
下面是状态数的一个更新流程,以太坊不只是更改相应叶节点的哈希值,而是使用新的键(哈希)和值(数据)创建一个新的 trie 节点,然后将其附加到数据库中。这将使父节点也被更新,为其附加一个新的 trie 节点。
2.3 Merkel Proof和Void Proof
保留状态树的更新历史记录允许对帐户进行简单的成员资格检查,即使没有状态树,也只能使用块头。图 1 显示了区块头存储的内容:prev hash(前一个区块的 hash)、tx root(区块体中交易 trie 的 root hash)、state root(状态 trie 的 root hash)。如果给出了默克尔证明,则只能在块头中的状态根中证明特定块中帐户的成员身份。状态树的账户的 Merkle 证明由从根节点到该账户节点的每个节点数据组成。例如在图1中,一个账户𝑎5在区块𝑖的Merkle证明是一个节点数据列表,用B(=B𝑖)表示如下:
𝑃𝑀[𝑖]𝑎5=[B(?𝑎𝑠?𝐴),B(?𝑎𝑠?𝐶),B(?𝑎𝑠?E) ,B(?𝑎𝑠?𝐼),B(?𝑎𝑠?(𝑎5))], 其中B(?𝑎𝑠?𝐴)= {𝑎:?𝑎𝑠?𝐵,𝑑},b(?𝑎𝑠?𝐶)= [?𝑎𝑠?𝐸,81],b(?𝑎𝑠?𝐸),b(?𝑎𝑠?𝐸)= {3:?𝑎𝑠?𝐻 ,5:?𝑎𝑠?𝐼},b(?𝑎𝑠?𝐼)= [?𝑎𝑠?(𝑎5),𝑓𝑐],b(?𝑎𝑠?(𝑎5))= {𝑏𝑎𝑙𝑎𝑛𝑐𝑒:32.01,𝑛𝑜𝑛𝑐𝑒:30}。
然后,如果验证者是给定的 Merkle 证明𝑃𝑀[𝑖]𝑎5,它可以检查𝑎5是否真的存在于区块𝑖以及𝑎5是否具有与𝑃𝑀[𝑖]𝑎5中相同的数据,仅使用区块头的状态根值(状态树本身或块体是不必要的)。也就是说,验证者通过将哈希函数应用于每个节点data in𝑃𝑀[𝑖]𝑎55次从叶子到根来复制哈希键,如下所示: ?𝑎𝑠?(𝑎5)=?𝑎𝑠?({{𝑏𝑎𝑙𝑎𝑛𝑐𝑒:32.01,𝑛𝑜𝑛𝑐𝑒:30}) ,?𝑎𝑠?𝐼=?𝑎𝑠?([[?𝑎𝑠?(𝑎5),𝑓𝑐]),?𝑎𝑠?𝐸=?𝑎𝑠?({3:?𝑎𝑠?𝐻,5:?𝑎𝑠?𝐼}),?𝑎𝑠?𝐶=?𝑎𝑠?([?𝑎𝑠?𝐸,81]),?𝑎𝑠?𝐴=?𝑎𝑠? , 𝑑:?𝑎𝑠?𝐶})。
最后,我们可以通过将?𝑎𝑠?(?𝑎𝑠?𝐴)与状态根进行比较来检查𝑎5的成员资格及其帐户数据。这是有道理的,因为在 Merkle 证明中几乎不可能操纵某些数据(例如,余额、随机数、𝑓𝑐、?𝑎𝑠?𝐻、81、?𝑎𝑠?𝐵)来产生相同的根哈希。
同样,可以使用无效证明来证明帐户的非会员资格。 例如,我们可以生成一个 Void 证明来证明某个帐户 0xa7ec6b (𝑎𝑛) 在状态𝜎𝑖中不存在,如下所示:𝑃𝑉[𝑖]𝑎𝑛=[B(?𝑎𝑠?𝐴),B(?𝑎𝑠?𝐵),B(?ashD)]。 如果验证者获得了 Void 证明,它可以通过对𝑃𝑉[𝑖]𝑎𝑛中的每个项目应用哈希函数来重现?𝑎𝑠?𝐴、?𝑎𝑠?𝐵和?𝑎𝑠?𝐷,以确认证明的有效性。 那么可以知道B(?𝑎𝑠?𝐷)={𝑑:?𝑎𝑠?𝐹,𝑓:?𝑎𝑠?𝐺}不包含key𝑒,也就是说𝜎𝑖没有地址以0xa7e开头的账户。 实际上,我们提出的 Ethanos 有效地利用了 Merkle 证明和 Void 证明来恢复交易(参见第 4 节)。
2.4 全节点的同步方法
全节点的同步方法主要有两种:一个是完全同步,另一个是快速同步
- 完全同步:下载所有块,然后将交易从创世块重放到当前块,以重新生成包括所有状态尝试在内的历史数据;
- 快速同步:下载所有块,以及枢轴块的状态树,并对应但是只重放枢轴块到当前块的交易以生成当前状态树,定义枢轴块为当前块前64个块,但是,快速同步在 2020 年 1 月仍然需要大约 235GB 的巨大空间,并且需要一天时间。在 235GB 中,状态树估计超过 70GB ,其余的以交易数据为主。
3 账户活动时间
追踪从创世区块到 800 万个区块的活跃账户数量,将过去一周发生交易的账户定义为一周活跃账户。过去一个月发生交易的账户定义为月活跃账户,只有 3% 和 5% 的帐户分别在一周和一个月期间处于活动状态。
同时,活动帐户很可能在更新后很快就会更新。一个活动账户在一周内(≈ 40, 320 个区块)更新的概率为 76%,一个月内(≈ 172, 800 个区块)更新后的概率为 89%。
4 Ethanos
任何时间点只有一小部分帐户处于活动状态,同时活跃账户很可能在短时间内发生二次交易。 Ethano技术提出每周或每月定期清理一次休眠帐户,并重建状态树,以此减少存储状态树所需的内存,从而实现更快的同步。
4.1 清除
Ethanos 定义了一个epoch为𝜆,在此之后Ethanos在每个周期内定期扫描休眠账户的块数, 每个周期的最后一个区块为检查点。Ethanos在每个epoch开始时,会创建一个空的状态树,并用新的epoch中活跃的账户填充,由于 trie 最初是空的,因此没有可用的帐户,因此Ethanos也会缓存前一个检查点的状态树,并将其用于查找帐户状态,如果交易涉及的账户在前一个检查点的状态树中存在的话直接复制,并执行最新的交易更新它,如果不存在的话需要缓存它,在当前 epoch 结束时,Ethanos 缓存当前状态 trie,丢弃旧缓存的 trie,然后开始一个新的 epoch 并重复相同的过程。
这个图是 Ethanos 的一个工作流程。 假设
𝑛
𝑡
h
𝑛^{𝑡?}
nthepoch 结束于块𝑖(=𝑛·𝜆),块𝑖为检查点,并带有一个缓存树
σ
n
?
λ
\sigma_{n\cdot\lambda}
σn?λ?,
n
+
1
𝑡
h
n+1_{𝑡?}
n+1th?epoch开始于一个空树。 矿工𝑎5执行从𝑎2到𝑎4的交易以生成区块𝑛·𝜆+1。 从缓存树
σ
n
?
λ
\sigma_{n\cdot\lambda}
σn?λ?中复制𝑎2、𝑎4、𝑎5到当前树
σ
(
n
?
λ
)
+
1
\sigma_{(n\cdot\lambda)+1}
σ(n?λ)+1? ,然后更新。 对于下一个区块𝑛·𝜆+2,交易的发送方𝑎4可以在当前状态树
σ
(
n
?
λ
)
+
1
\sigma_{(n\cdot\lambda)+1}
σ(n?λ)+1?中找到 ,矿工𝑎1可以在缓存状态树
σ
n
?
λ
\sigma_{n\cdot\lambda}
σn?λ?中找到 ,但接收方𝑎6都不是在2个状态树中,所以𝑎6在
σ
(
n
?
λ
)
+
2
\sigma_{(n\cdot\lambda)+2}
σ(n?λ)+2?被创建为一个新帐户。 通过这种方式,可获得一个仅包含在
n
+
1
t
h
n+1^{th}
n+1th epoch期间活跃的账户的状态树,它会被缓存到下一个检查点
σ
n
?
(
λ
+
1
)
\sigma_{n\cdot(\lambda+1)}
σn?(λ+1)?。
4.2 休眠账户的恢复
如果一个发生交易的账户既不在当前状态树中也不在缓存状态树中,则定义为休眠账户,如果它是交易的发送者或接收者,则应首先重新激活它。有两种情况 如果是交易的发送者,应该首先传输恢复交易,这将在当前的 状态树中创建该账户在其最后一个交易时的状态。 如果账户是交易的接收方,也有两种情况。 1、该帐户确实是一个休眠帐户,2、该帐户是一个新帐户,因为在以太坊中,当有人向一个不存在的任意地址发送一些值时,该地址的新帐户会在状态树中创建,并且nonce为零,而不管是否存在,如在 Ethanos 中都会创建一个新帐户,我们称之为碎屑帐户。
如果 crumb 账户有足够的余额,它可以成为另一个交易的发送者,但如果没有,它还应该传输一个恢复交易,这会将 crumb 账户(可以在多个 epoch 中存在多个实例)与原始账户合并。通过这种方式,Ethanos 可以降低恢复事务的频率。
4.2.1 恢复交易
恢复交易由休眠账户的所有者发起,由存档节点代表所有者创建和传输,账户所有者首先确定其账户是不是休眠账户,然后存档节点收到账户所有者发来的恢复交易请求后,通过访问相关检查点的状态并收集基本数据创建恢复事务,包括:
- 账户在最后一个活动检查点的 Merkle 证明
- 从最后一个活动检查点到最新检查点的每个检查点的帐户空证明
- 在每个检查点的块头中保存了Bloom filter,可减少90%的空证明
4.2.2 使用 Crumb 帐户恢复交易
常规的 Ethano 完整节点无法判断一个帐户是 crumb 还是新帐户,如果简单地将 crumb 帐户视为新帐户并将 nonce 设置为 0,则攻击者可能会传输过去已经执行的事务,即 replay 攻击,以再次接收值。 为了消除这种攻击的可能性,Ethanos 将任何帐户的初始 nonce 设置为当前块数 (k) 乘以每个帐户每个块的平均交易,该值将始终超过以前出现过的任何 nonce 值。
恢复交易仅合并最后一个crumb账户以及原始账户。这种做法会存在一种风险,使用 crumb 账户的恢复交易可能允许交易中有两个 Merkle 证明𝑃𝑀1 和𝑃𝑀2,这将被解释为𝑃𝑀1 是最后一个活动账户,𝑃𝑀2 是 crumb 账户。但是,如果𝑃𝑀1是旧的活跃账户,而𝑃𝑀2是𝑃𝑀1的恢复账户,这种情况下就会再次添加𝑃𝑀1的余额。因此需要通过在帐户状态中添加一位已恢复标志来解决此问题,其默认值为 0,但在帐户恢复时由验证者设置为 1。a图中最后一个crumb是合法的 crumb 帐户,𝑃𝑀1 和𝑃𝑀2 可以合并和恢复,恢复帐户的标志设置为 1。如果𝑃𝑀2 的标志为 1,如图b ,验证者将拒绝已恢复的作弊账户作为 crumb 账户。
4.3 同步
Ethanos 有两种同步模式。 一种是存档同步,与以太坊中的完全同步相同。 它下载所有块并重放/验证从创世块到当前块的所有交易以重现当前状态树。
另一种是紧凑同步,和以太坊的快速同步一样。快速同步下载枢轴块的状态树并仅重放从枢轴块到当前块的交易以重现当前状态树。 紧凑同步还下载最新检查点的状态树将其作为缓存状态树,但是不会重放交易,并省略了从创世块下载旧事务到主块; 如果需要,客户端可以向其他存档节点请求旧交易。 尽管紧凑同步应该下载2棵状态树,但2棵状态树都只包括当前epcho和之前一个epoch的活动帐户,因此它们会很小,在实验中约为220MB。
5 实验评估
使用第700 万个区块到 730 万个区块间的 30 万个以太坊区块评估了 Ethanos,在这期间,发生了 30,575,237 笔交易、执行更新 4,588,065 个账户。(同步开始以太坊)。实验前提:
- 将每笔交易的价值设为零以避免余额不足引起的异常
- 因为无法对每笔交易进行签名,因此将每个事务大小增加 20 个字节。将合约账户 (CA) 视为外部拥有账户 (EOA),因此涉及智能合约发送 Ether 的交易被视为常规交易
- 将epoch设置为40320个区块(约等于一周)
- 主机有 i7-9700K 3.60GHz CPU、64GB RAM、6TB SSD,以及 c客户端有 i7-7700 3.6GHz CPU、16GB RAM、1TB SSD。
5.1 存储大小
5.1.1 存档同步
Geth 和 Ethanos 从创世块到 300K 块的归档同步的总数据大小,分别为 52.0GB 和 46.7GB。大小减少了 5.3GB(超过 10%) Trie 节点大小减少了近 6GB。 布隆过滤器将块头(Headers)的大小增加了 10MB
5.1.2 快速同步和紧凑同步
快速同步下载所有交易,但仅根据枢轴块(也已下载)的状态树将交易从枢轴块重放到当前块。 紧凑同步执行相同的操作,只是它省略了下载在枢轴块之前发生的事务。 Geth 和 Ethanos 之间的唯一区别是,枢轴块的状态树的快照是由检查点 (Geth) 之前的所有帐户组成还是仅由一个周期内的活跃帐户组成。
由图可知对于快速同步,Ethanos 减少 状态树的大小对快速同步的总存储大小(6.0GB 与 5.9GB)几乎没有影响,因为当从创世块传递仅 300K 块时,交易大小显然主导了状态树大小。然而,在它削减事务大小后,它对紧凑同步的总存储大小(977MB 与 374MB)的影响要大得多。
5.2 同步时间
图 8 显示,是四种同步状态在每个周期所需要花费的时间,epoch 7 的 Geth-fast 和 Ethanos-fast,数据大小几乎相同,但 Ethanos 的同步时间缩短了 17%。同样,Ethanos-compact 的数据大小是 Geth compact 的一半,但同步时间缩短了 75%。
5.4 每个检查点上的账户类型
每个帐户在其 epoch 期间访问的类型分为四种类型:缓存、新建、crumb和恢复。 cached: 如果在最后一个表示上一个检查点所缓存的状态树中可用的点 new: 表示从创世块到当前块从未被使用过的账户 crumb: 前面说的碎屑账户 restored: 通过恢复交易将crumb与原账户合并的账户 在所有检查点中,缓存帐户的数量几乎保持不变,这意味着许多帐户在检查点之间被重用。
5.5 恢复交易的开销
左图描述了每个 epoch 中执行的恢复事务和正常事务的数量。它表明恢复事务是部分执行的,不到所有事务的 1.5%。
中间的图,显示了正常和恢复事务的大小分布图,正常交易的大小是恒定的(0.12KB),但恢复交易的大小超过 3KB 并且多种多样,因为恢复交易必须包括至少一个 Merkle 证明(≈ 3KB)和多个 Void 证明。需要的证明越多会增加恢复事务的大小。
右边的图描述了正常事务和恢复事务的执行时间。正常事务的执行时间平均不到一毫秒,而恢复事务的时间要长得多,
因为我们必须访问所有检查点,直到遇到最后一个活动检查点,搜索布隆过滤器并验证 Merkle/Void 证明在每个检查站。同时右边的图,使用 7epoch 的数据估计了恢复交易如何影响区块链性能。这里,一个区块平均执行 248 个正常交易和 2.7 个恢复交易,使用图 10? 估计其执行时间分别为 99.2 和 10.5 毫秒。因此,还原事务会将事务的平均执行时间增加 10%。然而,以太坊的区块间隔是 15 秒,所以交易时间的增加与挖矿时间相比微不足道。
5.6 Epoch长度评估
评估了四个时期的长度:三天、一周、两周和一个月,因此,在30w区块中分别进行了 50、20、10 和 5 个 epoch。分别获得每个 epoch 中不同账户的数量/分布
当 epoch 长度增加时,每个 epoch 的活跃账户数量也会增加,但比例较小
图 12 显示了每个时期的恢复事务数。它还描述了五个时间间隔的累积数量(例如,三天时期检查点 50 处的条形表示检查点 40 和 50 之间的累积数),可以看出更长时期长度的更大状态尝试避免了原本需要的恢复事务
|