Gloas 注解:信标链

  • potuz_
  • 发布于 2026-01-28 18:51
  • 阅读 23

本文是对以太坊 Gloas 规范中 beacon-chain.md 文件变更的注释,主要面向客户端实现者和规范研究者。文章详细解释了 Gloas 中引入的各种数据结构和函数,以及这些变更背后的设计理念和权衡,比如信用的支付、Payload 的及时性以及 Builder 的注册和退出机制。此外,还讨论了在共识层处理执行请求和提款的修改。

Annotated gloas beacon-chain

这是带注释的 Gloas 规范的第一部分。它主要面向客户端实现者或规范研究人员。文本将是简洁的,并假定对先前分叉代码的深入了解。在这篇文章中,我们将介绍beacon-chain.md文件中的一些更改。根据 Prysm 团队中许多人的要求,我们将把自己限制在描述这些更改的原因,而不是如何。我们将简单地省略任何自描述的部分。文本遵循 beacon-chain.md 文件的顺序,因此一些解释必须推迟到相应的辅助函数和类被声明。

我感谢 Justin Traglia、Paul Harris 和 Manu Nalepa 阅读此文并在过程中对其进行修正。

容器

免信任支付

BuilderPendingPayment

class BuilderPendingPayment(Container):
    weight: Gwei
    withdrawal: BuilderPendingWithdrawal

BuilderPendingWithdrawal

class BuilderPendingWithdrawal(Container):
    fee_recipient: ExecutionAddress
    amount: Gwei
    builder_index: BuilderIndex

这两个类用于实现构建者到提议者的免信任支付。每当构建者的竞价作为信标区块的一部分被处理时,对构建者支付的承诺将以 BuilderPendingPayment 的形式添加到信标状态。此承诺具有履行支付所需的最低限度信息:将在 EL 中贷记的费用接收者(这是提议者提前传达给构建者的)、支付金额以及指向需要扣除其余额的构建者的构建者索引。剩余的 weight 字段是为了防止对构建者的恶意攻击。该机制的工作原理如下。当信标区块被处理时,BuilderPendingPayment 被记录在信标状态中,但构建者不会立即被扣除,提议者也不会立即获得支付。提议者在两种不同的情况下获得支付。在这两种情况下,不是立即贷记给提议者,而是在信标状态中存储一个 BuilderPendingWithdrawal 以供以后处理。提议者获得支付的两种情况如下:

  • 如果稍后构建者承诺的 payload 在 process_execution_payload 中被处理,那么在这种情况下,一个 BuilderPendingWithdrawal 会立即被添加到信标状态。
  • 如果直到下一个 epoch 结束时还没有 payload 被处理,那么在 epoch 转换时,已承诺的 BuilderPendingPayment 将被处理,weight 参数用于跟踪有多少比例的槽位的信标委员会实际及时地看到了该区块并对其进行了证明。

一方面,我们必须等待最多两个 epoch 才能处理这些支付,因为 attestation 可以被包含到下一个 epoch。可能其中一些 attestation 被扣留或没有及时被网络看到。另一方面,我们跟踪在它的槽位期间证明了这个区块的权重,以防止区块成为规范的但没有任何 attestation 的情况(例如,提议者连续控制两个槽位并在第二个槽位中一起提交两个区块,在这种情况下,第一个区块不可能被证明);在这种情况下,构建者不可能知道他必须揭示一个 payload(或者它甚至被选择揭示一个 payload),因此如果 weight 字段低于委员会总数的 60%,当 payload 未被处理时,构建者可以免于支付其竞价。

与其以后在 EL 中作为提款处理支付,不如将支付直接贷记到提议者的余额中。这将大大简化提款处理。提议者将在提款清扫中或通过在他们喜欢的时间自己执行提款请求来稍后收到付款。然而,这是来自去中心化质押池的一个请求,因为提款凭证(通常的提款清扫的目标)通常是一个与费用接收者合约不同的合约。修改提款凭证合约以考虑 EL 支付被认为对池的侵入性太强。另一个原因(不再有效)被用来证明提款管道的合理性,在这次修改时,构建者是活跃的验证者,并且立即的余额转移会引发对转移被削减资金的安全担忧。无论如何,这将迫使支付像提款/存款流失一样在流失下被推迟。由于构建者不再是验证者切片的一部分,正如我们将在下面看到的那样,这个原因不再有效。

Payload及时性

PayloadAttestationData

class PayloadAttestationData(Container):
    beacon_block_root: Root
    slot: Slot
    payload_present: boolean
    blob_data_available: boolean

PayloadAttestation

class PayloadAttestation(Container):
    aggregation_bits: Bitvector[PTC_SIZE]
    data: PayloadAttestationData
    signature: BLSSignature

PayloadAttestationMessage

class PayloadAttestationMessage(Container):
    validator_index: ValidatorIndex
    data: PayloadAttestationData
    signature: BLSSignature

IndexedPayloadAttestation

class IndexedPayloadAttestation(Container):
    attesting_indices: List[ValidatorIndex, PTC_SIZE]
    data: PayloadAttestationData
    signature: BLSSignature

这些类处理Payload 及时性委员会,该委员会负责证明 payload 的及时性和其 blob 数据的可用性。每个槽位,从 attestation 委员会中选出一个由 512 名验证者组成的委员会,也提交一个 attestation,以证明 1) payload 的及时性和 2) blob 数据的可用性。PTC 成员提交一个 PayloadAttestationMessage,其中包含识别信标区块的最低限度信息(槽位和根)和两个独立的布尔值。如果 payload 已经及时收到,无论其有效性如何,payload_present 布尔值都设置为 true。如果 PTC 证明者已经收到了它需要保管的此 payload 的所有相应数据列侧链,则 blob_data_available 布尔值设置为 true。关于及时性,有几句话要说。当前规范尚未实现双截止日期 PTC 投票,但这已经开放以供审查,并且很可能在你阅读本文档时它将被合并。在这种双截止日期方法中,PTC 证明者记录它收到 payload 的时间,并继续等待数据到达。如果它有以下任何一种情况,它会提交其 attestation:

  • 接收到 payload 和所有需要的数据。
  • 已经到达最终的 PAYLOAD_ATTESTATION_DUE_BPS 截止日期。

为及时性和 DA 设置不同的截止日期有很多优点。一方面,它允许我们通过最小化 payload 在广播中所需的时间来最大化 payload 执行。它还允许我们通过最大化发送 attestation 的最终截止日期来最大化数据吞吐量。这反映了 payload 和数据的本质非常不同的事实:前者需要被接收然后执行,而后者只需要被接收,验证几乎是立即的。这解释了为什么两个布尔值是独立的。在愉快的情况下,证明者既及时收到了 payload,也及时收到了 blob 数据。在这种情况下,两者都为真。可能 payload 是及时的,但数据从未及时到达。可能 payload 和数据都没有及时到达证明者。最后,可能 payload 没有及时到达,但证明者在截止日期前收到了所有数据列侧链。这也是我们有两个独立布尔值的原因。与达成 payload 及时性共识所使用的 PTC 成员集可能不同于用于达成数据可用的 PTC 成员集。如果下一个提议者是一个超级节点,它自己对数据可用性的看法就足以保证数据可用。但如果提议者不是,它可以将 PTC 作为数据(不可)用的提示。

关于正常 attestation 的另一个变化是,单个证明者发送的消息实际上与链上的消息不同。已签名的消息包括一个实际的验证者索引来识别签名者,而不是使用与聚合对象相同的格式,并强制客户端检查它是否设置了一个单比特并从该比特获取索引。

竞价

ExecutionPayloadBid

class ExecutionPayloadBid(Container):
    parent_block_hash: Hash32
    parent_block_root: Root
    block_hash: Hash32
    prev_randao: Bytes32
    fee_recipient: ExecutionAddress
    gas_limit: uint64
    builder_index: BuilderIndex
    slot: Slot
    value: Gwei
    execution_payment: Gwei
    blob_kzg_commitments_root: Root

SignedExecutionPayloadBid

class SignedExecutionPayloadBid(Container):
    message: ExecutionPayloadBid
    signature: BLSSignature

