端到端时序
阶段 1:执行与追踪
写节点通过 P2P 正常同步和执行区块。pipeline tracer 以tracing.Hooks 的形式挂在 EVM 上,在执行过程中收集数据。
OnCommit 和 OnBlockDBStart 是 Leafage 对上游 Geth 的扩展,定义在 core/tracing/hooks.go,由 core/blockchain.go 在 ProcessBlock 中分发。
StateDiff 有两种获取方式:默认从
OnCommit 拿到的 StateDB 提交集直接转换;配置 enable_prestate_tracer 后改用 prestate tracer 在指令级采集。前者更准确也更省开销,后者用于没有 commit hook 的客户端。阶段 2:序列化与分发
OnCommit 里并发执行四路上传(tracer/pipeline_tracer.go):
上传完成后,写节点在确定新的 canonical head 时(
core/blockchain.go 的 writeBlockAndSetHead)向 Kafka 发布通知。发布前会用 getCommonAncestor 对比上一条已发布的区块与当前 head:
- 只有新块 →
changeType: 1,newBlocks按高度升序 - 存在被丢弃的分支 →
changeType: 2,同时带dropBlocks和newBlocks
阶段 3:状态摄入
leafage-evm 的KafkaUpdater(bin/leafage-evm/src/updater/kafka_updater.rs)消费通知:
1
解析通知
从
newBlocks 拿到区块哈希与父哈希。2
并行拉取 S3
按哈希取 Header,按 state root 取 StateDiff。父块与当前块 state root 相同时跳过 diff 拉取,视为空差异。
3
更新 StateTree
调用
tree.update_block(block_info, block_diff),把新差异层挂到链表头部。4
提交 offset
落盘成功后写入
offset_dir,用于崩溃恢复。阶段 4:终结化
当区块深度超过--diff-depth-limit(默认 64)时,最老的一层被刷写到 RocksDB 并从内存移除。
- State 节点:直接覆盖写
address/address || slot,只保留最新值。 - Archive 节点:双写。
address || block_num保留历史版本,address || u64::MAX作为最新值快速路径。历史查询用 RocksDB 的seek_for_prev定位不大于目标高度的最近版本。
阶段 5:查询服务
nodex-proxy 解析请求里的区块参数,结合 etcd 中的lastBlockNumber 决定节点池,再按权重或轮询选出具体节点。leafage-evm 在本地状态上用 revm 执行并返回结果。
两个自动故障转移路径:
并行链路:一致性校验
consistency-checker 消费同一条 Kafka 通知,但不在 leafage-evm 的数据路径上。它的处理顺序(check/check.go 的 Process):
1
消息校验与去重
重复消息或已处理过的消息走对齐后直接推进,避免重试死锁。
2
预取 S3
在等待副本的同时并行预取 BlockValidation 和同高度的键列表。
3
等待副本收敛
以
check_interval_ms(默认 20ms)轮询所有副本的 eth_blockNumber,直到 ready_ratio(默认 0.8)的副本追上该高度,或超过 check_timeout_ms(默认 2000ms)判失败。4
写入本地 Pebble DB
双索引:
h{hash} → BlockInfo,n{number} → hash,供 JSON-RPC 查询。5
标记分叉
列出 S3 外部桶中同高度的所有 BlockValidation 对象,把非规范的重写为
is_fork: true。6
发布外部通知
先发 drop 通知,再发新块通知,写入外部 Kafka topic。
7
发布后复核
用一次新鲜的 LIST 再检查一遍同高度的分叉标记,捕捉等待副本期间新上传的对象。
stateType(1 追上 / 2 落后 / 3 离线)以及链的 lastBlockNumber。这就是 nodex-proxy 的路由依据。
Reorg 的三处处理
Leafage 在三个层面分别处理链重组,理解它们的分工可以避免误判问题出在哪一层。写节点:产生 dropBlocks
写节点:产生 dropBlocks
writeBlockAndSetHead 用 getCommonAncestor 比较上一条已发布的区块和新 head,把从共同祖先到旧 head 的路径作为 dropBlocks,到新 head 的路径作为 newBlocks,以 changeType: 2 一次性发出。leafage-evm:分叉层与安全回补
leafage-evm:分叉层与安全回补
StateTree 用两张表索引差异层:
hash_diff_map 保存所有块(含分叉块),num_diff_map 只跟踪规范链。因此按哈希查询仍可访问分叉状态,按高度查询始终走规范链。S3 追赶时靠近链头的区块可能落在错误的分支上。--catchup-safe-depth 指定链头附近多少个区块改为沿 Kafka 通知里的精确 parent-hash 链回补,而不是按高度索引查。该值应大于目标链的最大 reorg 深度(例如 Moonriver 设 64),0 表示禁用。consistency-checker:标记与外部通知
consistency-checker:标记与外部通知
收到
changeType: 2 时先重写 dropBlocks 对应的 BlockValidation 为 is_fork: true,再扫描同高度的其他对象。另有周期巡检(fork_scan_interval_sec,默认 60 秒,回看 fork_scan_lookback 个高度)兜底。外部消费者通过 OuterBlockChangeNotification 的 is_fork 字段感知。冷启动与追赶
leafage-evm 启动时按持久化的 Kafka offset 决定路径:{chainID}[/{version}]/{height}/ 列出该高度的对象,取 is_fork 为 false 的那个哈希,再按哈希去内部桶取 Header 和 StateDiff。批大小由 --init-task-queue-size(默认 256)控制。
全新节点的常规恢复方式是下载 RocksDB 快照后再追赶 Kafka,通常在分钟级完成,而不是从创世块重放。