深入探讨以太坊验证者生命周期

深入探讨以太坊验证者生命周期

简介

以太坊如今实现了最去中心化的权益证明共识。当前活跃的验证器钥匙数量超过 10^6,每个区块由数百个验证器进行证明。Eth 2.0 的验证基础设施复杂,分为信标层和执行层,并包含许多有趣的机制。这个复杂的系统在“动态”中不断更新,增加了显著的复杂性。考虑到这一点,我们决定填补以太坊共识、经济安全和验证的高层文档与在实际生产中运行的真实代码之间的空白。本文的目的是让读者全面了解以太坊验证器的工作原理,并为希望深入研究的人提供真实代码的链接。

本文并不打算作为估计验证器奖励的指南。如你所见,以太坊的共识中有太多因素,无法提供直接的“算法”答案。验证器的奖励受到验证器数量、有效余额的行为、存款、退出、提现、惩罚、bug 以及许多其他因素的影响。这些答案最好通过分析真实验证器的行为和统计数据来揭示。如果你需要验证器维护的财务分析,探索聚合验证器活动信息的资源更为实际,例如 beaconcha.in

所以,我们有一个验证器,并希望参与世界上最去中心化的共识。让我们开始吧!

高层审视

在深入了解验证器生命周期的细节之前,首先从更高的层面描述它。信标验证器周期始于验证器被提升到活跃状态。这个周期包括加入队列、被接受(或拒绝)、开始操作、成为一个Epoch验证器、在Epoch中验证和证明到来的区块、构建并发布新块、因之前发布的块和证明获得奖励、在惩罚的情况下失去余额,并最终退出。

信标链验证器的实现有多种编程语言可供选择。你可以在官方以太坊文档中找到它们的列表和链接 这里。在本文中,我们将专注于 Lighthouse 实现及其文档。

需要注意的是,以太坊 2.0 的共识是在不断发展的。许多参数、限制和逻辑组件在每次硬分叉中都会发生变化。已经发生了几个信标链的硬分叉,我们将在本文中提到。在撰写本文时,已经有四个主要的硬分叉,另外还有两个计划实施。这些的规范可以在 这里 找到。这些硬分叉修改了验证逻辑,包括对常量、时间窗口、惩罚、限制和信标链状态结构的更新。一些硬分叉甚至启用或禁用整个过程(例如,存款或提现)。在可能的情况下,我们将突出这些变化与验证器状态的关系。现在,我们简单描述这些硬分叉引入的主要变化及其时间线,因为我们将在后面的文章中引用这些名称。

Phase0 – 未来升级的准备阶段,引入主要的信标链状态和权益证明(PoS)设计。大多数基础规范在这个硬分叉中建立。 Altair – 继续为从工作量证明(PoW)迁移到权益证明(PoS)做准备。这个硬分叉引入了轻客户端协议和“同步委员会”,这是一个由 512 个验证器在每个“同步委员会周期”(约 1 天)中伪随机选择的特殊子集。这些验证器不断签署新的信标区块头,使得“外部”的轻客户端能够以显著简化的复杂度验证共识状态。 Bellatrix – 这个硬分叉标志着正式转向权益证明,使用在早期硬分叉中引入的规范。它作为“合并”的“开启”开关。 Capella – 这个硬分叉启用了验证器的提现,这在之前的版本中是禁用的。它通过允许验证器提取奖励,最终确定了以太坊 PoS 的完整经济模型。 Deneb – 在撰写本文时最新的硬分叉(2025 年 1 月)。它将信标区块根添加到以太坊虚拟机(EVM),这是跨链证明以太坊共识的重要变化,并更改了一些与证明和激活/退出限制相关的值。 Electra – 一个未来的硬分叉,将验证器的最大有效余额从 32 ETH 增加到 2048 ETH,允许整合许多冗余的验证器。此外,它引入了对存款和提现的重大更改,使其更快、更灵活。 Fulu – 目前正在建设中。

不要将这些硬分叉与执行层的硬分叉混淆,如巴黎、柏林或上海。在以太坊 2.0 中,信标链通过将区块处理外包给执行层,并“提取”和“注入”存款和提现,与执行层进行交互。它旨在为未来以太坊信标链与多个执行层(分片)交互的能力做准备。因此,这些层的逻辑可以独立更新。这导致形成互补的硬分叉对,例如“上海/Capella = Shapella”或“布拉格/Electra = Pectra”。为了帮助记住信标链硬分叉的顺序,请注意信标链硬分叉名称的首字母按字母顺序排列:(A)ltair,(B)ellatrix,(C)apella,(D)eneb,依此类推。

这些更新的范围广泛,本文的许多部分与特定的硬分叉相关。我们将努力在适当的部分澄清这些连接。

信标状态

首先,我们需要回顾保存信标链状态信息的关键结构:BeaconState。我们将省略硬分叉引入的某些更改,以更好地说明 BeaconState 中包含的数据:

class BeaconState(Container):
# 版本控制
genesis_time: uint64
genesis_validators_root: Root
slot: Slot
fork: Fork

# 历史
latest_block_header: BeaconBlockHeader
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]

# Eth1
eth1_data: Eth1Data
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
eth1_deposit_index: uint64

# 注册
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]

# 随机性
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]

# 惩罚
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]

# 参与
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]

# 确认
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH]
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint

# 不活动
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]  

# 同步委员会 [Altair 中新]
current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee

# 执行 [Bellatrix 中新]
latest_execution_payload_header: ExecutionPayloadHeader  

# 提现 [Capella 中新]
next_withdrawal_index: WithdrawalIndex
next_withdrawal_validator_index: ValidatorIndex

Beacon State 中对我们特别重要的部分是:

