feat: implement should_apply_proposer_boost for gloas#9233
feat: implement should_apply_proposer_boost for gloas#9233
Conversation
Implement the `should_apply_proposer_boost` logic from consensus-specs commit 71d1151 (PR #4807). This addresses the builder reveal safety concern where a colluding next-slot proposer could use proposer boost to override a legitimately revealed block. Changes: - Add `ptcTimeliness` and `proposerIndex` fields to ProtoBlock - Add `isBlockPtcTimely` to track PTC deadline timeliness - Add `shouldApplyProposerBoost` which withholds boost when the parent is a weak, equivocating block from the previous slot - Add `findEquivocatingBlocks` in ProtoArray to detect proposer equivocations by scanning for PTC-timely blocks at the same slot from the same proposer - Gate proposer boost in `getWeight` on `shouldApplyProposerBoost()` - Pre-gloas blocks retain unconditional boost (backward compatible) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request implements the Gloas fork-choice logic for proposer boost and PTC (Payload Timeliness Committee) timeliness. It adds ptcTimeliness and proposerIndex to block metadata and introduces the shouldApplyProposerBoost logic, which considers parent block weight and proposer equivocations. Feedback focuses on preventing a crash during the fork transition by using dynamic payload status for parent nodes and optimizing the performance of the equivocation check to avoid O(N) map iterations during head updates.
| slotsPerEpoch: SLOTS_PER_EPOCH, | ||
| committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD, | ||
| }); | ||
| const parentNode = this.protoArray.getNode(parentBlock.blockRoot, PayloadStatus.PENDING); |
There was a problem hiding this comment.
The hardcoded PayloadStatus.PENDING will cause a crash during the fork transition. When the first Gloas block is processed, its parent is a pre-Gloas block. Pre-Gloas blocks only have the FULL variant, and protoArray.getNode throws a ProtoArrayError if PENDING or EMPTY is requested for a pre-Gloas block. Using parentBlock.payloadStatus ensures the correct variant is requested for both pre-Gloas and Gloas blocks.
| const parentNode = this.protoArray.getNode(parentBlock.blockRoot, PayloadStatus.PENDING); | |
| const parentNode = this.protoArray.getNode(parentBlock.blockRoot, parentBlock.payloadStatus); |
| for (const [root, variantOrArr] of this.indices.entries()) { | ||
| if (root === excludeRoot) continue; | ||
| const nodeIndex = Array.isArray(variantOrArr) ? variantOrArr[0] : variantOrArr; | ||
| if (nodeIndex === undefined) continue; | ||
| const node = this.nodes[nodeIndex]; | ||
| if ( | ||
| node !== undefined && | ||
| node.slot === slot && | ||
| node.proposerIndex === proposerIndex && | ||
| node.ptcTimeliness | ||
| ) { | ||
| result.push(node); | ||
| } | ||
| } |
There was a problem hiding this comment.
Iterating over all indices in the ProtoArray on every updateHead call (via shouldApplyProposerBoost) is a significant performance bottleneck, especially during periods of long unfinality when the number of nodes can be large. Since updateHead is called for every attestation and block, this this.nodes directly is more efficient than iterating Map entries, and we can filter by payloadStatus to ensure we only check one variant per block root.
for (let i = this.nodes.length - 1; i >= 0; i--) {
const node = this.nodes[i];
if (
node.slot === slot &&
node.proposerIndex === proposerIndex &&
node.blockRoot !== excludeRoot &&
node.ptcTimeliness &&
(isGloasBlock(node) ? node.payloadStatus === PayloadStatus.PENDING : true)
) {
result.push(node);
}
}There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9bc6eb6ce5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| slotsPerEpoch: SLOTS_PER_EPOCH, | ||
| committeePercent: this.config.REORG_HEAD_WEIGHT_THRESHOLD, | ||
| }); | ||
| const parentNode = this.protoArray.getNode(parentBlock.blockRoot, PayloadStatus.PENDING); |
There was a problem hiding this comment.
Use parent’s canonical payload status for weak-head lookup
When shouldApplyProposerBoost() evaluates a Gloas boosted block whose parent is still pre-Gloas (the common fork-boundary case where parent is from the immediately previous slot), this.protoArray.getNode(parentBlock.blockRoot, PayloadStatus.PENDING) can throw INVALID_NODE_INDEX because pre-Gloas roots only have a FULL variant. This makes updateHead() fail during proposer-boost evaluation right at transition conditions instead of cleanly applying/withholding boost. The lookup should use the parent’s canonical/default payload status (or parentBlock.payloadStatus) rather than hard-coding PENDING.
Useful? React with 👍 / 👎.
Performance Report✔️ no performance regression detected Full benchmark results
|
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into the existing `forkChoiceTest` runner. The on-disk layout matches the standard spec-test layout (`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`), so it slots in alongside `fork_choice` and `sync` runners. Three test-only accommodations the compliance fixtures require: 1. `bls_setting: 2` — every compliance fixture uses placeholder signatures. Pass `validSignatures: testcase.meta?.bls_setting !== BigInt(1)` to `chain.processBlock` so verification short-circuits. Standard `fork_choice` fixtures use `bls_setting: 1` so behavior there is unchanged. 2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally re-import the same block (`dup_shift` mutations in their `meta.yaml`). Spec semantics for `on_block(store, known_block)` is a no-op success. Production block import correctly rejects with ALREADY_KNOWN; this runner treats that case as success only when the step is `valid: true`. 3. Cross-epoch attestation shuffling — `on_attestation` decodes aggregation_bits using the state at the attestation's target checkpoint, not the head state. The runner now resolves the right shuffling via ShufflingCache + regen (mirroring the production validation path) instead of `headState.epochCtx.getIndexedAttestation`, which only worked when the attestation's epoch happened to be in the head's epoch cache (±1 epoch) and broke on cross-epoch fork attestations surfaced by the compliance suite. Adds support for two compliance-only check fields: - `viable_for_head_roots_and_weights` (consensus-specs#3831): compared via `getViableHeads()`. Both sides are sorted by root before comparison since the spec doesn't fix order. - `head_payload_status` (gloas): mapped between our internal enum ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0, FULL=1, PENDING=2). Pass rate against the latest comptests workflow `small.tar.gz` artifact: fulu/fork_choice_compliance: 253/1472 cases pass (17.2%) Top remaining failures: - ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced a `block.proposer_index == get_beacon_proposer_index(head_state)` guard in `update_proposer_boost_root` that we do not yet implement; affects all forks (not just gloas equivocation handling). Tracked for follow-up alongside ChainSafe#9233. - ~1% `Invalid viable heads` — proposer-boost rounding on minimal preset (see `getViableHeads()` weight note).
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into the existing `forkChoiceTest` runner. The on-disk layout matches the standard spec-test layout (`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`), so it slots in alongside `fork_choice` and `sync` runners. Three test-only accommodations the compliance fixtures require: 1. `bls_setting: 2` — every compliance fixture uses placeholder signatures. Pass `validSignatures: testcase.meta?.bls_setting !== BigInt(1)` to `chain.processBlock` so verification short-circuits. Standard `fork_choice` fixtures use `bls_setting: 1` so behavior there is unchanged. 2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally re-import the same block (`dup_shift` mutations in their `meta.yaml`). Spec semantics for `on_block(store, known_block)` is a no-op success. Production block import correctly rejects with ALREADY_KNOWN; this runner treats that case as success only when the step is `valid: true`. 3. Cross-epoch attestation shuffling — `on_attestation` decodes aggregation_bits using the state at the attestation's target checkpoint, not the head state. The runner now resolves the right shuffling via ShufflingCache + regen (mirroring the production validation path) instead of `headState.epochCtx.getIndexedAttestation`, which only worked when the attestation's epoch happened to be in the head's epoch cache (±1 epoch) and broke on cross-epoch fork attestations surfaced by the compliance suite. Adds support for two compliance-only check fields: - `viable_for_head_roots_and_weights` (consensus-specs#3831): compared via `getViableHeads()`. Both sides are sorted by root before comparison since the spec doesn't fix order. - `head_payload_status` (gloas): mapped between our internal enum ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0, FULL=1, PENDING=2). Pass rate against the latest comptests workflow `small.tar.gz` artifact: fulu/fork_choice_compliance: 253/1472 cases pass (17.2%) Top remaining failures: - ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced a `block.proposer_index == get_beacon_proposer_index(head_state)` guard in `update_proposer_boost_root` that we do not yet implement; affects all forks (not just gloas equivocation handling). Tracked for follow-up alongside ChainSafe#9233. - ~1% `Invalid viable heads` — proposer-boost rounding on minimal preset (see `getViableHeads()` weight note).
Wire the consensus-specs Fork Choice Compliance suite (ChainSafe#3831) into the existing `forkChoiceTest` runner. The on-disk layout matches the standard spec-test layout (`tests/<preset>/<fork>/fork_choice_compliance/<handler>/<suite>/<case>/`), so it slots in alongside `fork_choice` and `sync` runners. Three test-only accommodations the compliance fixtures require: 1. `bls_setting: 2` — every compliance fixture uses placeholder signatures. Pass `validSignatures: testcase.meta?.bls_setting !== BigInt(1)` to `chain.processBlock` so verification short-circuits. Standard `fork_choice` fixtures use `bls_setting: 1` so behavior there is unchanged. 2. `BLOCK_ERROR_ALREADY_KNOWN` — compliance fixtures intentionally re-import the same block (`dup_shift` mutations in their `meta.yaml`). Spec semantics for `on_block(store, known_block)` is a no-op success. Production block import correctly rejects with ALREADY_KNOWN; this runner treats that case as success only when the step is `valid: true`. 3. Cross-epoch attestation shuffling — `on_attestation` decodes aggregation_bits using the state at the attestation's target checkpoint, not the head state. The runner now resolves the right shuffling via ShufflingCache + regen (mirroring the production validation path) instead of `headState.epochCtx.getIndexedAttestation`, which only worked when the attestation's epoch happened to be in the head's epoch cache (±1 epoch) and broke on cross-epoch fork attestations surfaced by the compliance suite. Adds support for two compliance-only check fields: - `viable_for_head_roots_and_weights` (consensus-specs#3831): compared via `getViableHeads()`. Both sides are sorted by root before comparison since the spec doesn't fix order. - `head_payload_status` (gloas): mapped between our internal enum ordering (PENDING=0, EMPTY=1, FULL=2) and spec ordering (EMPTY=0, FULL=1, PENDING=2). Pass rate against the latest comptests workflow `small.tar.gz` artifact: fulu/fork_choice_compliance: 253/1472 cases pass (17.2%) Top remaining failures: - ~80% `Invalid proposer boost root` — consensus-specs#4807 introduced a `block.proposer_index == get_beacon_proposer_index(head_state)` guard in `update_proposer_boost_root` that we do not yet implement; affects all forks (not just gloas equivocation handling). Tracked for follow-up alongside ChainSafe#9233. - ~1% `Invalid viable heads` — proposer-boost rounding on minimal preset (see `getViableHeads()` weight note).
Summary
Implements the
should_apply_proposer_boostlogic ethereum/consensus-specs/pull/4807.Changes
ptcTimelinessandproposerIndexfields toProtoBlockisBlockPtcTimelyto track PTC deadline timelinessshouldApplyProposerBoostwhich withholds boost when the parent is a weak, equivocating block from the previous slotfindEquivocatingBlocksinProtoArrayto detect proposer equivocationsgetWeightonshouldApplyProposerBoost()🤖 Generated with Claude Code