这些对象通过 P2P 网络传播,也可以直接从构建者处请求以包含在区块中。它们不包括当前 ExecutionPayloadHeader 具有的所有元素。原因很简单,信标链对完整的 header 没有用处,我们找不到任何人在信标状态中针对该 header 证明执行(无论如何,可以针对信标状态中的区块哈希证明执行,或者也可以直接从 EL 证明)。包含在竞价中的字段是能够识别由构建者构建的 payload 承诺与提议者的头部兼容所需的最低限度信息。父区块根和父区块哈希都承诺 CL 和 EL 中的父项。需要两者的原因而不是仅仅一个的原因是,CL 区块根承诺 CL 中的头部,但承诺 EL 中的两个可能的头部:要么该区块的 payload 可用且它是 EL 头部,要么 payload 不可用,那么父 payload 是 EL 中的头部。gas_limitprev_randao 的字段在 CL 中检查,一个在状态转换中显式检查,另一个检查它与提议者的配置是否一致。这是为了让提议者(预计会更加去中心化)设置 gas 限制,而不是构建者。槽位是为了完全识别提议者,费用接收者是为了让提议者知道他们将在正确的地址中获得支付。value 字段指定提议者通过上述机制以免信任的方式确定获得的金额,而 execution_payment 字段包含承诺提议者以任何其他方式支付的承诺(如果提议者决定接受)。blob_kzg_commitments_root 在那里是为了即使没有收到 payload 信封也可以验证数据列侧链。我们稍后将描述这一点,但这使构建者能够立即开始广播 blob,即使他们还不愿意广播 payload。避免任何带宽瓶颈。数据列侧链包含所有 kzg 承诺,只要它们哈希到这个根,我们就知道我们可以信任它们对应于此竞价。如果我们已经在竞价中拥有完整的承诺列表,以便客户端甚至可以在任何侧链到达之前直接从 EL 请求 blob,那就更好了,但这会膨胀竞价的 p2p 网络,以及提议者 <-> 构建者在请求竞价时的直接连接。

竞价中最有争议的字段是 block_hash。它承诺一个特定的 payload,这不是严格必要的。它也创造了可怕的免费期权问题。另一种选择是省略此字段,让构建者在公开时只生成任何 payload。但这种方法的问题在于,提议者不会承诺任何竞价,只是像自我构建一样签名,然后在最大 payload 截止日期之前及时进行拍卖,错过了 EIP-7732 的大部分扩展属性。

Payloads

ExecutionPayloadEnvelope

class ExecutionPayloadEnvelope(Container):
    payload: ExecutionPayload
    execution_requests: ExecutionRequests
    builder_index: BuilderIndex
    beacon_block_root: Root
    slot: Slot
    blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]
    state_root: Root

SignedExecutionPayloadEnvelope

class SignedExecutionPayloadEnvelope(Container):
    message: ExecutionPayloadEnvelope
    signature: BLSSignature

当构建者发现他们的已签名竞价包含在信标区块中时,他们会广播这些对象。除了 payload 之外,还添加了执行请求(需要在信标链中执行额外的状态转换),构建者索引用于识别签名(这只是一个方便,因为原则上可以从信标区块根获取此数据)。槽位也是如此,不是严格需要,但它最终在早期的 Teku/Prysm 互操作中变得有用,其中 payload 在信标区块之前被广播(我们正在使用空 payload 进行测试),因此客户端需要经常依赖挂起的 payload 缓存,直到他们看到信标区块根。blob kzg 承诺在共识中完全无用,并且可能被移除,无论如何,它们都包含在每个数据列侧链中,但移除它们会使需要它们的 payload 处理复杂化,这也是这个 pull request 被关闭的原因。状态根也是一种便利,并且可能被移除,它提供了一个承诺,可以立即针对任何 payload 的后信标状态进行证明。

请注意,虽然信标区块处理是单个状态转换,自 Gloas 以来根本不触及 EL,但 execution payload 处理执行两次状态转换,一次在 EL 中,另一次在 CL 中,因为执行请求。

执行请求无法在共识区块中处理:要在同一个槽位中处理它们,它们必须在已签名竞价中,并且如果 payload 没有被包含,或者下一个 payload 必须强制包含相同的执行请求,无论如何,协调将非常复杂,状态转换必须被恢复。类似地,如果在下一个共识区块中处理请求也是如此。

信标区块

class BeaconBlockBody(Container):
    randao_reveal: BLSSignature
    eth1_data: Eth1Data
    graffiti: Bytes32
    proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]
    attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA]
    attestations: List[Attestation, MAX_ATTESTATIONS_ELECTRA]
    deposits: List[Deposit, MAX_DEPOSITS]
    voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]
    sync_aggregate: SyncAggregate
    # [Modified in Gloas:EIP7732]
    # Removed `execution_payload`
    bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]
    # [Modified in Gloas:EIP7732]
    # Removed `blob_kzg_commitments`
    # [Modified in Gloas:EIP7732]
    # Removed `execution_requests`
    # [New in Gloas:EIP7732]
    signed_execution_payload_bid: SignedExecutionPayloadBid
    # [New in Gloas:EIP7732]
    payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS]

这里没有什么新东西,移除的字段现在包含在其他容器中,新的对象是已签名竞价——承诺一个 payload 和一个支付,以及 payload attestation。Payload attestation 包含在链上,以允许提议者断言其对 head 的看法。如果提议者想要构建在 attester 没有及时看到的完整 payload 之上(例如,因为他们没有收到所有 PTC attestation),那么提议者可以向他们展示确实存在法定人数。类似地,要断言相反的情况:如果存在 payload 不及时的法定人数,即使提议者自己可能拥有 payload,提议者也可以包含这些投票来证明其 payload 的重组是正当的。

信标状态

更改如下:

class BeaconState(Container):
    genesis_time: uint64
    ...
    # [Modified in Gloas:EIP7732]
    # Removed `latest_execution_payload_header`
    # [New in Gloas:EIP7732]
    latest_execution_payload_bid: ExecutionPayloadBid
    ...
    # [New in Gloas:EIP7732]
    builders: List[Builder, BUILDER_REGISTRY_LIMIT]
    # [New in Gloas:EIP7732]
    next_withdrawal_builder_index: BuilderIndex
    # [New in Gloas:EIP7732]
    execution_payload_availability: Bitvector[SLOTS_PER_HISTORICAL_ROOT]
    # [New in Gloas:EIP7732]
    builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH]
    # [New in Gloas:EIP7732]
    builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT]
    # [New in Gloas:EIP7732]
    latest_block_hash: Hash32
    # [New in Gloas:EIP7732]
    payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]

如前所述,execution payload header 在共识中没有用处,唯一检查的是父区块哈希,它现在包含在要处理的 payload 的竞价中,以及 latest_block_hash 中用于处理的最新 payload。有一个新的 builders 列表,他们的权益没有积极地验证,因此它不受削减的影响,因此不影响任何弱主观性计算。构建者余额中持有的权益只能用于支付竞价或提取。

位向量 execution_payload_availability 跟踪哪些 payload 已被看到。由于对于每个槽位,我们现在有效地有两个不同的状态转换,因此有三种可能的结果:区块是完整的,也就是说 CL 和 EL 区块都已被处理。区块是空的,也就是说 CL 区块已被处理但 EL 区块尚未处理,或者区块是跳过的,也就是说 CL 区块和 EL 区块都未被处理。execution_payload_availability 中的位记录了在给定槽位中是否处理了 payload。请注意,payload 不可能在没有其相应的信标区块承诺它的情况下被包含。

数据类

ExpectedWithdrawals

@dataclass
class ExpectedWithdrawals(object):
    withdrawals: Sequence[Withdrawal]
    # [New in Gloas:EIP7732]
    processed_builder_withdrawals_count: uint64
    processed_partial_withdrawals_count: uint64
    # [New in Gloas:EIP7732]
    processed_builders_sweep_count: uint64
    processed_sweep_withdrawals_count: uint64
```提款的处理变得更加复杂,因此我们决定为辅助函数 `get_expected_withdrawals` 的返回值添加一个特殊的数据类。除了之前的 **withdrawals**、`processed_partial_withdrawals_count` 和 `processed_sweep_withdrawals_count` 之外,我们还添加了已处理的构建者提款的数量(这些是构建者向提议者的付款)和构建者清扫。原则上,后者可以移除,但如果构建者存入一个退出的构建者,最终可能会导致锁定死亡资本。清扫的目的仅仅是为了防止这种资本损失。

### 构建者状态的谓词

#### is\_builder\_index

```python
def is_builder_index(validator_index: ValidatorIndex) -> bool:
    return (validator_index & BUILDER_INDEX_FLAG) != 0

is_active_builder

def is_active_builder(state: BeaconState, builder_index: BuilderIndex) -> bool:
    """
    检查 ``builder_index`` 处的构建者对于给定的 ``state`` 是否处于活动状态。
    """
    builder = state.builders[builder_index]
    return (
        # 在构建者列表中的位置已最终确定
        builder.deposit_epoch &lt; state.finalized_checkpoint.epoch
        # 尚未启动退出
        and builder.withdrawable_epoch == FAR_FUTURE_EPOCH
    )

此函数仅检查存款是否已最终确定,以及构建者是否已退出。

is_builder_withdrawal_credential

