Erigon阶段同步与控制流

  • Erigon
  • 发布于 2022-05-05 14:24
  • 阅读 59

本文对Erigon的ETH核心组件内部结构进行了详细分析,着重介绍了Stage Loop的功能和各个阶段的处理流程,包括Headers、Block Hashes、Bodies、Senders等阶段的数据库操作。同时提到了一些设计挑战,如控制流与数据传输的并行处理,以及即将到来的POS过渡的复杂性。

快速跟进之前的帖子:在完全重新同步最新的 alpha 版本 2022.05.02 后,数据库的理论数据量为 1015 Gb(不包括 225 Gb 的区块快照),而实际数据量为 1083 Gb,其中 12 Gb 是尚未重复利用的空闲空间。

在这篇文章中,我们将更详细地研究架构图中“ETH core”组件的内部结构,该图在之前的帖子中展示。内部有我们称之为“Stage Loop”的内容。以下是图示:

这里有两个主要的控制流 - 一个通过黄色箭头表示,另一个通过蓝色箭头表示。Stage loop 由一个单独的 goroutine(线程)驱动,它重复经过所谓的阶段。以下是每个阶段的简短描述:

  • Headers。请求 ETH sentry 的区块头,接收、验证并将其持久化到数据库中的 3 个表中(完整表格列表可以在 erigon-lib 库的 kv 包中的 file tables.go 找到):

    • Headers: height|hash => RLP 编码的头部

    • HeaderTD: height|hash => RLP 编码的 total_difficulty

    • HeaderCanonical: height => hash

  • Block Hashes。计算反向映射(并将其持久化到一个数据库表中):HeaderNumber: hash => height。将其与 Headers 阶段单独执行更有效,因为哈希不是单调的,所以将它们插入数据库表的效率最高,当哈希首先被预排序时。

  • Bodies。请求与规范区块哈希对应的区块体,接收、验证并将其持久化到数据库中的 2 个表中:

    • BlockBody: height => (start_tx_id; tx_count)

    • EthTx: tx_id => RLP 编码的 tx

  • Senders。处理交易的数字签名(ECDSA)并“恢复”相应的公钥,因此,为每个交易持久化“From”地址到数据库中的一个表:

    • Senders: height => sender_0|sender_1|…|sender_{tx_count-1}
  • Execution。重放所有交易并计算所谓的“Plain State”,以及 2 种类型的变更日志,和收据及事件日志,所有这些持久化到数据库中的表中。它还创建一个临时表,这在 Call Trace Index 阶段中后来使用。

    • PlainState: account_address => (balance; nonce; code_hash)account_address|incarnation|location => storage_value

    • PlainContractCode: account_address|incarnation => code_hash

    • Code: code_hash => contract_bytecode

    • AccountChangeSet: height => account_address => (prev_balance; prev_nonce; prev_code_hash)

    • StorageChangeSet: height => account_address|location => prev_storage_value

    • Receipts: height => CBOR 编码的收据

    • Log: height|tx_index => CBOR 编码的事件日志

    • CallTraceSet: height => account_address => (bit_from; bit_to)

  • Hashed State。仅存在于为下一个阶段提供输入数据。读取 AccountChangeSet 和 StorageChangeSet 表中的新记录,以确定添加到状态的新账户地址和存储位置,并向两个表添加条目,这些表的内容与 PlainState 类似,只是映射来自“哈希键”,账户和存储项则在单独的表中:

    • HashesAccounts: keccak256(account_address) => (balance; nonce; code_hash)

    • HashedStorage: keccak256(account_address)|incarnation|keccak256(location) => storage_value

  • Trie。计算状态根哈希,并在数据库中维护两个表(TrieOfAccountsTrieOfStorage),从而更高效地进行状态根哈希的计算。

  • Call Trace Index。处理临时表 CallTraceSet 的数据并创建两个反向索引(由 roaring 位图表示):

    • CallFromIndex: account_address => bitmap of heights where account has “from” traces

    • CallToIndex: account_address => bitmap of heights where accounts has “to” traces

  • History Index。处理来自 AccountChangeSetStorageChangeSet 表的数据并创建两个反向索引(作为 roaring 位图):

    • AccountHistory: account_address => bitmap of heights where account was modified

    • StorageHistory: account_address|location => bitmap of heights where storage item was modified

  • Log Index。处理来自 Log 表的数据并创建两个反向索引(作为 roaring 位图):

    • LogAddressIndex: account_address => bitmap of heights where account is mentioned in any event logs

    • LogTopicIndex: topic => bitmap of heights where topic is mentioned in any event logs

  • Tx Lookup。处理来自 BlockBodyEthTx 表的数据并持久化映射,以便通过其 tx 哈希查找交易:

    • TxLookup: tx_hash => height

每当控制流到达任何阶段时,它总是尝试处理此阶段当前可用的所有数据。这意味着在初始启动期间,Headers 阶段将尝试下载所有现有的区块头,Bodies 阶段将尝试下载所有相应的区块体,依此类推。在控制流再次返回到 Headers 阶段之前可能需要几个小时,到那时又会有更多的头可用,因此该过程会重复,但处理的头数量较少,然后是区块。最终,这些重复集中处理 1 (或有时更多) 的区块,因为这些区块是由网络生产的。

另一个控制流由蓝色的箭头表示,它由来自其他对等方的数据驱动,由 sentry 管理。接收新生成的区块,或应请求的区块头和区块体的响应,可能会发生在 Stage Loop(黄色控制流)偏离“Headers”或“Bodies”阶段的时刻,这些阶段能够相应地“摄取”新的头和区块体。为了不阻塞蓝色控制流,提出了所谓的“Exchange”数据结构。它们允许蓝色控制流存入数据,而黄色控制流则能够取出数据。当然,这些交换需要设计得不消耗无限内存,但同时最小化重复请求的数量以及最新头和区块的交付延迟。换句话说,这并不简单,遗憾的是,实施中仍然存在一些错误。

合并

“合并”(即 POS 过渡)的设计在上面的图中引入了另一个第三个控制流。它是与共识层(Consensus Layer)进行通信的控制流。Staged Sync、Sentry 流和 CL 流之间的交互细节仍在研究中,希望会在另一篇帖子中描述。正如人们所猜测的,事情变得越来越复杂……


  • 原文链接: erigon.substack.com/p/er...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Erigon
Erigon
高效以太坊客户端,Go 编写