本文对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。计算状态根哈希,并在数据库中维护两个表(TrieOfAccounts
和 TrieOfStorage
),从而更高效地进行状态根哈希的计算。
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。处理来自 AccountChangeSet
和 StorageChangeSet
表的数据并创建两个反向索引(作为 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。处理来自 BlockBody
和 EthTx
表的数据并持久化映射,以便通过其 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!