def is_builder_withdrawal_credential(withdrawal_credentials: Bytes32) -> bool:
    return withdrawal_credentials[:1] == BUILDER_WITHDRAWAL_PREFIX

我们使用相同的验证者索引类型来检查给定的索引是否属于构建者。这是因为构建者切片中的偏移量也将是一个验证者索引。相反,我们设置了位 1&lt;&lt;40 (BUILDER_INDEX_FLAG),表示给定的验证者索引应被视为构建者索引。请注意,2^40 是验证者切片的限制,因此即使不重用验证者索引,也保证超出范围。构建者在其存款处理完成后以及未提款的情况下被认为是活跃的。最后,构建者具有 0x03 的提款凭证,用于识别构建者的存款。

我们采用这种重载验证者索引并共享 uint64 的相同空间以及与 BUILDER_INDEX_FLAG 的转换机制的原因是,另一种选择是在所有涉及构建者的辅助函数中显式使用公钥。对于实现者来说,这似乎太具侵入性了,因为公钥通常是一个很少访问的大型缓存。如果在给定时间注册的构建者不多,这个决定可能会适得其反。

辅助函数

is_attestation_same_slot

def is_attestation_same_slot(state: BeaconState, data: AttestationData) -> bool:
    """
    检查证明是否针对在证明槽位提出的区块。
    """
    if data.slot == 0:
        return True

    blockroot = data.beacon_block_root
    slot_blockroot = get_block_root_at_slot(state, data.slot)
    prev_blockroot = get_block_root_at_slot(state, Slot(data.slot - 1))

    return blockroot == slot_blockroot and blockroot != prev_blockroot

这个辅助函数是出于两个原因需要的。首先,当槽位 N 的信标委员会中的验证者证明槽位 N 的及时区块时,该验证者不会提交该槽位的任何有效负载内容。槽位 N 的有效负载可能包含也可能不包含,如果包含的话,稍后会显示,并且证明者无需知道该有效负载会发生什么。但是,如果该证明者证明过去的区块,例如 N-1 中的父区块,那么该证明者必须发出信号,表明它是证明该区块及其相应的有效负载,还是不证明。因此,对于 N 的证明者,证明 N 处的区块断言:CL 链的头部是 N,EL 链的头部是 N-1(在 N 构建在 N-1 的 CL 和 EL 区块之上的理想情况下),而如果同一证明者证明 N-1,则需要显式地说明 EL 头部,因为信标区块根 N-1 没有明确说明 N-1 的有效负载是否可用且有效。

这些检查对应于以下事实:证明是针对该槽位预期的区块根,并且它与前一个槽位的区块根不同:如果它们相等,则区块根将用于过去的槽位,反之亦然,如果此槽位预期的区块根也在前一个槽位中预期,那么根将相等。

is_valid_indexed_payload_attestation

def is_valid_indexed_payload_attestation(
    state: BeaconState, attestation: IndexedPayloadAttestation
) -> bool:
    """
    检查 ``attestation`` 是否为非空、具有排序的索引,并且具有有效的聚合签名。
    """
    # 验证索引为非空且已排序
    indices = attestation.attesting_indices
    if len(indices) == 0 or not indices == sorted(indices):
        return False

    # 验证聚合签名
    pubkeys = [state.validators[i].pubkey for i in indices]
    domain = get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(attestation.data.slot))
    signing_root = compute_signing_root(attestation.data, domain)
    return bls.FastAggregateVerify(pubkeys, signing_root, attestation.signature)

这只是通常的证明路径的标准副本,我们只检查签名。

is_parent_block_full

注意:如果上次提交的有效负载竞标已通过有效负载完成,则此函数返回 true,这只有在信标区块和有效负载都存在时才会发生。必须在处理区块中的执行有效负载竞标之前,在信标状态上调用此函数。

def is_parent_block_full(state: BeaconState) -> bool:
    return state.latest_execution_payload_bid.block_hash == state.latest_block_hash

如注释所强调的那样,此函数是危险的,是少数几个有效性取决于调用它的状态处理时间的辅助函数之一。如果在处理槽 N 的区块之前调用此函数,则该槽的竞标将不会被处理,这意味着 latest_execution_payload_bid 仍然对应于最新处理的区块。如果该竞标中提交的哈希是 state.latest_block_hash,那么确实处理了最后一个区块有效负载。但是,如果在处理 CL 区块之后但在处理有效负载之前调用了同一函数,那么这些哈希将有所不同。

也许我们应该考虑将此函数重命名为 is_latest_block_full 或使用状态的有效负载可用性位向量代替。

convert_builder_index_to_validator_index

def convert_builder_index_to_validator_index(builder_index: BuilderIndex) -> ValidatorIndex:
    return ValidatorIndex(builder_index | BUILDER_INDEX_FLAG)

convert_validator_index_to_builder_index

def convert_validator_index_to_builder_index(validator_index: ValidatorIndex) -> BuilderIndex:
    return BuilderIndex(validator_index & ~BUILDER_INDEX_FLAG)

需要这两个辅助函数是因为我们重载了验证者索引和构建者索引,以共享相同的范围空间 (uint64),同时它们表示不同切片上的偏移量。因此,例如,第二个构建者,即 builders[1] 中的构建者,其索引为 1,被视为索引为 2^40 + 1 的验证者。这确保了它确实超出了验证者切片的范围(该切片具有 2^40 的限制)。因此,例如,该构建者的提款(将在构建者退出时发生)将使用此索引而不是 1,否则将提款到索引 1 处的验证者。自愿退出将包括索引 2^40 + 1,并且在 process_voluntary_exit 中,将调用辅助函数 convert_validator_index_to_builder_index 以获取索引 1,从而退出正确的构建者。类似地,在处理构建者提款时,为验证者索引 2^40 + 1 而不是索引 1 添加了通常的提款。为此,在 get_builder_withdrawals 中调用了辅助函数 convert_builder_index_to_validator_index

构建者质押

get_pending_balance_to_withdraw_for_builder

def get_pending_balance_to_withdraw_for_builder(
    state: BeaconState, builder_index: BuilderIndex
) -> Gwei:
    return sum(
        withdrawal.amount
        for withdrawal in state.builder_pending_withdrawals
        if withdrawal.builder_index == builder_index
    ) + sum(
        payment.withdrawal.amount
        for payment in state.builder_pending_payments
        if payment.withdrawal.builder_index == builder_index
    )

can_builder_cover_bid

def can_builder_cover_bid(
    state: BeaconState, builder_index: BuilderIndex, bid_amount: Gwei
) -> bool:
    builder_balance = state.builders[builder_index].balance
    pending_withdrawals_amount = get_pending_balance_to_withdraw_for_builder(state, builder_index)
    min_balance = MIN_DEPOSIT_AMOUNT + pending_withdrawals_amount
    if builder_balance &lt; min_balance:
        return False
    return builder_balance - min_balance >= bid_amount

由于延迟付款或提款,构建者的资本可能会在流动中。这些辅助函数只是计算已承诺从构建者处扣除的总金额,首先是在构建者待处理提款中(那些已经是之前向提议者的付款),以及在待处理付款中(那些尚未承诺付款,但已锁定直到 epoch 处理,以确定有效负载是否应该支付竞标,即使它没有被包括在内)。仅允许构建者竞标高达其超过 1 ETH 的质押,加上他们已承诺支付的任何金额。

权重选择重构

compute_balance_weighted_selection

def compute_balance_weighted_selection(
    state: BeaconState,
    indices: Sequence[ValidatorIndex],
    seed: Bytes32,
    size: uint64,
    shuffle_indices: bool,
) -> Sequence[ValidatorIndex]:
    """
    返回通过有效余额采样的 ``size`` 个索引,使用 ``indices`` 作为候选者。如果 ``shuffle_indices`` 为 ``True``,则候选索引本身通过混洗从 ``indices`` 中采样,否则按顺序遍历 ``indices``。
    """
    total = uint64(len(indices))
    assert total > 0
    selected: List[ValidatorIndex] = []
    i = uint64(0)
    while len(selected) &lt; size:
        next_index = i % total
        if shuffle_indices:
            next_index = compute_shuffled_index(next_index, total, seed)
        candidate_index = indices[next_index]
        if compute_balance_weighted_acceptance(state, candidate_index, seed, i):
            selected.append(candidate_index)
        i += 1
    return selected

compute_balance_weighted_acceptance