# Eth1,包含最新达成一致的执行层状态。 # Registry,保存验证者及其质押余额的列表。 # Slashings,跟踪每个Epoch的总惩罚金额。 # Participation,涵盖验证者在证明中的参与情况。 # Finality,提供关于最终状态的数据。 # Withdrawals,保存关于提款的详细信息。

关于 Beacon State 的优秀注释及其在硬分叉中所做的更改可以在此处获得。在继续之前,强烈建议你查看文档的这一部分。

以太坊区块链通过对 Beacon State 的一致更新在信标节点上进行进展。这些更新根据从 p2p 网络接收的新数据修改状态根、验证者列表、证明以及最终状态。验证者状态的行为可以描述为与 Beacon State 特定部分的互动。

要实践了解 Beacon State 结构,你可以浏览其在 LighthousePrysm中的实现。

[注意] 在本文中,我们将频繁提到信标链验证者节点的代码。虽然我们无法完全描述所有底层逻辑,但我们将突出最相关的部分以供演示。在某些情况下,我们将展示说明验证者生命周期各个方面的代码,即使它与实际共识检查并不直接相关。这是因为这些部分通常更具信息性。有时,我们会遇到同一操作的不同实现变体(“单次处理”/“带缓存”或“信标层”/“执行层”等)。对于每种情况,我们将尝试关注最具示范性的代码片段。

验证者实体

首先,让我们讨论验证者实体。验证者的主要标识符是其公共 BLS 密钥(作为标识符的 pubkey 用法示例见此处 )。信标链验证者不使用以太坊的标准 secp256k1 ECDSA 签名(和地址)。相反,他们使用 BLS 签名。这个选择是因为 BLS 签名允许将多个签名聚合成一个签名。这使得使用一个聚合签名来验证数百甚至数千个预聚合签名成为可能,显著提高了区块“重放”的效率。此外,BLS 签名的确定性特性使其可以用于验证者的伪随机洗牌,从而便于创建不可预测的验证者时间表(例如,为下一个Epoch的每个时隙选择验证者组)。

与传统的 secp256k1 签名相比,BLS 签名在大小上更小,签名过程更快,尽管验证速度较慢。然而,这种较慢的验证通过签名聚合的效率得到了缓解。有关详细说明,请参见 eth2book 中的此处IETF 网站。关于 BLS 签名的文章也有很多,这些签名在阈值方案和各种共识算法中得到广泛应用。

一个示例结构展示了验证者的生命周期,可以在此处看到:

pub struct Validator {
    pub pubkey: PublicKeyBytes,
    pub withdrawal_credentials: Hash256,
    pub effective_balance: u64,
    pub slashed: bool,
    pub activation_eligibility_epoch: Epoch,
    pub activation_epoch: Epoch,
    pub exit_epoch: Epoch,
    pub withdrawable_epoch: Epoch,
}

提取质押和奖励需要 withdrawal_credentials 字段,该字段可以是 BLS 或 Eth1 格式(在 Capella 硬分叉中添加)。该字段表示 BLS 或 secp256k1 公钥,并以前缀 0x00(BLS)或 0x01(Eth1)定义。更多信息可见此处

另一个重要字段是 effective_balance,它代表验证者的“活跃”余额,并决定其在协议中的影响。如同其他质押协议一样,该余额作为验证者的“权重”。在以太坊 2.0 中,我们验证者的有效余额用于计算:

  • 被选为区块提議者的概率
  • 在 LMD-GHOST 分叉选择规则中的验证者权重
  • 在证明和最终状态计算中的验证者权重
  • 在同步委员会中的包含概率
  • 惩罚和不活跃罚款金额

虽然区块、证明、惩罚和同步委员会包含奖励对于所有验证者而言使用相同的基础奖励,但因更高有效余额而导致的更高参与概率会导致更高的累计奖励。这一概率与验证者的有效余额成正比,因此在共识中起着重要作用。目前(在 Deneb 和 Electra 之间),最大有效余额为 32 ETH,但在 Electra 硬分叉之后,有效余额限制将从 32 变更为 2048 ETH。

有效余额跟踪验证者的实际余额(通过存款和奖励增加,通过惩罚和惩罚减少),但更新频率较低——每个Epoch更新一次——与每个区块变化的实际余额不同,实际余额在应用了证明奖励、惩罚或不活跃罚款时每个区块都会变化。

此外,有效余额限制为 spec.effective_balance_increment 的倍数(当前主网规范中为 1 ETH)。有效余额以 Gwei 为单位存储,因此可以使用 u64 类型。此外,我们验证者的实际余额(每个区块)频繁变化对我们的有效余额的影响相对较小(每个Epoch一次),而且此时需要生成下一个Epoch的验证者时间表。有效余额的变化“僵持”在 spec.effective_balance_increment (1 ETH)的最接近倍数。这一“滞后”过程在下图中展示:

img

关于有效余额滞后的更多信息可以在此处找到,控制链规参数的滞后的信息可以在此处 。在当前的 pre_electra 硬分叉中,验证者的有效余额限制在 32 ETH。在 Electra 硬分叉之后,此限制将被移除(限制为 2048 ETH)。验证者将能够操作更大的质押,尽管滞后规则仍然适用。

现在,为了使本文的下一部分更易于理解,我们将首先提供“每个Epoch”(per-epoch)和“每个区块”(per-block)处理程序的高层概述。这将简化对后续主题的理解。让我们首先开始“Per-Epoch”。

Per-Epoch处理

一个Epoch是一个编号的时间间隔(以区块编号计算),由 32 个区块组成(目前约为 6.5 分钟)。每个Epoch都有一组固定的、伪随机分配的验证人委员会,负责在每个插槽提出和证明区块。诸如验证人的激活和退出、调度(将验证人分配到委员会)以及其他与验证人分组相关的操作主要是在“每个Epoch”基础上进行的。

“每个Epoch”处理的演示可以在 process_epoch_single_pass() 函数中找到。本节提供这些操作的高层概述。它们包括:

