本文探讨了以太坊 Gloas (EIP-7732) 提案对“最终性”定义的深层影响。文章指出,由于信标块与执行负载的可用性可能分离,协议最终确认的是信标块根而非完整的负载状态。作者详细分析了这一变化对 Beacon API、引擎 API 以及证明验证逻辑的技术挑战,并提出了相应的处理规则。

目录
以太坊提供了一个强有力的保证,即诚实节点绝不会 重组 (Reorg) 一个已 Finalized 的 Checkpoint。此外,如果两个相互冲突的 Checkpoint 被 Finalized,FFG(以太坊的最终性组件)意味着至少 1/3 的总质押是 可被罚没的 (Slashable)。这是通常的 加密经济 安全陈述。
本文讨论的是一个更窄的问题:当以太坊说一个 Checkpoint 已 Finalized 时,该保证实际上保护了哪些状态?
在纪元边界区块缺失的情况下,这个问题在今天已经具有一定的微妙性。Gloas 使其变得更加复杂,因为 信标区块可用性 (Beacon-block availability) 和 Payload 可用性 (Payload availability) 现在可能会出现分歧。特别是,EIP-7732 迫使我们要明确一个 Finalized 的 Checkpoint 到底承诺了什么,以及没有承诺什么。
下文讨论使用以下术语:
full、empty 或 missed:full 意味着信标区块加 Payload 都可用;empty 意味着信标区块可用但没有包含 Payload;missed 意味着没有信标区块可用。在运行良好的链中,Slot 92 的验证者在该 Slot 的区块到达后进行 Attestation:
92
...
65
64
...
32
...
对 Slot 92 的绿色区块进行的 Attestation 唯一确定了 Slot 64 的目标区块。如果在第 2 个 Epoch 期间收集到足够的此类投票,那么当链在 Slot 96 进入第 3 个 Epoch 时,区块 64 变为 Justified。此时,之前 Justified 的 Checkpoint,即区块 32,变为 Finalized。
在 Slot 96 之后、Slot 128 之前,如果诚实节点通过标准的 Beacon API 终端点 被询问其 Finalized Checkpoint,它将返回区块 32 的 后置状态 (Post-state)。该保证适用于该 Finalized Checkpoint 的内容:
诚实节点永远不会考虑不包含区块 32 的链。即使后来有另一条链被 Finalized,或者即使 100% 的验证者集都是恶意的,诚实节点也永远不会接受一条排除区块 32 的链。
如果另一条已 Finalized 的链确实排除了区块 32,那么至少 1/3 的总质押是 可被罚没的 (Slashable)。
现在考虑 Slot 64 的纪元边界区块缺失或被 重组 (Reorg) 掉的情况:
92
...
65
63
64
...
32
...
在这里,对区块 92 的投票隐式地将区块 63 设置为目标,因为 63 是上一个 Epoch 中的最后一个区块。如果收集到足够的此类投票,当链越过 Slot 96 时,区块 63 变为 Justified,当链随后在 Slot 128 进入第 4 个 Epoch 时,它变为 Finalized。
但这种情况包含比正常路径更多的信息。验证者不仅在为区块 63 投票。他们 还 在发出 Slot 64 为空的信号。所以有效的保证更强:
诚实节点永远不会考虑不包含区块 63 且 Slot 64 为空的链。他们将拒绝以下两种情况:
如果另一条已 Finalized 的链要么排除了区块 63,要么在 Slot 64 包含了区块,那么至少 1/3 的总质押是 可被罚没的 (Slashable)。
同样的逻辑延伸到多个缺失的 Slot。如果最后一个可用的区块是 62,那么验证者将同时 Finalized 区块 62 以及 Slot 63 和 64 处区块的缺失。
在上述边界缺失的情况下,节点应该返回什么作为 Finalized 状态?
仅返回最后一个可用区块(区块 63)的 后置状态 (Post-state) 会丢失信息:它没有反映出 Slot 64 已知为空的事实。历史解决方案是返回区块 63 的 后置状态 (Post-state),但将其推进到 Slot 64。该状态捕获了这两个事实:
这就是为什么 Checkpoint 同步标准化为纪元边界状态,而不是最后一个区块的原始 后置状态 (Post-state)。参见 此 Beacon API 讨论。
到目前为止,微妙之处在于纪元边界处是否存在区块。Gloas 引入了一种不同的歧义:信标区块 和 执行 Payload (Execution payload) 现在可以分离。
在 Gloas 中,分叉选择 (Forkchoice) 节点可能会将一个 Slot 视为:
missed,和今天一样,当没有信标区块可用时;full,当信标区块及其 Payload 都可用时;或者empty,当信标区块可用但未包含 Payload 时。这很重要,因为 Attester 不会对当前 Slot 的 Payload 进行 Attestation,但他们确实间接地对早期 Slot 的 Payload 可用性 进行 Attestation。有关完整理由,请参阅 注释版分叉选择规范。在这里,我们只需要理解 Finalization 所需的最低限度知识。
再次考虑同样的正常路径图示:
92
...
65
64
...
32
...
对区块 92 的 Attestation 仍然唯一标识了 Slot 64 的目标区块。在 Gloas 下,它还有效地标识了 Slot 64 的 Payload 状态,除了在 Slot 64 本身期间进行 Attestation 的委员会。那些验证者投票支持 64 同时作为 Head 和 Target,但他们 尚未 对 64 的 Payload 做出声明。该 Epoch 中的 每一个后续委员会 都会做出这样的声明。
因此,到链达到 Slot 96 时,Attestation 包含足够的信息来同时使以下内容 Justified:
然而,信标状态并不存储这些完整信息。为了直接在链上保留它,规范需要扩展 Checkpoint 本身,例如:
class Checkpoint(Container):
epoch: Epoch
root: Root
block_hash: Hash32
这种方法被认为对客户端太具侵入性,因此 Checkpoint 结构保持不变。结果,只有 Justification/Finalization 信息中的 信标区块 (Beacon-block) 部分存储在信标状态中。
在 Slot 96 进入第 3 个 Epoch 后,Justified Checkpoint 包含足够的信息来使 Slot 64 处的信标区块 Justified,但 不足以 提供协议状态信息来使其 Payload Justified。
这是关键区别:
在 Gloas 下,Attestation 可能包含足够的信息来重建 Payload 可用性,但 协议 Finalized 的对象仍然是存储在信标状态中的 Checkpoint 根。换句话说,即使 Payload 派生状态 尚未 Finalized,信标区块 也可能已经 Finalized。
这导致了一些尴尬但必要的 API 决策:
safe 区块哈希应引用 Slot 63 的 Payload,而不是 Slot 64。该行为正确地编码了协议保证。Finality 适用于 Slot 64 处的 信标区块,而不是 Slot 64 处由 Payload 派生的状态。
在 Slot 64 被 Finalized 之后,诚实节点将永远不会接受排除 Slot 64 处信标区块的链。但他们仍可能接受在 Slot 64 的 Payload 上有所不同的链。
现在回到 Slot 64 没有信标区块的情况:
92
...
65
63
64
...
32
...
在这种情况下,所有较早的 Payload 可能都可用,但 Slot 64 处的信标区块(以及因此 Slot 64 的任何 Payload)都缺失了。
在这里,Attestation 实际上说明了很多。即使是 Slot 64 处的委员会,也通过设置 attestation.data.index = 1 投票支持区块 63 及其 Payload。后续委员会通过在区块 65 之上构建来支持同样的 Payload。因此,仅从 Attestation 中,人们就可以恢复关于区块 63 的 Payload 状态的强有力证据。
但再一次,这与协议 Finality 不是 一回事。存储在信标状态中的 Checkpoint 仍然只承诺 Checkpoint 根。它 不 承诺节点可能从 Attestation 离线重建的 Payload 状态。
因此,即使在这种情况下,实际上 1/3 的 Attester 揭示了 Payload 信息,区块 63 的 Payload 仍然 不是 协议 Finalized 的内容。
这就是 Gloas 之前的规则失效的地方。
在 Gloas 之前,将区块 63 的 后置状态 (Post-state) 推进到 Slot 64 是安全的,因为推进后的状态捕获了 Slot 64 为空这一被 Finalized 的事实。在 Gloas 下,推进状态现在可能隐式地在 相互竞争的 Payload 解释 之间做出选择。
出现了两个糟糕的选项:
所以现在的正确规则要严格得多:
唯一可以安全返回的 Finalized 状态是根为 Justified/Finalized Checkpoint 根的区块的 信标区块后置状态 (Post-beacon-block state)。在 Gloas 下,该状态 不应 被推进到纪元边界。
这是本文核心的操作性结论。
节点可能仍会问:如果我能很好地在本地跟踪 Payload 信息,为什么不公开更强的状态呢?
原则上,记录了足够多来自链上 Attestation 的离线信息的节点可以重建一个更强的感知 Payload 的视图。在正常情况下,它可以返回 Slot 64 完整的 Payload 后置状态。在边界缺失的情况下,它可以返回推进到 Slot 64 的区块 63 的 Payload 后置状态。
问题不在于该重建是否通常正确。问题在于它比信标状态本身 Finalized 的内容 更强。
如果不同的节点对同一个已 Finalized 的 Checkpoint 根在本地重建了不同的 Payload 状态,那么它们可能会提供不兼容的答案,同时仍然在协议 Finalized 的内容上达成一致。这在 Engine API 的任何地方都是不可接受的,甚至在 Checkpoint 同步中也是不可取的。
Engine API 的后果是重要的一个:
对于 Engine API 调用,safe 哈希应 始终 是包含在 Justified 区块中的竞价 (Bid) 的父哈希。
对于 Checkpoint 同步或调试终端点,影响较小,但并非为零。不同的 Payload 解释可能会改变执行请求,从而改变某些证明,或导致同步节点最初遵循错误的分支。
on_attestation 也有一个规范影响。
今天,store_target_checkpoint_state 返回推进到纪元边界的后置 CL 状态,然后该状态在验证 Attestation 签名时被使用。但是,一旦 FFG/LMD 一致性已经确保了相同的信标区块根,剩下的歧义正是上面讨论的 Gloas Payload 歧义。
该状态稍后用于计算委员会:
def get_indexed_attestation(state: BeaconState, attestation: Attestation) -> IndexedAttestation:
"""
返回对应于 ``attestation`` 的索引 Attestation。
"""
attesting_indices = get_attesting_indices(state, attestation)
return IndexedAttestation(
attesting_indices=sorted(attesting_indices),
data=attestation.data,
signature=attestation.signature,
)
这最终取决于 get_beacon_committee,它取决于 Epoch Seed 和活跃验证者索引。Seed 不受目标 Payload 是否存在的影响,但如果 Payload 中的执行请求触发了存款、退出、取款或合并,活跃验证者集可能会发生变化。
只有当目标 Slot 和 Attestation Slot 之间缺失足够多的 Slot,使得这些变化变得重要时,这一点才会显现出来,但在这种情况下,区别是真实的:带有 Payload 的目标状态和不带 Payload 的目标状态可能意味着不同的活跃验证者索引。
因此,规范可能应该在两个方面进行更改:
beacon_block_root 的目标状态,而不是存储在 Store 中缓存的 Checkpoint 状态。full,在另一个分支中是 empty,并且目标状态相应地有所不同。信标节点通常使用 依赖根 (Dependent root) 的概念来决定跨两个不同分支的两个信标状态是否共享某些不变量,如提议者预测 (Proposer lookahead) 或信标委员会等。这些依赖根被选为该 Epoch 的 最后一个信标区块根。在 Gloas 之前,共享最后一个信标区块根将保证任何两个分支在下一个 Epoch 具有相同的预状态 (Pre-state),但现在情况不再如此,因为最后一个信标区块的 Payload 实际上可能会更改下一个区块(在下一个 Epoch 中)的预状态。因此,我们要分析从 Epoch $e$ 到 Epoch $e+1$ 到底会发生什么变化。
令 B 为 Epoch $e$ 上最新的信标区块,令 P 为其 Payload。令 E 和 F 分别是通过处理来自 B 和 P 的后置状态到 $e+1$ 的 Slot 而获得的状态。我们想了解 E 和 F 之间的主要可能差异。我们有明显的更改,如 latest_block_hash 等,但我们主要对可以更改信标委员会、提议者预测等的操作感兴趣。这些更改将来自处理 P 上的 执行请求 (Execution requests),因为这些更改将包含在 F 中,但不会包含在 E 中。
Builder 存款会立即处理并添加到注册表中,因此 F 可能会有更多的 Builder 及其余额。但这通常不会影响委员会/提议者和验证者职责。
验证者存款在处理 P 时作为 PendingDeposit 附加到状态,它们的 Slot 是 B 的 Slot。
全额退出将调用 initiate_validator_index,后者又使用退出余额调用 compute_exit_epoch_and_update_churn。最早可能的退出 Epoch 是 $e+5$,状态的最早退出 Epoch 更新为此。退出会将验证者的退出 Epoch 设置为此退出 Epoch(至少为 $e+5$),将其可取款 Epoch 设置为至少 $e+261$。
待处理的部分取款会被附加。同样,这些可取款 Epoch 至少为 $e+261$。
在切换到复利请求时,会为所有高于最低激活余额的余额添加一个带有 GENESIS_SLOT 的待处理存款。
对于成功的合并请求,源验证者的退出 Epoch 设置为至少 $e+5$,状态的最早合并 Epoch 设置为至少 $e+5$,验证者的可取款 Epoch 设置为至少 $e+261$。状态中会添加一个待处理合并。
到目前为止,我们分析了由于在 B 之上应用或不应用来自 P 的请求而导致的状态主要差异。两个最终状态将通过对这两个不同的状态执行纪元转换而产生差异。纪元转换将在以下方面有所不同:
如果 P 包含了退出,并且有一些验证者跌破了驱逐余额,这些验证者可能会被设置为不同的退出 Epoch,无论如何,这个退出 Epoch 至少会是 $e+5$。
任何 Slot > 0 的已应用待处理存款都不会被应用,因为 Slot 无法被 Finalized。
如果在 P 中将验证者切换为复利验证者而产生的 GENESIS_SLOT 存款,如果 Churn 尚未耗尽,则将被应用。在这种情况下,验证者的余额将再次增加,因此与 B 的状态没有区别,除了验证者已切换为复利验证者。如果已达到 Churn,则 P 之后该验证者的余额将减少到 32ETH,并且待处理存款将保留。
由于尚未达到可取款 Epoch,因此不会处理来自 P 的待处理合并。
这是可能出现问题的地方!来自实际上持有超过 32ETH 的验证者的合并转换请求,将导致该验证者的有效余额变得不同,因为有效余额会从之前的最高 32ETH 发生变化。然而,这要求验证者起初拥有超过 32.25ETH。
因此,我们看到,除去验证者持有超过 32.25ETH 并在 P 上切换到复利这一潜在问题,从 $e$ 到 $e+1$ 的转换中,有效余额和活跃验证者集不可能有任何差异,因此 E 和 F 状态共享相同的活跃验证者索引,并共享相同的有效余额。
由于上述相同的原因,这至少会持续 5 个以上的 Epoch。因此,只要没有超过 4 个 Epoch 缺失区块,这些状态就会保持相同的活跃验证者和有效余额(请注意,除非包含区块,否则从 P 中包含的切换到复利验证者实际上无法增加有效余额)。
需要提议者预测的验证不会改变,来自 F 或 E 的提议者预测完全相同。需要活跃验证者和活跃验证者余额的验证也无法从 F 改变到 E。
唯一 的变化可能是,如果 F 和 E 随后都在未包含任何区块的情况下被推进到未来的 Epoch。我们看到,这至少需要 5 个 Epoch 才能使这些状态产生差异。因此,当出现 5 个 Epoch 缺失区块,然后链突然恢复并立即使作为目标的区块 B 变为 Justified 时,可能会出现问题。未来的链如果使用推进后的 F 或推进后的 E,将具有不同的 Justified 余额集。
- 原文链接: potuz.net/posts/gloas-fi...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!