def compute_balance_weighted_acceptance(
    state: BeaconState, index: ValidatorIndex, seed: Bytes32, i: uint64
) -> bool:
    """
    返回是否接受验证者 ``index`` 的选择,其概率与其 ``effective_balance`` 成正比,并且 randomness 由 ``seed`` 和 ``i`` 给出。
    """
    MAX_RANDOM_VALUE = 2**16 - 1
    random_bytes = hash(seed + uint_to_bytes(i // 16))
    offset = i % 16 * 2
    random_value = bytes_to_uint64(random_bytes[offset : offset + 2])
    effective_balance = state.validators[index].effective_balance
    return effective_balance * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_ELECTRA * random_value

compute_proposer_indices

注意compute_proposer_indices 已被修改为使用 compute_balance_weighted_selection 作为余额加权采样过程的助手。

def compute_proposer_indices(
    state: BeaconState, epoch: Epoch, seed: Bytes32, indices: Sequence[ValidatorIndex]
) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]:
    """
    返回给定 ``epoch`` 的提案者索引。
    """
    start_slot = compute_start_slot_at_epoch(epoch)
    seeds = [hash(seed + uint_to_bytes(Slot(start_slot + i))) for i in range(SLOTS_PER_EPOCH)]
    # [在 Gloas:EIP7732 中修改]
    return [\
        compute_balance_weighted_selection(state, indices, seed, size=1, shuffle_indices=True)[0]\
        for seed in seeds\
    ]

get_next_sync_committee_indices

注意get_next_sync_committee_indices 已被修改为使用 compute_balance_weighted_selection 作为余额加权采样过程的助手。

def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]:
    """
    返回下一个同步委员会的同步委员会索引,可能包含重复项。
    """
    epoch = Epoch(get_current_epoch(state) + 1)
    seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE)
    indices = get_active_validator_indices(state, epoch)
    return compute_balance_weighted_selection(
        state, indices, seed, size=SYNC_COMMITTEE_SIZE, shuffle_indices=True
    )

对于信标链中的所有随机选择,我们使用相同的混洗算法,然后我们通过验证者的有效余额来加权从混洗列表中进行采样。此重构通过添加新的辅助函数 compute_balance_weighted_selection 显式地表示了这一点。用于计算提案者索引和同步委员会索引的通常选择路径将完整的有序活动验证者索引传递给助手。然而,PTC 是从已经在 compute_committee 中计算出的信标委员会进行采样的,这使得布尔控件标志 shuffle_indices(在我看来总是 代码味道)。

证明计数

get_attestation_participation_flag_indices

注意:函数 get_attestation_participation_flag_indices 已被修改为包含一个新的有效负载匹配约束 is_matching_head

def get_attestation_participation_flag_indices(
    state: BeaconState, data: AttestationData, inclusion_delay: uint64
) -> Sequence[int]:
    """
    返回由证明满足的标志索引。
    """
    # 匹配源
    if data.target.epoch == get_current_epoch(state):
        justified_checkpoint = state.current_justified_checkpoint
    else:
        justified_checkpoint = state.previous_justified_checkpoint
    is_matching_source = data.source == justified_checkpoint

    # 匹配目标
    target_root = get_block_root(state, data.target.epoch)
    target_root_matches = data.target.root == target_root
    is_matching_target = is_matching_source and target_root_matches

    # [Gloas:EIP7732 中的新功能]
    if is_attestation_same_slot(state, data):
        assert data.index == 0
        payload_matches = True
    else:
        slot_index = data.slot % SLOTS_PER_HISTORICAL_ROOT
        payload_index = state.execution_payload_availability[slot_index]
        payload_matches = data.index == payload_index

    # 匹配头部
    head_root = get_block_root_at_slot(state, data.slot)
    head_root_matches = data.beacon_block_root == head_root
    # [Gloas:EIP7732 中修改]
    is_matching_head = is_matching_target and head_root_matches and payload_matches

    assert is_matching_source

    participation_flag_indices = []
    if is_matching_source and inclusion_delay &lt;= integer_squareroot(SLOTS_PER_EPOCH):
        participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX)
    if is_matching_target:
        participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX)
    if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY:
        participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX)

    return participation_flag_indices

如上所述,正在为当前槽的区块投票的证明者不能承诺任何有效负载内容,如果该区块是规范的,则应获得及时的头部标志(假设他们的证明包含在下一个区块中)。但是,如果证明者正在证明先前槽的区块,那么他们还需要正确地获取有效负载内容。

考虑以下情况。

相同槽

35

34

33

33

32

...

槽 33 的证明者在其槽期间投票支持区块 33,该区块有两种可能的有效负载内容,要么有效负载存在(浅蓝色节点),要么有效负载不存在(橙色节点)。在缺失有效负载的区块之上提出了区块 34 中的共识区块,也就是说,34 的提案者试图 重组 33 的有效负载。35 的提案者忽略了此信标区块,并在区块 33 的共识区块之上构建,并包含其有效负载。两个分支发散并现在正在竞争。但是,33 的证明支持两个分支,因为证明者在其证明期间无法获得有关有效负载内容的任何信息。

先前的槽,有效负载存在

在与上面相同的图中,想象一个位于槽 34 的信标委员会中的证明者。该证明者已经看到了 34 的信标区块,但是他们也看到了 33 的有效负载,并且它是有效的和及时的,他们的头部仍然在 33 上,因此他们希望强制执行有效负载保持规范(因此允许 35 如上所述重组 34)。这些 34 的证明者将投票支持 33,并且还将有效负载标记为存在。为此,我们重新利用了 data.index,在证明者使用有效负载投票支持先前槽的区块的情况下,data.index1,否则为 0。强制相同槽区块的证明者将此索引设置为 0,但如上所述,它对两个分支都有效。

先前的槽,有效负载缺失

在与上面相同的图中,如果槽 34 的证明者尚未看到 34 的区块或 33 的有效负载,或者例如从他们的角度来看无法获得 33 的有效负载数据,那么这些证明者将投票支持 33 已设置为头部,但将 data.index 设置为 0 以表示他们的头部是没有有有效负载的 33 的共识区块。

get_attestation_participation_flag_indices 中的更改处理了这些方案。我们使用 execution_payload_availability[slot] 来检查从传递的信标状态(在处理信标区块时是头部状态)的角度来看,有效负载是否存在。

PTC 选择

get_ptc

def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]:
    """
    获取给定 ``slot`` 的有效负载及时性委员会。
    """
    epoch = compute_epoch_at_slot(slot)
    seed = hash(get_seed(state, epoch, DOMAIN_PTC_ATTESTER) + uint_to_bytes(slot))
    indices: List[ValidatorIndex] = []
    # 按顺序连接该槽的所有委员会
    committees_per_slot = get_committee_count_per_slot(state, epoch)
    for i in range(committees_per_slot):
        committee = get_beacon_committee(state, slot, CommitteeIndex(i))
        indices.extend(committee)
    return compute_balance_weighted_selection(
        state, indices, seed, size=PTC_SIZE, shuffle_indices=False
    )

这个简单的助手只是从连接的信标委员会成员中获取 512 个 PTC 成员。它调用助手来获取按余额加权的选择,但它不会重新混洗索引,因为 get_beacon_commitee 已经返回了混洗的索引。

剩余状态访问器

get_indexed_payload_attestation

def get_indexed_payload_attestation(
    state: BeaconState, payload_attestation: PayloadAttestation
) -> IndexedPayloadAttestation:
    """
    返回与 ``payload_attestation`` 对应的索引有效负载证明。
    """
    slot = payload_attestation.data.slot
    ptc = get_ptc(state, slot)
    bits = payload_attestation.aggregation_bits
    attesting_indices = [index for i, index in enumerate(ptc) if bits[i]]

    return IndexedPayloadAttestation(
        attesting_indices=sorted(attesting_indices),
        data=payload_attestation.data,
        signature=payload_attestation.signature,
    )

此助手只是常规证明的对应标准版本。获取索引列表,以便更轻松地验证签名。

get_builder_payment_quorum_threshold