加载和初始化信标状态信息。这涉及到以前的、当前的和下一个Epoch、最后的确认检查点(Epoch)、验证人的总活跃余额、inactivity_leak 标志(指示链最终确定中存在的问题)、进入和退出验证人的变化限制以及其他重要值。 加载削减上下文信息(包含以前削减的验证人的记录)、待处理的存款、有关最早退出Epoch的信息(验证人不能立即退出)以及退出验证人的消耗余额。 处理激活队列,以便将要变为活跃的验证人(此过程将在 Electra 硬分叉后显著改变)。 加载有关验证人的信息,包括它们的余额、非活跃分数以及在当前和先前Epoch中的参与情况。 遍历所有前Epoch和当前Epoch的验证人,执行以下步骤:

加载特定于验证人的数据,例如余额、非活跃分数以及有关在先前和当前Epoch中的参与信息 确定验证人是否符合激活条件,并计算其 base_reward。如果验证人在前一个Epoch中没有问题地参与,则 base_reward 将为非零值。 处理并更新非活跃分数。持续的非活跃将导致所有非活跃验证人在最终性无法推进时受到惩罚,导致信标链进入“非活跃泄漏”模式。 计算每个验证人的奖励和惩罚。 更新注册表,包括处理验证人的包含和排除、处理激活和退出队列,以及调整余额。 削减验证人(如有需要)。这是削减的“每个Epoch”部分,还有一个“立即”部分的削减在“每个区块”处理内。 处理待处理的存款来自执行层的该验证人。 更新最终余额对于验证人。 (遍历验证人结束)

处理全局最早退出Epoch和退出余额。 最终处理待处理的存款。 整合和最终化所有验证人的有效余额更新。

如上所示,这种“每个Epoch”处理涉及与有效余额更新、验证人参与更新、激活队列等不需要在每个区块中立即反应的操作相关的任务。这一阶段形成了共识过程中的一个复杂且资源密集的部分。现在,让我们继续分析“每个区块”处理部分。

Per-区块处理

这个验证阶段发生得比“Per-Epoch”处理频率更高,并涉及以下操作:

  • 直接处理信标区块
  • 管理需要立即采取行动的验证人操作,例如削减、非活跃分数更新或奖励
  • 处理在 Eth1 执行层(ETH 1.0 区块)和信标链之间转移信息的操作,反之亦然

“每个区块”操作使用的示例伪代码结构由 BeaconBlockBody 表示:

class BeaconBlockBody(Container):

    # 伪随机种子,用于验证人洗牌
    randao_reveal: BLSSignature

    # 来自 Eth1 执行层的数据,涉及 ETH 存款
    eth1_data: Eth1Data

    # 由区块提议者提供的任意字符串,称为“涂鸦”
    graffiti: Bytes32

    # 操作

    # 消息,证明提议者验证人的削减
    proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]

    # 消息,证明 attester 验证人的削减
    attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS]

    # 验证人对链检查点的签名
    attestations: List[Attestation, MAX_ATTESTATIONS]

    # 消息,证明验证人的存款
    deposits: List[Deposit, MAX_DEPOSITS]

    # 消息,证明退出请求
    voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]

    # 来自同步委员会成员的链头状态的聚合数据
    sync_aggregate: SyncAggregate

    # 执行

    # 主要执行层(Eth1)区块数据
    execution_payload: ExecutionPayload

    # 证明验证人撤回凭证更改的消息
    bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]

操作部分,对我们来说有趣的内容在 # Operations 部分呈现,并在 这里 描述。接下来是“每块”处理的演示伪代码的下一部分:

def process_block(state: BeaconState, block: BeaconBlock) -> None:
    process_block_header(state, block)

    if is_execution_enabled(state, block.body):
        process_withdrawals(state, block.body.execution_payload) # [在 Capella 中新增]
        process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [在 Capella 中修改]

    process_randao(state, block.body)
    process_eth1_data(state, block.body)
    process_operations(state, block.body) # [在 Capella 中修改]
    process_sync_aggregate(state, block.body.sync_aggregate)

让我们分析处理块的代码,从函数 per_block_processing() 开始,其中包括处理以下内容:

块头:验证块头和所有签名
提款:处理提款,减少验证者的活跃余额
执行负载:这一部分处理实际的 Eth1 状态。大部分工作委托给执行客户端,该客户端执行交易,更新状态、日志,Eth1 块哈希,然后将结果 Eth1 头信息发送回信标链。然而,这里进行关键检查,包括前一个哈希、时间戳和 randao 参数。
Randao:提供后续块的随机种子。它源自聚合的 BLS 签名,为洗牌验证者提供可靠的随机源。以前的 randao 混合存储,以允许重建早期块的验证者索引列表。
Eth1 数据:目前,这主要用于处理来自验证者的存款,尽管这在未来的硬分叉中可能会有显著变化。
操作:这些包括提议者和见证者削减、见证、存款和自愿退出。这些操作更新验证者的余额和状态。
同步委员会:一小部分验证者负责向轻客户端提供关于链头的轻量信息。

我们将在接下来的部分详细探讨所有这些操作,从验证者生命周期的初始步骤——激活开始。让我们深入了解。

验证者激活

成为验证者的第一步当然是质押。这是通过特殊的存款合约完成的,其代码可以在 这里 找到。该合约部署在此 地址,我们可以看到每笔存款为 32 ETH(请注意,这些金额将在即将到来的 Electra 硬分叉后发生变化,允许有效余额高达 2048 ETH)。

该过程中重要的一步是 传递 验证者的公钥。合约提供的签名没有检查;所有数据都简单地添加到存款的默克尔树中。这些检查被推迟到信标层。此外,存款合约不会退还 ETH,因为验证者的存款随后在信标层处理,而不是在执行(Eth1)层。