def get_builder_payment_quorum_threshold(state: BeaconState) -> uint64:
    """
    计算构建者付款的法定人数阈值。
    """
    per_slot_balance = get_total_active_balance(state) // SLOTS_PER_EPOCH
    quorum = per_slot_balance * BUILDER_PAYMENT_THRESHOLD_NUMERATOR
    return uint64(quorum // BUILDER_PAYMENT_THRESHOLD_DENOMINATOR)

这只是计算委员会规模的 60%,如上所述。在处理构建者付款以确定构建者是否需要为未包含的有效负载付款时,会使用此助手。

状态转换

initiate_builder_exit

def initiate_builder_exit(state: BeaconState, builder_index: BuilderIndex) -> None:
    """
    启动索引为 ``index`` 的构建者的退出。
    """
    # 如果构建者已经启动退出,则返回
    builder = state.builders[builder_index]
    if builder.withdrawable_epoch != FAR_FUTURE_EPOCH:
        return

    # 设置构建者退出 epoch
    builder.withdrawable_epoch = get_current_epoch(state) + MIN_BUILDER_WITHDRAWABILITY_DELAY

标准。处理构建者存款没有延迟,因为他们的质押未验证。但是,处理退出/提款必须有一个最小延迟,因为否则构建者可能会阻塞区块上的存款请求和自愿退出,从而阻止诚实的验证者存款和退出。有一个开放的 PR 来删除此常量,但我们很可能会将其降低到安全最小值。

请注意,构建者没有 exit_epoch 字段或类似字段。构建者只需将 withdrawable_epoch 设置为某个实数即可退出。这是因为没有理由让构建者处于 退出 状态,因为他们的股权没有被验证。

process_slot

def process_slot(state: BeaconState) -> None:
    # 缓存状态根
    previous_state_root = hash_tree_root(state)
    state.state_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_state_root
    # 缓存最新的区块头状态根
    if state.latest_block_header.state_root == Bytes32():
        state.latest_block_header.state_root = previous_state_root
    # 缓存区块根
    previous_block_root = hash_tree_root(state.latest_block_header)
    state.block_roots[state.slot % SLOTS_PER_HISTORICAL_ROOT] = previous_block_root
    # [Gloas:EIP7732 中的新功能]
    # 取消设置下一个有效负载可用性
    state.execution_payload_availability[(state.slot + 1) % SLOTS_PER_HISTORICAL_ROOT] = 0b0

对处理槽位的唯一修改是将当前槽位的有效负载可用性位设置为 0。

process_epoch

def process_epoch(state: BeaconState) -> None:
    process_justification_and_finalization(state)
    process_inactivity_updates(state)
    process_rewards_and_penalties(state)
    process_registry_updates(state)
    process_slashings(state)
    process_eth1_data_reset(state)
    process_pending_deposits(state)
    process_pending_consolidations(state)
    # [Gloas:EIP7732 中的新功能]
    process_builder_pending_payments(state)
    process_effective_balance_updates(state)
    process_slashings_reset(state)
    process_randao_mixes_reset(state)
    process_historical_summaries_update(state)
    process_participation_flag_updates(state)
    process_sync_committee_updates(state)
    process_proposer_lookahead(state)

process_builder_pending_payments

def process_builder_pending_payments(state: BeaconState) -> None:
    """
    处理来自上一个 epoch 的构建者待处理付款。
    """
    quorum = get_builder_payment_quorum_threshold(state)
    for payment in state.builder_pending_payments[:SLOTS_PER_EPOCH]:
        if payment.weight >= quorum:
            state.builder_pending_withdrawals.append(payment.withdrawal)

    old_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:]
    new_payments = [BuilderPendingPayment() for _ in range(SLOTS_PER_EPOCH)]
    state.builder_pending_payments = old_payments + new_payments

对 epoch 处理的唯一修改是处理构建者付款。在我们将构建者转移到非验证者之前,此辅助函数在历史原因之前被调用。有一个未解决的问题。此助手所做的只是获取已达到委员会 60% 阈值的那些待处理付款(因此信标区块是及时的并且已证明)并且未包含有效负载(包含的有效负载从此列表中删除付款)并将相应的构建者待处理提款附加到状态。那些未达到阈值的付款只是被删除,而没有强制构建者付款。

提款

get_builder_withdrawals

def get_builder_withdrawals(
    state: BeaconState,
    withdrawal_index: WithdrawalIndex,
    prior_withdrawals: Sequence[Withdrawal],
) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]:
    withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1
    assert len(prior_withdrawals) &lt;= withdrawals_limit

    processed_count: uint64 = 0
    withdrawals: List[Withdrawal] = []
    for withdrawal in state.builder_pending_withdrawals:
        all_withdrawals = prior_withdrawals + withdrawals
        has_reached_limit = len(all_withdrawals) >= withdrawals_limit
        if has_reached_limit:
            break

        builder_index = withdrawal.builder_index
        withdrawals.append(
            Withdrawal(
                index=withdrawal_index,
                validator_index=convert_builder_index_to_validator_index(builder_index),
                address=withdrawal.fee_recipient,
                amount=withdrawal.amount,
            )
        )
        withdrawal_index += WithdrawalIndex(1)
        processed_count += 1

    return withdrawals, withdrawal_index, processed_count

get_builders_sweep_withdrawals

def get_builders_sweep_withdrawals(
    state: BeaconState,
    withdrawal_index: WithdrawalIndex,
    prior_withdrawals: Sequence[Withdrawal],
) -> Tuple[Sequence[Withdrawal], WithdrawalIndex, uint64]:
    epoch = get_current_epoch(state)
    builders_limit = min(len(state.builders), MAX_BUILDERS_PER_WITHDRAWALS_SWEEP)
    withdrawals_limit = MAX_WITHDRAWALS_PER_PAYLOAD - 1
    assert len(prior_withdrawals) &lt;= withdrawals_limit

    processed_count: uint64 = 0
    withdrawals: List[Withdrawal] = []
    builder_index = state.next_withdrawal_builder_index
    for _ in range(builders_limit):
        all_withdrawals = prior_withdrawals + withdrawals
        has_reached_limit = len(all_withdrawals) >= withdrawals_limit
        if has_reached_limit:
            break

        builder = state.builders[builder_index]
        if builder.withdrawable_epoch &lt;= epoch and builder.balance > 0:
            withdrawals.append(
                Withdrawal(
                    index=withdrawal_index,
                    validator_index=convert_builder_index_to_validator_index(builder_index),
                    address=builder.execution_address,
                    amount=builder.balance,
                )
            )
            withdrawal_index += WithdrawalIndex(1)

        builder_index = BuilderIndex((builder_index + 1) % len(state.builders))
        processed_count += 1

    return withdrawals, withdrawal_index, processed_count

get_expected_withdrawals

def get_expected_withdrawals(state: BeaconState) -> ExpectedWithdrawals:
    withdrawal_index = state.next_withdrawal_index
    withdrawals: List[Withdrawal] = []

    # [Gloas:EIP7732 中的新功能]
    # 获取构建者提款
    builder_withdrawals, withdrawal_index, processed_builder_withdrawals_count = (
        get_builder_withdrawals(state, withdrawal_index, withdrawals)
    )
    withdrawals.extend(builder_withdrawals)

    # 获取部分提款
    partial_withdrawals, withdrawal_index, processed_partial_withdrawals_count = (
        get_pending_partial_withdrawals(state, withdrawal_index, withdrawals)
    )
    withdrawals.extend(partial_withdrawals)

    # [Gloas:EIP7732 中的新功能]
    # 获取构建者清扫提款
    builders_sweep_withdrawals, withdrawal_index, processed_builders_sweep_count = (
        get_builders_sweep_withdrawals(state, withdrawal_index, withdrawals)
    )
    withdrawals.extend(builders_sweep_withdrawals)

    # 获取验证者清扫提款
    validators_sweep_withdrawals, withdrawal_index, processed_validators_sweep_count = (
        get_validators_sweep_withdrawals(state, withdrawal_index, withdrawals)
    )
    withdrawals.extend(validators_sweep_withdrawals)

    return ExpectedWithdrawals(
        withdrawals,
        # [Gloas:EIP7732 中的新功能]
        processed_builder_withdrawals_count,
        processed_partial_withdrawals_count,
        # [Gloas:EIP7732 中的新功能]
        processed_builders_sweep_count,
        processed_validators_sweep_count,
    )

apply_withdrawals

def apply_withdrawals(state: BeaconState, withdrawals: Sequence[Withdrawal]) -> None:
    for withdrawal in withdrawals:
        # [Gloas:EIP7732 中修改]
        if is_builder_index(withdrawal.validator_index):
            builder_index = convert_validator_index_to_builder_index(withdrawal.validator_index)
            builder_balance = state.builders[builder_index].balance
            state.builders[builder_index].balance -= min(withdrawal.amount, builder_balance)
        else:
            decrease_balance(state, withdrawal.validator_index, withdrawal.amount)

update_payload_expected_withdrawals

def update_payload_expected_withdrawals(
    state: BeaconState, withdrawals: Sequence[Withdrawal]
) -> None:
    state.payload_expected_withdrawals = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawals)

update_builder_pending_withdrawals

def update_builder_pending_withdrawals(
    state: BeaconState, processed_builder_withdrawals_count: uint64
) -> None:
    state.builder_pending_withdrawals = state.builder_pending_withdrawals[\
        processed_builder_withdrawals_count:\
    ]

update_next_withdrawal_builder_index

def update_next_withdrawal_builder_index(
    state: BeaconState, processed_builders_sweep_count: uint64
) -> None:
    if len(state.builders) > 0:
        # 更新下一个 builder 索引,以开始下一次提款扫描
        next_index = state.next_withdrawal_builder_index + processed_builders_sweep_count
        next_builder_index = BuilderIndex(next_index % len(state.builders))
        state.next_withdrawal_builder_index = next_builder_index