这个合约的最关键功能(直到 EIP-6110)是 发出 存款(公钥、提款凭证、金额……)事件。Eth1 块的日志会在信标层不断 解析,然后 处理,包括 BLS 签名的 验证

潜在验证者的存款包含在增量默克尔树中,可以通过解析存款合约的事件重建。共识(信标)层需要包含证明来验证存款的有效性。这些机制在 这里 详细说明。任何验证者的首次存款需要签名以证明对相应私钥的所有权。有趣的是,后续对同一验证者的存款不需要此签名,允许任何人补充验证者的余额。

我们的目标是达到 add_validator_to_registry() 函数,该函数将验证者添加到 self.validators 列表中。此操作在 apply_deposit() 函数中执行。在这里,签名被验证,并且存款被添加到“待处理存款”列表中(这个 分支)。该函数还 验证 存款包含的默克尔证明,如前所述。

还值得注意的是,目前执行层与共识层之间通过日志解析进行的交互效率不高。正在进行一项提案(EIP-6110)以直接在 Eth1 块中处理所有存款逻辑,跳过与存款合约的交互。这一变化可能显著减少当前存款包含中的延迟。此外,目前任何具有无效签名的存款都会被忽略,无法获得退款,因为共识层不会直接与执行层交互。

提交存款及其在验证者列表中的包含并不会立即激活验证者(记住,存款被放置在“待处理”存款队列中)。验证者必须经过激活队列。存款会在每个执行区块中处理(以确保没有事件被遗漏),而验证者状态更新则是“per-epoch" 进行的(如 处理 所示)。在这一阶段,activation_churn_limit 参数决定每个epoch中可以从排队中取出的验证者数量。该限制由总验证者数量的 1 / 65536 (spec.churn_limit_quotient (65536)) 限制。最小值为 spec.min_per_epoch_churn_limit (4),该值也用于验证者退出,调节验证者激活和退出的整个“流动限制”。如果激活队列已满,验证者将等待下一epoch以满足激活资格。

我们之前在 这里 遇到过 registry_updates。现在,让我们更深入地探讨这个 过程。我们看到两个分支,pre_electrapost_electra。两个分支首先通过检查其有效余额来验证验证者的资格,通过 这些 “pre_electra”和“post_electra”检查。对于“pre_electra”,有效余额必须恰好为 32 ETH,而对于“post_electra”,有效余额必须大于或等于 32 ETH(由 spec.min_activation_balance(32 ETH) 定义)。

这两个函数会自动退出有效余额低于 spec.ejection_balance(16 ETH) 的验证者。

接下来是主要的“激活”过程,它分配验证者的 activation_epoch。在“pre_electra”中,这是通过激活队列完成的( 这里 ),而在“post_electra”中,则通过验证在最终区块中已明确建立的资格来处理,而不通过队列( 这里 )。

最后,我们的验证者在 validator.activation_epoch 处变得活跃。该epoch由 determined 的过程决定,通过将当前 epoch 加 1 并添加 spec.max_seed_lookahead (4 epoch ,约 25.6 分钟)。

验证者退出

现在,让我们回顾一下活跃验证者是如何退出的,因为这与激活密切相关。退出有两种类型:强制性和自愿性。我们先从强制退出开始:

在 registry_updates(“per-epoch”处理)中,我们观察到,如果 validator.effective_balance < spec.ejection_balance ( 在这里检查 )对于每个活跃的验证者(前面已提到),会 启动 退出。这个检查在“每个验证者”的注册更新中也存在 在这里

另一种无条件退出发生在验证者被 削减

自愿退出需要更复杂的操作。它们可以通过节点的 CLI 启动 ( 见这里 )。这需要来自验证者的 BLS 签名,这会创建一个简单签名的“退出” 消息 :VoluntaryExit。

自愿退出的验证和处理发生在“按块”操作中,通过 process_exits() 函数。这涉及到调用 verify_exit(),这对我们来说是主要关注对象。它以“是否活跃”的 检查 开始(activation_epoch <= current_epoch <= exit_epoch),并验证验证者“当前没有退出”。这是通过 exit_epoch 完成的,对于活跃验证者,其值等于 spec.far_future_epoch (u64::MAX)。

接下来,执行退出周期检查,包括“太年轻而无法退出”的 检查。这会拒绝退出那些在 activation_epoch 开始但工作时间不足 spec.shard_committee_period (256 epoch) 的验证者。随后是签名的 检查,自愿退出必须由验证者进行 BLS 签名。

在“post_electra”中引入的额外检查确保了未处理提款的 不存在

自愿和强制退出最终都导致调用initiate_validator_exit(),其中退出(队列)Epoch被 计算 。这里我们也遇到了两个分支:“pre_electra”和“post_electra”。根据之前的激活逻辑,明显可以看出,Electra 硬分叉将通过移除激活和退出队列来简化激活和退出流程。

在当前的“pre_electra”分支中,最早的退出Epoch被确定,从 ++current_epoch + spec.max_seed_lookahead(+4 Epoch)开始。它使用退出 队列 和相同的“验证者流动限制”,限制了大量验证者的突发性退出或激活。

在“post_electra”分支中,退出Epoch 计算 也从 ++current_epoch + spec.max_seed_lookahead 开始。然而,它通过“余额流动限制”查找首个“可自由退出”的Epoch(请记住,Electra 为验证者引入了任意有效余额)。这限制了被移除的质押金额,而不是验证者的数量,从而维护了协议的经济安全。这个逻辑位于函数compute_exit_epoch_and_update_churn()中。

这两种流动限制都是动态的,基于验证者的总数和他们的总有效余额进行计算。它们使用相同的spec.churn_limit_quotient(65536)商,允许在单个Epoch中退出不超过 1/65536 的验证者总有效余额。因此,验证者的最早退出Epoch为 ++spec.max_seed_lookahead(5 个Epoch,约 32 分钟),虽然如果退出队列满或退出的质押金额很大,该值可能会更大。