process_withdrawals

注意:此函数经过修改,仅将 state 作为参数。提款是根据信标状态确定的,任何将相应区块作为父信标区块的执行 payload 都必须遵守执行层中的这些提款。process_withdrawals 必须在 process_execution_payload_bid 之前调用,因为后者会影响验证者余额。

def process_withdrawals(
    state: BeaconState,
    # [在 Gloas:EIP7732 中修改]
    # 移除 `payload`
) -> None:
    # [在 Gloas:EIP7732 中新增]
    # 如果父区块为空,则提前返回
    if not is_parent_block_full(state):
        return

    # 获取预期提款
    expected = get_expected_withdrawals(state)

    # 应用预期提款
    apply_withdrawals(state, expected.withdrawals)

    # 更新状态中的提款字段
    update_next_withdrawal_index(state, expected.withdrawals)
    # [在 Gloas:EIP7732 中新增]
    update_payload_expected_withdrawals(state, expected.withdrawals)
    # [在 Gloas:EIP7732 中新增]
    update_builder_pending_withdrawals(state, expected.processed_builder_withdrawals_count)
    update_pending_partial_withdrawals(state, expected.processed_partial_withdrawals_count)
    # [在 Gloas:EIP7732 中新增]
    update_next_withdrawal_builder_index(state, expected.processed_builders_sweep_count)
    update_next_withdrawal_validator_index(state, expected.withdrawals)

这里的大部分是对先前分叉的提款处理的重写,因为用于获取预期提款的辅助函数在复杂性方面已经失控。提款处理在 Gloas 中确实变得更加复杂,无论是在规范上还是在实现上。原因是 EL 中提款的履行相对于 CL 中的扣除被延迟了。这解释了对 is_parent_block_full 的第一次检查。如果父 payload 尚未出现,那么我们无法在当前槽中处理任何提款,因为先前的共识区块的提款已经在 CL 中被扣除,并且尚未在 EL 中被贷记。由于 payload 和区块执行状态转换,这些扣除的提款将会丢失,并且很难在区块构建时恢复它们。这证明在互操作方面很难实现,因此我们选择在调用 update_payload_expected_withdrawals 时将这些提款显式地缓存在状态中。

除了这些变化之外,新功能是我们首先获取 builder 提款,这些是 builder 向提议者的付款。有一个明确的限制 MAX_WITHDRAWALS_PER_PAYLOAD - 1,以便最后一个提款必须是针对验证者的,因此我们能够正确地更新下一个验证者索引。另请注意,在 get_builder_withdrawals 中,我们使用 convert_builder_index_to_validator_index 获取验证者索引,以添加标记 2^40

然后我们追加部分提款,这些提款就像先前分叉中验证者的提款一样。

然后我们追加 builder 扫描提款,这些提款仅适用于已经退出的 builder,他们在其中有一些余额(例如,如果 builder 在退出时被存入)。

最后,我们扫描验证者以进行提款。

请注意,对于 builder 待处理提款,我们不关心任何可提款 epoch,就像扫描一样,这些提款会在处理 payload 或 builder 待处理付款时立即添加,并在下一次符合条件的扫描中立即支付。

Execution payload bid

verify_execution_payload_bid_signature

def verify_execution_payload_bid_signature(
    state: BeaconState, signed_bid: SignedExecutionPayloadBid
) -> bool:
    builder = state.builders[signed_bid.message.builder_index]
    signing_root = compute_signing_root(
        signed_bid.message, get_domain(state, DOMAIN_BEACON_BUILDER)
    )
    return bls.Verify(builder.pubkey, signing_root, signed_bid.signature)

process_execution_payload_bid

def process_execution_payload_bid(state: BeaconState, block: BeaconBlock) -> None:
    signed_bid = block.body.signed_execution_payload_bid
    bid = signed_bid.message
    builder_index = bid.builder_index
    amount = bid.value

    # 对于自建,无论提款凭证前缀如何,金额必须为零
    if builder_index == BUILDER_INDEX_SELF_BUILD:
        assert amount == 0
        assert signed_bid.signature == bls.G2_POINT_AT_INFINITY
    else:
        # 验证 builder 是否处于活动状态
        assert is_active_builder(state, builder_index)
        # 验证 builder 是否有资金来支付 bid
        assert can_builder_cover_bid(state, builder_index, amount)
        # 验证 bid 签名是否有效
        assert verify_execution_payload_bid_signature(state, signed_bid)

    # 验证 bid 是否适用于当前槽
    assert bid.slot == block.slot
    # 验证 bid 是否适用于正确的父区块
    assert bid.parent_block_hash == state.latest_block_hash
    assert bid.parent_block_root == block.parent_root
    assert bid.prev_randao == get_randao_mix(state, get_current_epoch(state))

    # 如果有付款,则记录待处理付款
    if amount > 0:
        pending_payment = BuilderPendingPayment(
            weight=0,
            withdrawal=BuilderPendingWithdrawal(
                fee_recipient=bid.fee_recipient,
                amount=amount,
                builder_index=builder_index,
            ),
        )
        state.builder_pending_payments[SLOTS_PER_EPOCH + bid.slot % SLOTS_PER_EPOCH] = (
            pending_payment
        )

    # 缓存已签名的 execution payload bid
    state.latest_execution_payload_bid = bid

处理 bid 相对简单:对于外部 builder,签名会被验证。由于我们还支持自建,因此如果提议者是自建,我们会强制签名是无穷远点。只有当外部 builder 处于活动状态并且可以在 amount 字段中支付 bid 金额时,我们才会接受来自外部 builder 的 bid。我们在这里忽略任何受信任的付款值。请注意,在 Gloas 分叉的前几个 epoch 中,我们目前没有任何方法来拥有活跃的外部 builder,直到他们的存款最终确定。其余的验证是为了确保 bid 与信标区块兼容,特别是它是在 CL 和 EL 上构建在相同的父区块之上的。如果有任何付款,则将新的 BuilderPendingPayment 添加到状态。builder_pending_payments 切片的前半部分包含先前 epoch 的付款,后半部分包含当前 epoch 的付款。这是为了在 epoch 转换时,仅处理前半部分,并将后半部分移动到切片的开始。

Builder 存款

get_index_for_new_builder

def get_index_for_new_builder(state: BeaconState) -> BuilderIndex:
    for index, builder in enumerate(state.builders):
        if builder.withdrawable_epoch &lt;= get_current_epoch(state) and builder.balance == 0:
            return BuilderIndex(index)
    return BuilderIndex(len(state.builders))

get_builder_from_deposit

def get_builder_from_deposit(
    state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64
) -> Builder:
    return Builder(
        pubkey=pubkey,
        version=uint8(withdrawal_credentials[0]),
        execution_address=ExecutionAddress(withdrawal_credentials[12:]),
        balance=amount,
        deposit_epoch=get_current_epoch(state),
        withdrawable_epoch=FAR_FUTURE_EPOCH,
    )

add_builder_to_registry

def add_builder_to_registry(
    state: BeaconState, pubkey: BLSPubkey, withdrawal_credentials: Bytes32, amount: uint64
) -> None:
    index = get_index_for_new_builder(state)
    builder = get_builder_from_deposit(state, pubkey, withdrawal_credentials, amount)
    set_or_append_list(state.builders, index, builder)

apply_deposit_for_builder

注意:Builder 索引是可重用的。当 builder 退出时,其索引可能会在以后重新分配给具有新公钥的不同 builder。发送给已退出 builder 的任何存款都将退还给 builder 的执行地址。已退出的 builder 无法重新激活,尽管新注册的 builder 的公钥可能先前已出现在 builder 集合中。依赖缓存的实现应考虑此行为。

def apply_deposit_for_builder(
    state: BeaconState,
    pubkey: BLSPubkey,
    withdrawal_credentials: Bytes32,
    amount: uint64,
    signature: BLSSignature,
) -> None:
    builder_pubkeys = [b.pubkey for b in state.builders]
    if pubkey not in builder_pubkeys:
        # 验证存款签名(所有权证明),存款合约未对其进行检查
        if is_valid_deposit_signature(pubkey, withdrawal_credentials, amount, signature):
            add_builder_to_registry(state, pubkey, withdrawal_credentials, amount)
    else:
        # 按存款金额增加余额
        builder_index = builder_pubkeys.index(pubkey)
        state.builders[builder_index].balance += amount

process_deposit_request

def process_deposit_request(state: BeaconState, deposit_request: DepositRequest) -> None:
    # [在 Gloas:EIP7732 中新增]
    builder_pubkeys = [b.pubkey for b in state.builders]
    validator_pubkeys = [v.pubkey for v in state.validators]

    # [在 Gloas:EIP7732 中新增]
    # 无论提款凭证前缀如何,如果具有此公钥的 builder/validator
    # 已经存在,则将存款应用到他们的余额
    is_builder = deposit_request.pubkey in builder_pubkeys
    is_validator = deposit_request.pubkey in validator_pubkeys
    is_builder_prefix = is_builder_withdrawal_credential(deposit_request.withdrawal_credentials)
    if is_builder or (is_builder_prefix and not is_validator):
        # 立即应用 builder 存款
        apply_deposit_for_builder(
            state,
            deposit_request.pubkey,
            deposit_request.withdrawal_credentials,
            deposit_request.amount,
            deposit_request.signature,
        )
        return

    # 将验证者存款添加到队列
    state.pending_deposits.append(
        PendingDeposit(
            pubkey=deposit_request.pubkey,
            withdrawal_credentials=deposit_request.withdrawal_credentials,
            amount=deposit_request.amount,
            signature=deposit_request.signature,
            slot=state.slot,
        )
    )

在 Gloas 中,存款请求的路径已得到大量修改。Builder 只能通过存款请求而不是 staking 合约中的存款来加入。

我们拥有并依赖的一个不变量是,在任何给定时间,没有验证者和 builder 可以具有相同的公钥。

当接收到新的存款请求时,它可以是为现有 builder、退出验证者、新 builder 或新验证者。现有情况很容易处理,builder 立即应用,验证者添加到待处理队列。对于不在退出验证者或 builder 列表中的公钥,我们使用提款前缀进行区分。如果是 0x03 前缀,我们则添加一个新的 builder。为了强制执行上述不变量,如果新验证者的提款凭证以 0x03 开头,我们会将 Gloas 分叉时任何针对新验证者的待处理存款转换为 builder。

请注意,builder 重用索引。这可能需要客户端仔细缓存公钥 -> 索引映射。

操作

process_operations

注意process_operations 经过修改以处理 PTC 证明并删除对 process_deposit_requestprocess_withdrawal_requestprocess_consolidation_request 的调用。

def process_operations(state: BeaconState, body: BeaconBlockBody) -> None:
    # 一旦处理完所有先前的存款,则禁用先前的存款机制
    eth1_deposit_index_limit = min(
        state.eth1_data.deposit_count, state.deposit_requests_start_index
    )
    if state.eth1_deposit_index &lt; eth1_deposit_index_limit:
        assert len(body.deposits) == min(
            MAX_DEPOSITS, eth1_deposit_index_limit - state.eth1_deposit_index
        )
    else:
        assert len(body.deposits) == 0

    def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
        for operation in operations:
            fn(state, operation)

    # [在 Gloas:EIP7732 中修改]
    for_ops(body.proposer_slashings, process_proposer_slashing)
    for_ops(body.attester_slashings, process_attester_slashing)
    # [在 Gloas:EIP7732 中修改]
    for_ops(body.attestations, process_attestation)
    for_ops(body.deposits, process_deposit)
    # [在 Gloas:EIP7732 中修改]
    for_ops(body.voluntary_exits, process_voluntary_exit)
    for_ops(body.bls_to_execution_changes, process_bls_to_execution_change)
    # [在 Gloas:EIP7732 中修改]
    # 移除 `process_deposit_request`
    # [在 Gloas:EIP7732 中修改]
    # 移除 `process_withdrawal_request`
    # [在 Gloas:EIP7732 中修改]
    # 移除 `process_consolidation_request`
    # [在 Gloas:EIP7732 中新增]
    for_ops(body.payload_attestations, process_payload_attestation)

如上所述,所有请求不再与信标区块一起处理,它们在槽中的第二个状态转换中与 payload 信封一起处理。其他修改包括提议者削减(以删除任何 builder 待处理付款,从而防止通过等效性进行 builder 申诉)。自愿退出(以处理 builder 退出)和证明(以处理修改后的头部帐户,并跟踪区块的权重,以查看它们是否已达到法定人数以强制 builder 付款)。此外,我们处理 payload 证明,这是 Gloas 中的一个新对象。

Builder 退出

process_voluntary_exit

def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None:
    voluntary_exit = signed_voluntary_exit.message
    domain = compute_domain(
        DOMAIN_VOLUNTARY_EXIT, CAPELLA_FORK_VERSION, state.genesis_validators_root
    )
    signing_root = compute_signing_root(voluntary_exit, domain)

    # 退出必须指定一个它们生效的 epoch;在此之前它们无效
    assert get_current_epoch(state) >= voluntary_exit.epoch

    # [在 Gloas:EIP7732 中新增]
    if is_builder_index(voluntary_exit.validator_index):
        builder_index = convert_validator_index_to_builder_index(voluntary_exit.validator_index)
        # 验证 builder 是否处于活动状态
        assert is_active_builder(state, builder_index)
        # 仅当 builder 在队列中没有待处理提款时才退出
        assert get_pending_balance_to_withdraw_for_builder(state, builder_index) == 0
        # 验证签名
        pubkey = state.builders[builder_index].pubkey
        assert bls.Verify(pubkey, signing_root, signed_voluntary_exit.signature)
        # 启动退出
        initiate_builder_exit(state, builder_index)
        return

    validator = state.validators[voluntary_exit.validator_index]
    # 验证验证者是否处于活动状态
    assert is_active_validator(validator, get_current_epoch(state))
    # 验证是否未启动退出
    assert validator.exit_epoch == FAR_FUTURE_EPOCH
    # 验证验证者是否已活跃足够长的时间
    assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD
    # 仅当验证者在队列中没有待处理提款时才退出
    assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0
    # 验证签名
    assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature)
    # 启动退出
    initiate_validator_exit(state, voluntary_exit.validator_index)

如果已签名退出对应于 builder,这是一个简单的修改,可以撤回 builder。需要工具来生成这些退出,因为索引需要作为 builder 切片中的索引加上标记 2^40 传递。只有当 builder 没有待处理付款或提款时,我们才会接受这些退出。

请注意,builder 扫描实际上是因为索引重用而产生的边缘情况。只要另一个 builder 已在退出的 builder 上存款,则先前 builder 的存款将生成一个具有旧公钥的新 builder。因此,客户端需要确保处理同一公钥可能存在的不同索引。如果我们接受具有某些余额的退出 builder 的边缘情况,并仅在索引重用时将其烧毁,那么我们可能会完全摆脱 builder 扫描。

process_attestation

注意:该函数经过修改,以跟踪待处理 builder 付款的权重,并使用 AttestationData 中的 index 字段来表示 payload 的可用性。

def process_attestation(state: BeaconState, attestation: Attestation) -> None:
    data = attestation.data
    assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state))
    assert data.target.epoch == compute_epoch_at_slot(data.slot)
    assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY &lt;= state.slot

    # [在 Gloas:EIP7732 中修改]
    assert data.index &lt; 2
    committee_indices = get_committee_indices(attestation.committee_bits)
    committee_offset = 0
    for committee_index in committee_indices:
        assert committee_index &lt; get_committee_count_per_slot(state, data.target.epoch)
        committee = get_beacon_committee(state, data.slot, committee_index)
        committee_attesters = set(
            attester_index
            for i, attester_index in enumerate(committee)
            if attestation.aggregation_bits[committee_offset + i]
        )
        assert len(committee_attesters) > 0
        committee_offset += len(committee)

    # 比特字段长度与参与者总数匹配
    assert len(attestation.aggregation_bits) == committee_offset

    # 参与标志索引
    participation_flag_indices = get_attestation_participation_flag_indices(
        state, data, state.slot - data.slot
    )

    # 验证签名
    assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation))

    # [在 Gloas:EIP7732 中修改]
    if data.target.epoch == get_current_epoch(state):
        current_epoch_target = True
        epoch_participation = state.current_epoch_participation
        payment = state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH]
    else:
        current_epoch_target = False
        epoch_participation = state.previous_epoch_participation
        payment = state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH]

    proposer_reward_numerator = 0
    for index in get_attesting_indices(state, attestation):
        # [在 Gloas:EIP7732 中新增]
        # 对于同槽证明,请检查我们是否设置任何新标志。
        # 如果是,则此验证者尚未贡献于此槽的法定人数。
        will_set_new_flag = False

        for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS):
            if flag_index in participation_flag_indices and not has_flag(
                epoch_participation[index], flag_index
            ):
                epoch_participation[index] = add_flag(epoch_participation[index], flag_index)
                proposer_reward_numerator += get_base_reward(state, index) * weight
                # [在 Gloas:EIP7732 中新增]
                will_set_new_flag = True

        # [在 Gloas:EIP7732 中新增]
        # 当设置任何新标志时,为同槽证明添加权重。
        # 这确保每个验证者每个槽只贡献一次。
        if (
            will_set_new_flag
            and is_attestation_same_slot(state, data)
            and payment.withdrawal.amount > 0
        ):
            payment.weight += state.validators[index].effective_balance

    # 奖励提议者
    proposer_reward_denominator = (
        (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT
    )
    proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator)
    increase_balance(state, get_beacon_proposer_index(state), proposer_reward)

    # [在 Gloas:EIP7732 中新增]
    # 更新 builder 付款权重
    if current_epoch_target:
        state.builder_pending_payments[SLOTS_PER_EPOCH + data.slot % SLOTS_PER_EPOCH] = payment
    else:
        state.builder_pending_payments[data.slot % SLOTS_PER_EPOCH] = payment