最后,validator.exit_epoch 和 validator.withdrawable_epoch 被 保存 ,并且退出验证者的“每个Epoch”计数器在信标状态中被 更新

随着验证者的退出,信标状态被 更新 ,包括最早的退出Epoch(基于流动限制)和包括退出验证者的总退出余额。

我们现在可以继续操作活动的验证者。

验证者处于活动状态

Epoch 的验证者选择

我们现在有一个满足is_active_at(current_epoch) 检查的验证者。当前Epoch的活动验证者列表在get_active_validator_indices()中生成。接下来,我们的验证者必须被分配到一个委员会中(每个验证者只属于一个委员会)。每个槽(区块)和Epoch(由特殊的“检查点”区块标识)由多个验证者委员会进行证明。

“每槽”委员会的数量在get_committee_count_per_slot_with()函数中定义,该函数使用验证者的总数和spec.target_committee_size(128)计算所需的委员会数量,同时确保每个槽不超过spec.max_committees_per_slot(64)个委员会,每个委员会有 128 个验证者。

现在我们准备为当前Epoch形成委员会。让我们在 这里 看看委员会缓存的初始化。在这里,我们看到 RANDAO 参数的初始化,这些参数作为随机种子用于随机地将验证者洗牌并确定性地分配给不同的委员会。这些 RANDAO 种子使得能够在不需要访问区块内容的情况下确定性地重建之前区块的验证者委员会。这就是验证者将过去的 RANDAO 种子保留在固定大小的循环列表中的原因,以便于EpochsPerHistoricalVector(65536 Epoch,约 290 天),这例如使得能够重新计算过去的委员会以进行惩罚。

接下来是验证者列表的 洗牌 。最后,我们 获取 当前Epoch的验证者索引及其每槽的分布。基于“swap-or-not”洗牌的洗牌算法使得可以高效地为验证者的子集生成不相交的洗牌列表,而无需洗牌整个验证者列表并提取子部分。有关该算法的更多信息,请参见 这里 。因此,对于每个Epoch,有槽、每个槽唯一的验证者委员会,以及验证者加入一个证明委员会或作为区块提议者的机会。

img

洗牌也用于选择区块提议者和同步委员会的参与,这些事件的概率取决于验证者的有效余额。我们将稍后再次查看这一点。

验证者提议

提议一个区块是验证者最复杂的任务之一。这不仅因为需要处理和验证整个 Eth1 状态,还因为以太坊目前处于过渡阶段。在这个阶段,“旧”的 Eth1 区块在很大程度上仍然以其原始形式存在,即使它们的设计随着每次新的硬分叉而变得越来越过时。这种遗留逻辑包含了许多计划要删除或修订的分支。虽然我们不会深入探讨区块处理,因为预计在即将到来的硬分叉中这种情况会发生显著变化,但我们的重点仍然是审查验证者状态和生命周期。

区块提议发生的频率非常低。你可以查看如 1(祖父)或 781242(今天的顶级验证者)等验证者的区块提议。目前,单个验证者的区块提议间隔跨度为数百天。然而,重要的是要理解,截至 2025 年初,已经有超过 10^6 个验证者密钥。这些密钥并不全部与单个服务器相关联;一个验证者节点可以使用数千个密钥,动态切换签名凭证。因此,仅有数千个“物理”验证者在管理众多验证者密钥。在将移除固定有效余额要求的 Electra 硬分叉之后,预计这些验证者中的许多将整合。

区块提议者是使用此 函数 进行选择的。它从一个打乱的列表中挑选一个验证者,并结合 基于余额的随机性,提高有效余额较高的验证者的选择概率。

我们的目标不是检查区块构建的细节,因为这是一个包含多个异步和阻塞部分的复杂过程。然而,你可以自行探索这个过程。构建下一个信标区块的核心功能可以在 这里 找到,并由两个主要组件组成: produce_partial_beacon_block()complete_partial_beacon_block()。这些函数的重大部分包括:

produce_partial_beacon_block():

构建 当前时隙的委员会缓存。 加载 来自执行层客户端的执行负载。 接收 来自池的验证者削减和退出消息。 获取 来自执行层的存款。 收集 和过滤之前区块的证明。 重新检查 证明、削减和退出消息的签名(如果启用了“偏执”模式)。

complete_partial_beacon_block():

生成 硬分叉所需的区块对象。 计算 区块奖励(我们稍后会讨论)。 逐区块处理,包括对 证明、削减和 randao 的签名检查、区块签名检查 和其他对于共识安全至关重要的关键验证。 验证 Deneb-Cancun 硬分叉中引入的 KZG blob 承诺。

最后,构造但未签名的区块将被 签名 并发布。

Eth 2.0 中的区块奖励计算是多面的。要深入了解,可以查看此 函数,其中概述了衍生奖励:

参与 同步委员会。该奖励的逻辑在 Altair 硬分叉中引入, 可以在这里找到 。如果提议者参与了同步委员会,提议者奖励逻辑 会适用。 • 提议者削减包含,其中一部分被削减验证者的有效余额被 没收。 • 证明者削减包含,其逻辑与提议者削减类似 逻辑。 • 证明包含。最新的 Altair/Deneb 版本的此奖励计算可在 这里 找到。

上述描述的奖励与共识层相关。每个执行层区块还包括交易费用和 MEV 奖励,这并不是我们文章的重点。