唯一的更改是获取证明槽的相应 builder 待处理的付款,并将证明者的权重添加给它,如果该证明与上述相同的槽区块相同。只有对于获取其中一个参与标志的证明,我们才添加此权重。这就是使用标记 will_set_new_flag 的原因。

Payload 证明

process_payload_attestation

def process_payload_attestation(
    state: BeaconState, payload_attestation: PayloadAttestation
) -> None:
    data = payload_attestation.data

    # 检查该证明是否适用于父信标区块
    assert data.beacon_block_root == state.latest_block_header.parent_root
    # 检查该证明是否适用于先前的槽
    assert data.slot + 1 == state.slot
    # 验证签名
    indexed_payload_attestation = get_indexed_payload_attestation(state, payload_attestation)
    assert is_valid_indexed_payload_attestation(state, indexed_payload_attestation)

Payload 证明的处理很简单。仅允许先前的槽的 PTC 证明,这有助于提议者断言其对重组的看法或在父级之上构建。唯一的检查是信标区块根是否为父区块,以及它们的签名是否有效。还有另一个在分叉选择中调用的助手,我们将在单独的带注释的文档中介绍。请注意,payload 证明不会被奖励,如果它们丢失了也不会受到处罚,也不会因等效性而被削减。一方面,添加奖励以进行激励调整的复杂性不值得收益,另一方面,任何惩罚,如果相关,都必须非常大。因此,我们选择不对这些证明应用任何奖励。EIP 的第一稿从信标委员会获取了 PTC,并忽略了来自这些验证者的证明,因此如果他们按时投下 PTC,他们将获得完整的证明奖励。但这使得证明处理变得更加复杂,没有充分的理由。

通过等效行为来申诉 builder

process_proposer_slashing

def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None:
    header_1 = proposer_slashing.signed_header_1.message
    header_2 = proposer_slashing.signed_header_2.message

    # 验证头部槽是否匹配
    assert header_1.slot == header_2.slot
    # 验证头部提议者索引是否匹配
    assert header_1.proposer_index == header_2.proposer_index
    # 验证头部是否不同
    assert header_1 != header_2
    # 验证提议者是否可削减
    proposer = state.validators[header_1.proposer_index]
    assert is_slashable_validator(proposer, get_current_epoch(state))
    # 验证签名
    for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2):
        domain = get_domain(
            state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(signed_header.message.slot)
        )
        signing_root = compute_signing_root(signed_header.message, domain)
        assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature)

    # [在 Gloas:EIP7732 中新增]
    # 如果与此提议相对应的 BuilderPendingPayment
    # 仍在 2-epoch 窗口中,则删除该付款。
    slot = header_1.slot
    proposal_epoch = compute_epoch_at_slot(slot)
    if proposal_epoch == get_current_epoch(state):
        payment_index = SLOTS_PER_EPOCH + slot % SLOTS_PER_EPOCH
        state.builder_pending_payments[payment_index] = BuilderPendingPayment()
    elif proposal_epoch == get_previous_epoch(state):
        payment_index = slot % SLOTS_PER_EPOCH
        state.builder_pending_payments[payment_index] = BuilderPendingPayment()

    slash_validator(state, header_1.proposer_index)

此修改是为了防止提议者强制 builder 支付他们没有发布的 bid,因为存在等效性(例如,builder 可能有一个带有针对不同 builder 的已提交 bid 的非规范区块,并且规范区块是针对他自己的 bid 的等效行为)。在这种情况下,我们只需删除与削减头部中的槽关联的 builder 的任何待处理付款。

Execution Payload

verify_execution_payload_envelope_signature

def verify_execution_payload_envelope_signature(
    state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope
) -> bool:
    builder_index = signed_envelope.message.builder_index
    if builder_index == BUILDER_INDEX_SELF_BUILD:
        validator_index = state.latest_block_header.proposer_index
        pubkey = state.validators[validator_index].pubkey
    else:
        pubkey = state.builders[builder_index].pubkey

    signing_root = compute_signing_root(
        signed_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER)
    )
    return bls.Verify(pubkey, signing_root, signed_envelope.signature)

process_execution_payload

注意process_execution_payload 现在是状态转换中的一个独立检查。当导入当前槽的 builder 提出的已签名 execution payload 时,会调用它。

def process_execution_payload(
    state: BeaconState,
    # [在 Gloas:EIP7732 中修改]
    # 移除 `body`
    # [在 Gloas:EIP7732 中新增]
    signed_envelope: SignedExecutionPayloadEnvelope,
    execution_engine: ExecutionEngine,
    # [在 Gloas:EIP7732 中新增]
    verify: bool = True,
) -> None:
    envelope = signed_envelope.message
    payload = envelope.payload

    # 验证签名
    if verify:
        assert verify_execution_payload_envelope_signature(state, signed_envelope)

    # 缓存最新的区块头部状态根
    previous_state_root = hash_tree_root(state)
    if state.latest_block_header.state_root == Root():
        state.latest_block_header.state_root = previous_state_root

    # 验证与信标区块的一致性
    assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header)
    assert envelope.slot == state.slot

    # 验证与已提交 bid 的一致性
    committed_bid = state.latest_execution_payload_bid
    assert envelope.builder_index == committed_bid.builder_index
    assert committed_bid.blob_kzg_commitments_root == hash_tree_root(envelope.blob_kzg_commitments)
    assert committed_bid.prev_randao == payload.prev_randao

    # 验证与预期提款的一致性
    assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals)

    # 验证 gas_limit
    assert committed_bid.gas_limit == payload.gas_limit
    # 验证区块哈希
    assert committed_bid.block_hash == payload.block_hash
    # 验证父哈希相对于先前 execution payload 的一致性
    assert payload.parent_hash == state.latest_block_hash
    # 验证时间戳
    assert payload.timestamp == compute_time_at_slot(state, state.slot)
    # 验证承诺是否在限制范围内
    assert (
        len(envelope.blob_kzg_commitments)
        &lt;= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
    )
    # 验证 execution payload 是否有效
    versioned_hashes = [\
        kzg_commitment_to_versioned_hash(commitment) for commitment in envelope.blob_kzg_commitments\
    ]
    requests = envelope.execution_requests
    assert execution_engine.verify_and_notify_new_payload(
        NewPayloadRequest(
            execution_payload=payload,
            versioned_hashes=versioned_hashes,
            parent_beacon_block_root=state.latest_block_header.parent_root,
            execution_requests=requests,
        )
    )

    def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None:
        for operation in operations:
            fn(state, operation)

    for_ops(requests.deposits, process_deposit_request)
    for_ops(requests.withdrawals, process_withdrawal_request)
    for_ops(requests.consolidations, process_consolidation_request)

    # 将 builder 付款排队
    payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH]
    amount = payment.withdrawal.amount
    if amount > 0:
        state.builder_pending_withdrawals.append(payment.withdrawal)
    state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = (
        BuilderPendingPayment()
    )

    # 缓存 execution payload 哈希
    state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1
    state.latest_block_hash = payload.block_hash

    # 验证状态根
    if verify:
        assert envelope.state_root == hash_tree_root(state)

Execution payload 处理与先前分叉基本相同。唯一的区别是,现在此对象独立于信标区块广播,并且在 ExecutionPayloadEnvelope 中广播。Builder 索引用于指示提议者是否正在自建(传递 BUILDER_INDEX_SELF_BUILD)以获取提议者的公钥,否则我们使用 builder 的公钥来验证签名。在先前的分叉上,关于何时设置状态的最新区块头部的状态根,存在一个先有鸡还是先有蛋的问题,此模式现在也在此处执行。因此,当 payload 存在时,我们在处理 payload 时设置状态根,否则稍后将在缺少 payload 时在 process_slot 中设置状态根。在处理 payload 时,我们删除 builder 待处理付款,并立即使用支付将 builder 待处理提款排队到提议者。我们缓存已处理的最新区块哈希,这对于检查父级是否已满很有用,并设置长期 payload 可用性位。

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

0 条评论

请先 登录 后评论
potuz_
potuz_
江湖只有他的大名,没有他的介绍。