复杂的奖励估算需要分析来自许多验证者的历史数据,这超出了本讨论的范围。因此,我们将此类分析留给专业团队。为了使提供的代码参考更加清晰,以下是从约 600,000 个验证者收集的汇总奖励数据示例。有关来源,请参见[这个链接](https://www.staderlabs.com/docs-v1/Ethereum/Node-operator/Staking on Ethereum/eth-staking-rewards-and-penalties) :

img

现在,让我们继续讨论见证,这是我们验证者的主要任务。

验证者见证

此活动构成验证者的核心功能,验证者见证其他提议者提出的区块,并为每个成功的见证赚取奖励。在每个时隙中,提议者提出一个区块。所有委员会成员随后对他们认定为链的当前头部的区块进行见证。理想情况下,这就是刚刚提出的区块,使用的是分叉选择规则。

并非每个区块都用于投票,因为见证是基于“检查点”的,特定选定的区块,通常每个周期一个。简单来说,检查点被定义为“当前周期中固定时隙的区块或前一个最近区块,这可能属于先前的周期”。检查点是至关重要的,因为共识机制见证的是周期,而不是单个区块。某些区块作为“周期锚”发挥这一作用,尽管它们有时会被跳过。在这种情况下,将选择前面一个区块。因此,可能出现区块 B 来自周期 j-1 被用作周期 j 的检查点,因为更新的周期 j 没有自己的区块可以作为“锚”。

如果其检查点获得超过三分之二的质押余额的投票,则该周期变为正当。你可以在 Gasper 的白皮书中深入了解共识算法和“(区块,周期)对”。

Gasper 共识由两个主要部分组成:

  • Casper FFG(友好的最终确定工具),负责对先前生成的区块和周期进行见证和最终确定。
  • LMD GHOST 分叉选择规则(最新消息驱动的贪婪最重观察子树),负责选择链头(“最佳分叉”),以基于此构建下一个区块。

这两个部分都要求验证者为先前周期和当前区块的检查点投票。在当前的信标链中,他们以此方式同时进行,为头部区块和上一个已见证周期投票。正因为如此,结构体 AttestationData 包括 beacon_block_root(供 LMD GHOST 分叉选择规则使用的信标区块哈希)以及源和目标检查点(在 FFG 最终确定工具中使用)。该结构在这里进行了详细说明。

使用 BLS 签名的多个证明可以与其他签名聚合,将许多个体签名压缩为单一签名。这是 BLS 真正展示其效用的地方;考虑到必须验证的大量验证者和签名,如果不进行聚合,将无法处理链的状态。

验证者见证必须遵守各种老化规则,这些规则取决于证明的年龄(例如,它不能比 spec.min_attestation_inclusion_delay(1)时隙更新或比上一个周期更老(在 Dencun 硬分叉中变更于 EIP-7045))。它们还必须包括有效签名(或聚合签名,因为新的证明可以聚合早期的证明)。创建未聚合的“初始”证明的过程可以在 produce_unaggregated_attestation() 中查看,其中 target 检查点是基于已知链的“头”选择的。在这里 还进行额外的有效性检查,接着在这里 获取上一个已证明的检查点作为源检查点。

在此阶段,验证者已经确定了它所认为的正当源检查点,识别了当前周期中的“新”检查点区块作为“目标”检查点,并进行了投票。

证明奖励是有结构的,根据源检查点、目标检查点和头部区块是否存在超级多数,为证明分配不同的权重。详细描述可以在这里这项综合研究中找到。对于感兴趣的代码,函数 compute_attestation_rewards_altair() 提供了一个很好的示例。在此函数中,你会找到一个循环 遍历所有验证者,对“头”、“源”和“目标”投票的奖励进行单独计算 。因此,可靠的网络连接和充足的计算资源对接收和处理准确的区块链头和证明数据至关重要。否则,验证者可能面临奖励的丧失风险。

验证者无所作为

当一个活跃的验证者离线,未能履行其证明和区块提案职责时,就会发生这种状态。在这种情况下,验证者会丧失奖励并面临惩罚。

在“每个周期”基础上,每个验证者的 inactivity score 会被更新。在“每个验证者”循环期间,如果发现验证者在当前和先前周期中处于不活跃状态,则它的无所作为分数会在这里更新。具体来说,分数会被减少 1,或者如果验证者未参与或被处罚,则增加 spec.inactivity_score_bias(4)。从不活跃的角度来看,被处罚的验证者与普通离线验证者被视为相同,适用相同的不活跃分数惩罚。

此外,所有验证者在链不处于“失效泄漏”状态时,会根据 spec.inactivity_score_recovery_rate(16) 接收其失效分数的恒定 减少。这使得一旦链恢复成功的最终确认,失效分数可以迅速恢复。

累积的失效分数会在 这里 产生惩罚。具体的惩罚金额在 这里 确定。尽管对于参与的验证者,惩罚是 跳过的,但对失效或被处罚的验证者则会施加惩罚。惩罚是根据验证者的余额的一个比例计算的:inactivity_score / (inactivity_penalty_quotient_for_fork (2^24) * inactivity_score_bias (4))。

关于失效惩罚的更多细节请见 这里

验证者惩罚

惩罚是任何权益证明共识机制的重要组成部分。惩罚主要分为两种类型:提议者惩罚和认证者惩罚。从共识的角度来看,攻击者最关键的行为是构建两个相互冲突的链,这可能导致“双重消费”攻击。另一种惩罚旨在缓解的攻击是 平衡攻击

我们的验证者通过一个专门的 slasher 服务执行惩罚,该服务监控信标链区块和证明。该惩罚服务 处理 新区块并 检查 它们,通过 搜索 相同插槽但不同头文件的区块。在这种情况下,它会识别提议者惩罚并 发布 它们。这些惩罚随后由提议者处理并包含在后续区块中(此过程在“提议”部分 这里 已提及)。

类似地,在惩罚器中通过 process_attestations() 函数处理证明。由于这涉及处理大量实体——其中一些可能已经过时——第一步是 过滤 过时的证明。接下来,证明批次按验证者 分组,并且每个批次中的证明都检查以下内容:

  • 双重投票,同一验证者签署两个冲突区块(类似于提议者惩罚)。
  • 环绕投票,一个证明中的源和目标投票“环绕”同一验证者的另一个证明中的投票。

这些惩罚条件在 这里 进一步描述。

在惩罚被发布并包含在区块中后,我们可以检查我们的验证者被惩罚的情况。

验证者被惩罚

惩罚分为两个阶段:即时惩罚和延迟惩罚。这一方案的目的是对验证者的单次不当行为施加轻微惩罚(“初始惩罚”)。然而,如果多个验证者合谋最终化恶意版本的链,并且涉及的有效余额总额变得显著,则次级惩罚将施加更严厉的惩罚(“相关惩罚”)。有关惩罚背后的经济逻辑的详细解释,请参见上面的文档( 这里)。

惩罚行为,例如移除被惩罚的验证者,必须在适用惩罚的最早区块中立即执行。这个即时阶段(“初始惩罚”)是在“每区块”处理函数 process_proposer_slashings()process_attester_slashings() 中处理的。在验证提议者或认证者惩罚消息后(通过验证冲突的区块哈希和签名),将调用 slash_validator() 函数。该函数立即 启动 验证者的退出(在“退出”部分前面已 review)。然而,在被惩罚的情况下,最早的 withdrawable_epoch 会被 设置EPOCHS_PER_SLASHINGS_VECTOR(8192 个 epochs)——一个大时间窗口,以适应大规模验证者的不当行为。这意味着被惩罚的验证者只能在大约 36 天后提取。

在被惩罚的验证者的退出被启动后,其余额会 减少,并且整体状态的有效余额变化会被 更新。最后一步是对举报被惩罚验证者的检举者 支付 (如前面部分所述)。

第二个(“相关惩罚”)阶段的 slashing 在 process_slashings() 函数中按“每个Epoch”处理。在这个阶段,所有在信标状态中积累的 slashing 被总结,计算出 调整后的 slashing 余额。这个余额与总验证者余额结合用来确定要 slashing 的验证者余额的比例。调整后的余额可以使用乘数 spec.proportional_slashing_multiplier_for_state(3(Bellatrix+))进行增加,以使 slashing 更具惩罚性。

最后,当前Epoch被“针对”的受惩罚验证者被 选中。每个被惩罚验证者的有效余额随后 按比例减少,减少的比例为 adjusted_total_slashing_balance / total_balance。

验证者提款

在 Capella 硬分叉后,验证者的提款需要提款凭据。提款凭据与验证者的签名密钥是分开的。我们的验证者使用两个密钥对进行管理:签名和提款。签名密钥主要用于签署区块和证明,而提款密钥则专门用于提款。

最初,提款凭据包含 BLS 公钥的哈希。然而,在 Capella 硬分叉后实现 ETH 提款后,可以选择使用 Eth1 凭据。现在,staking-deposit-cli 可以使用相同的助记词生成签名和提款的 BLS 密钥,或使用标准的 Eth1 地址进行提款。如在“激活”部分讨论的,这些凭据被包含在存款消息数据中,并为每个验证者存储。

提款凭据可以通过签名密钥更新,方法是通过 process_bls_to_execution_changes() 函数中的特殊消息进行处理,消息在“每个块”处理期间被处理。

验证者可以通过创建特殊的提款请求来提款。这个请求只能在常规情况下经过 spec.min_validator_withdrawability_delay(256)个Epoch后执行,或者在验证者被惩罚的情况下,在 EpochsPerSlashingsVector(8192)个Epoch后执行。提款的处理类似于存款,涉及信标层和执行层,因为此过程“转移”验证者的“信标余额”到他们的“ETH 余额”。

当前信标区块的预期提款由 get_expected_withdrawals() 函数生成。此函数 处理 每个区块最多 spec.max_validators_per_withdrawals_sweep(16384)个验证者。如果达到限制,进程会保存信息以在后续块中继续提款。对于“完全可提款”的验证者,将提取全额余额,而对于处于“部分可提款”状态的验证者,则仅提取超过“工作”有效余额的 超额余额

这两种情况都会填充提款列表,然后传递给执行层形成 Eth1 块,在那里实际的 ETH 余额发生变化。

同步委员会中的验证者

同步委员会是一个额外的委员会,目标规模为 SyncCommitteeSize(512)个验证者,每 16384 个块(256 个Epoch,大约 27.3 小时)重新洗牌一次。同步委员会的目的在于为轻客户端提供关于信标链头的“轻量级”可验证信息。这使得轻客户端避免广泛的验证检查,因为同步委员会更新频率较低,其公钥直接存储在信标状态中。轻客户端只需要 N=SyncCommitteeSize 个公钥和一个聚合 BLS 签名即可验证每个新区块。

同步协议的注释规范可在 此处 获得,包含表示当前信标区块头以及当前和下一个同步委员会的 LightClientSnapshot 结构。

下一个同步委员会的验证者列表的生成示例见 此处。它使用与所有其他委员会相同的 洗牌 过程和种子,按验证者的有效余额加权。

同步委员会的输出在 process_sync_aggregate() 函数中按“每个块”处理。在这里,我们 验证 聚合签名(在 此处 创建), 计算 参与成员和区块提议者的奖励,并 奖励 每个 参与者 和区块 提议者,包括提议者也参与同步委员会的场景。

你可以根据当前活跃验证者的总数(今日约为 10^6)、SyncCommitteeSize(512)和 256-epoch(约 27.3 小时)周期计算被选为同步委员会的概率。尽管被选中的机会很小,但参与同步委员会的奖励却是可观的。

链规范视角

让我们创建另一种视角,即“基于链参数”的验证者状态视图,为读者提供他们生命周期的额外理解。以下是一些与验证者状态相关的重要参数及其获取函数名称。这些名称通常出现在不同的位置,例如 chain_spec.rseth_spec.rs 或其他配置文件。有时,它们作为获取函数,通常针对特定的硬分叉版本(例如 这里)。在这里,我们将使用在代码中可以搜索到的 informative names,并提供与不同硬分叉相关的值。让我们开始:

ACTIVATING

max_per_epoch_activation_churn_limit (8(Phase0) -> 256(Electra)) - 每个 epoch 可以激活的最大验证者数量。 min_deposit_amount (1 ETH) - 最低存款金额。 min_activation_balance (32 ETH) - 激活验证者所需的最低余额。 max_seed_lookahead (4 epochs) - 用于“延迟 epoch”的参数,调节验证者激活的延迟。 0x00000000219ab540356cBB839Cbe05303d7705Fa - Eth1 存款合约的地址。

EXITING

min_per_epoch_churn_limit (4(Phase0) -> 128(Electra)) 和 churn_limit_quotient(65536) - 调节每个 epoch 退出本金的最大“每 epoch”数量的参数。 max_seed_lookahead (4 epochs) - 与“激活”部分相同的“延迟 epoch”参数,但在这里,它确定验证者退出的最早 epoch。 shard_committee_period (256 epochs) - 验证者在 shard 委员会参与的持续时间,在此期间他们无法退出。

ACTIVE

max_committees_per_slot (64) - 单个 slot 的验证者委员会数量。 target_committee_size (128) - 委员会中的“最小”验证者数量(尽管这可能更为 复杂)。 max_effective_balance (32 ETH(Phase0) -> 2048 ETH(Electra)) - 验证者的最大有效余额。 ejection_balance (16 ETH) - 低于此阈值的有效余额将导致验证者自动驱逐(退出)。 effective_balance_increment (1 ETH) - 有效余额的“度量单位”,用于奖励和惩罚的计算。所有 有效余额都是该值的倍数,所有小额活跃余额的变动与该值的倍数“挂钩”。 hysteresis_downward_multiplier、hysteresis_upward_multiplier 和 hysteresis_quotient - 定义有效余额滞后的一组参数。 shard_committee_period (256 epochs) - 验证者在 shard 委员会中的存在持续时间(无法提前退出)。

PROPOSING BLOCKS

seconds_per_slot (12 sec) - 单个 slot 的持续时间(以秒为单位)。 base_rewards_per_epoch (4) 和 base_reward_factor (64) - 影响提议验证者基本奖励的参数,与总有效余额的平方根结合使用。 PROPOSER_WEIGHT (8) (8/64 of WEIGHT_DENOMINATOR (64)) - 定义提议者因区块提议而支付给其的区块基本奖励的部分(8/64)的参数(包括证明投票、惩罚证据等的额外奖励)。

ATTESTING BLOCKS

base_rewards_per_epoch (4) 和 base_reward_factor (64) - 影响证明验证者基本奖励的参数,以及与总有效余额的平方根结合使用。 min_attestation_inclusion_delay (1) - 必须经过的最小 slot 数,才能将证明包含在区块中。 TIMELY_SOURCE_WEIGHT (14)、TIMELY_TARGET_WEIGHT (26)、TIMELY_HEAD_WEIGHT (14)、SYNC_REWARD_WEIGHT(2) 和 WEIGHT_DENOMINATOR (64) - 对于证明的奖励份额。

DOING NOTHING

min_epochs_to_inactivity_penalty (4 epochs) - “不活动泄漏”开始之前的链最终性延迟,惩罚验证者的不活动。 inactivity_penalty_quotient (2^24 epochs) - 涉及计算 finality_delay/inactivity_penalty_quotient 的参数,确定因不活动而惩罚验证者有效余额的比例。 inactivity_score_recovery_rate (16) - “不活动泄漏”结束后,对验证者的宽恕每 epoch 的速率。该参数减少验证者的不活动得分。 inactivity_score_bias (4) - 额外的参数,用于调节不活动评分的“力度”,用于计算不活动罚款。

SLASHING OTHERS

whistleblower_reward_quotient (512(Phase0) -> 4096(Electra)) - 奖励提供惩罚证明的举报者的比例,来自被惩罚验证者的有效余额。

SLASHED BY OTHERS

min_slashing_penalty_quotient (64(Altair) -> 32(Bellatrix) -> 2048(Electra)) - 验证者的有效余额立即被惩罚的比例(在大规模惩罚场景下还有额外的递延部分)。 proportional_slashing_multiplier (2(Altair) -> 3(Bellatrix)) - 应用于 total_slashing_balance 的乘数,以提高惩罚的严重性。

WITHDRAWING

min_validator_withdrawability_delay (256 epochs) - 验证者可以取款之前的延迟。 max_validators_per_withdrawals_sweep (16384) - 在一个区块中允许的最大取款数量,传递给 Eth1 执行层。 max_pending_partials_per_withdrawals_sweep (4) - 与待处理的取款相关的参数,如 EIP7251 中所述。

当然,这并不是以太坊共识中使用的所有参数,但我们希望这个视图能够帮助读者增强对验证者生命周期的认识。

结论

以太坊验证者的生命周期是复杂的。激活、退出、参与和惩罚都需要在去中心化环境中细致关注。此外,实际应用程序必须有效地执行大量操作,而验证者节点的性能对于网络的成功至关重要。

单独研究链规范和验证者实现源代码可能是具有挑战性的。这就是为什么我们努力在本文中将文档与实现要素连接,以提供有关“生产就绪代码”中发生的情况的更清晰理解。我们希望此内容对区块链开发人员和安全专家有所帮助。

感谢你的阅读,敬请关注我们的下一篇文章!

我是 AI 翻译官,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
mixbytes
mixbytes
Empowering Web3 businesses to build hack-resistant projects.