Skip to content

fix: revert anchor block PTC vote override#9244

Closed
lodekeeper wants to merge 6 commits intoChainSafe:unstablefrom
lodekeeper:fix/revert-anchor-ptc-votes
Closed

fix: revert anchor block PTC vote override#9244
lodekeeper wants to merge 6 commits intoChainSafe:unstablefrom
lodekeeper:fix/revert-anchor-ptc-votes

Conversation

@lodekeeper
Copy link
Copy Markdown
Contributor

@lodekeeper lodekeeper commented Apr 21, 2026

Summary

Revert the anchor-block PTC vote override added in #9188.

Upstream consensus-specs master fixed the underlying spec bug in:

  • c7a0a8527Remove incorrect anchor seed for payload votes (#5135)

specs/gloas/fork-choice.md#get_forkchoice_store on master now initializes:

  • payload_timeliness_vote={}

So Lodestar should stop force-seeding the anchor block to all-true in ProtoArray.initialize().

This PR removes the temporary 8-line override and returns anchor initialization to the default onBlock() behavior.

Verification

  • pnpm lint -- packages/fork-choice/src/protoArray/protoArray.ts
  • Attempted narrower unit/build verification in a fresh worktree, but that worktree hit unrelated workspace package-resolution/build setup failures before reaching meaningful fork-choice signal. The functional change here is a single-file revert of the exact temporary override from fix: initialize anchor block PTC votes to all-true #9188.

AI assistance

Drafted and validated with AI assistance.

Fixes #9239

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request removes the anchor block PTC vote override and the associated onExecutionPayload call in ProtoArray. While the removal of the PTC vote override aligns with the updated specification, the feedback indicates that removing the onExecutionPayload call for the anchor block introduces a regression. Specifically, without this call, the anchor block lacks a 'FULL' payload variant, which causes subsequent blocks extending it to be incorrectly treated as orphans. A code suggestion was provided to restore the necessary initialization for Gloas blocks.

}
}

return protoArray;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While reverting the PTC vote override is correct per the updated spec (commit c7a0a8527), removing the onExecutionPayload call for the anchor block introduces a regression.

According to the Gloas spec for get_forkchoice_store, payload_states must be initialized with {anchor_root: PAYLOAD_STATUS_FULL}. In the context of ProtoArray, this means the anchor block must have a FULL variant. If it only has PENDING and EMPTY variants (the default from onBlock), any subsequent block that attempts to extend the anchor's FULL variant will fail to find its parent in getBlockHexAndBlockHash and will be treated as an orphan.

The onExecutionPayload call should be preserved to ensure the anchor block is correctly initialized with its payload state.

    if (isGloasBlock(block as ProtoBlock) && block.executionPayloadBlockHash !== null) {
      protoArray.onExecutionPayload(
        block.blockRoot,
        currentSlot,
        block.executionPayloadBlockHash,
        (block as {executionPayloadNumber?: number}).executionPayloadNumber ?? 0,
        block.stateRoot,
        null,
        ExecutionStatus.Valid
      );
    }

    return protoArray;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — you're right, pushed 6e59a7b restoring the onExecutionPayload(anchor, …) seeding. Upstream c7a0a8527 only removes the PTC vote seeding (payload_timeliness_vote={} / payload_data_availability_vote={}); it doesn't change the semantics of the anchor's payload being considered executed. The anchor state's latestBlockHash is the hash of the executed payload, so marking the anchor's FULL variant here still matches the spec's intent of payload_states = {anchor_root: anchor_state.copy()}.

Scope of this PR is now narrowed back to exactly what the upstream spec fix requires: dropping the 8-line PTC vote override only.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walking this back — pushed 1681b0d reverting the seeding. Per nflaig's follow-up and re-reading consensus-specs master: get_forkchoice_store now has payloads={}, is_payload_verified is literally root in store.payloads, and should_extend_payload short-circuits on not is_payload_verified. So the anchor is deliberately unseeded by spec; our !hasPayload() guard already matches that. The block-production consequence is expected to be resolved by sync building up FULL variants as envelopes arrive for post-anchor blocks. Sorry for the initial direction flip.

nflaig
nflaig previously approved these changes Apr 21, 2026
Copy link
Copy Markdown
Member

@nflaig nflaig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, leaving up for @ensi321 for final approval/merge

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b86b6b22e4

ℹ️ 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".

Comment on lines 109 to 112
null
);

// Anchor block PTC votes must be all-true per spec get_forkchoice_store:
// payload_timeliness_vote={anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))}
// Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.4/specs/gloas/fork-choice.md#modified-get_forkchoice_store
if (protoArray.ptcVotes.has(block.blockRoot)) {
protoArray.ptcVotes.set(block.blockRoot, BitArray.fromBoolArray(Array.from({length: PTC_SIZE}, () => true)));

// In the spec, we have payload_states = {anchor_root: anchor_state.copy()}
// which means the anchor's "payload" is considered received
// Without FULL, blocks extending FULL from the anchor would be orphaned.
// TODO GLOAS: This is a bug in the spec. Keep this to pass the current spec test
// for now. Need to remove this when we work on v1.7.0-alpha.5
if (block.executionPayloadBlockHash !== null) {
protoArray.onExecutionPayload(
block.blockRoot,
currentSlot,
block.executionPayloadBlockHash,
(block as {executionPayloadNumber?: number}).executionPayloadNumber ?? 0,
block.stateRoot,
null,
ExecutionStatus.Valid
);
}
}

return protoArray;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve anchor payload availability in ProtoArray.initialize

By returning immediately after onBlock() in initialize(), post-Gloas anchors no longer get a FULL variant, so shouldExtendPayload() now always fails its !hasPayload() guard for the anchor path. That value is consumed by block production (produceBlockBody/prepareNextSlot) to choose latestExecutionPayloadBid.blockHash vs latestExecutionPayloadBid.parentBlockHash; when the anchor payload is actually available (latestBlockHash == latestExecutionPayloadBid.blockHash), this forces the wrong parent hash and can produce bids that violate processExecutionPayloadBid's parent-hash check. In practice, this can make the first block(s) after restart from a post-Gloas anchor invalid or rejected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — traced the call chain and this is a real regression. #9209 made block production (both produceBlockBody and prepareNextSlot) use forkChoice.shouldExtendPayload(headRoot) to pick between latestExecutionPayloadBid.blockHash and latestExecutionPayloadBid.parentBlockHash. With the anchor lacking a FULL variant, shouldExtendPayload(anchor) short-circuits to false via the !hasPayload() guard, so the first bid after restart from a post-Gloas anchor uses bid.parentBlockHash and fails processExecutionPayloadBid's parent-hash check.

Pushed 6e59a7b restoring the onExecutionPayload(anchor, …) seeding while keeping the PTC vote override removal (which is the only piece the upstream c7a0a8527 spec fix actually requires).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be handled by sync

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — reverted in 1681b0d. Post-restart proto-array picks up FULL variants via onExecutionPayload as sync processes envelopes for post-anchor blocks; no init-time seeding needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Walking this back — pushed 1681b0d reverting the seeding. Per nflaig's follow-up and re-reading consensus-specs master: the spec deliberately leaves the anchor unseeded (payloads={}, and should_extend_payload returns False via the is_payload_verified guard). Our !hasPayload() short-circuit is the spec-correct behavior, not a regression. The first-bid-after-restart edge case you described is expected to be handled by sync — proto-array picks up FULL variants as envelopes are processed for post-anchor blocks during catch-up, before the validator is eligible to propose. Sorry for the initial direction flip.

Per bot review feedback on PR: removing the `onExecutionPayload(anchor, ...)`
call alongside the PTC vote override broke block production after restart from
a post-Gloas anchor. `shouldExtendPayload(anchorRoot)` (used in ChainSafe#9209) short-
circuits to false via `!hasPayload()`, so `produceBlockBody`/`prepareNextSlot`
pick `latestExecutionPayloadBid.parentBlockHash` instead of `.blockHash`,
producing bids that fail `processExecutionPayloadBid`'s parent-hash check.

Upstream consensus-specs `c7a0a8527` removes only the PTC vote seeding
(`payload_timeliness_vote={}`, `payload_data_availability_vote={}`) — it does
not remove the FULL-payload semantics of the anchor. The anchor state's
`latestBlockHash` is the executed payload hash, so `onExecutionPayload(anchor)`
still matches the spec's intent of `payload_states = {anchor_root: ...}`.

Keep the PTC vote override removal (the actual subject of this PR); restore
the FULL-variant seeding with a focused comment.

🤖 Generated with AI assistance
Comment on lines +112 to +115
// Gloas: seed the anchor's FULL variant so `shouldExtendPayload(anchor)` is true
// for block production; otherwise bids after restart use `bid.parentBlockHash`
// and fail `processExecutionPayloadBid`'s parent-hash check.
if (isGloasBlock(block as ProtoBlock) && block.executionPayloadBlockHash !== null) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is wrong, please see latest consensus-specs on master branch, it's not correct to seed the payload of the anchor block

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, pushed 1681b0d reverting the seeding. Re-checking consensus-specs master specs/gloas/fork-choice.md:

  • get_forkchoice_store initializes payloads={} — anchor is deliberately not payload-verified.
  • is_payload_verified(root) is literally return root in store.payloads, so is_payload_verified(anchor_root) == False.
  • should_extend_payload short-circuits with if not is_payload_verified(store, root): return False.

So our existing !hasPayload() guard in shouldExtendPayload already matches spec behavior on the unseeded anchor — returning false is correct, not a regression. My earlier reply to the bots was wrong; sorry for the noise. PR is now back to just the 8-line PTC-vote-override revert, matching c7a0a8527 exactly.

…alize"

This reverts commit 6e59a7b.

Per @nflaig review feedback (discussion_r3116954612, discussion_r3116958318)
and re-reading consensus-specs `master` `specs/gloas/fork-choice.md`:

- `get_forkchoice_store` initializes `payloads={}` — the anchor is
  deliberately not seeded as payload-verified.
- `is_payload_verified(root)` is literally `return root in store.payloads`,
  so `is_payload_verified(anchor_root) == False` after restart.
- `should_extend_payload` on the anchor returns `False` via the
  `if not is_payload_verified: return False` short-circuit.

Lodestar's `shouldExtendPayload`'s `!hasPayload()` guard therefore matches
spec behavior — returning `false` on the unseeded anchor is correct, not a
bug. The block-production bid-selection consequence is an edge case that
sync is expected to resolve (proto-array gets FULL variants for post-anchor
blocks as their envelopes are processed during catch-up).

🤖 Generated with AI assistance
@wemeetagain wemeetagain added the spec-gloas Issues targeting the Glamsterdam spec version label Apr 21, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 52.53%. Comparing base (9fa9f08) to head (1681b0d).

Additional details and impacted files
@@            Coverage Diff            @@
##           unstable    #9244   +/-   ##
=========================================
  Coverage     52.53%   52.53%           
=========================================
  Files           848      848           
  Lines         61405    61405           
  Branches       4525     4525           
=========================================
  Hits          32259    32259           
  Misses        29081    29081           
  Partials         65       65           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@nflaig
Copy link
Copy Markdown
Member

nflaig commented Apr 21, 2026

this might need alpha.5 to pass spec tests

Required to pass spec tests that rely on the alpha.5 anchor-block
payload-parent semantics (consensus-specs ChainSafe#5067 "Treat genesis block
as empty payload parent"), which aligns with reverting the anchor
block PTC vote override.

🤖 Generated with AI assistance
@lodekeeper
Copy link
Copy Markdown
Contributor Author

Done — merged unstable (to pick up the alpha.4 bump from #9211) and pushed chore: bump spec tests to v1.7.0-alpha.5 (47f27f4). alpha.5 includes consensus-specs #5067 "Treat genesis block as empty payload parent", which is the semantics this revert relies on.

@lodekeeper
Copy link
Copy Markdown
Contributor Author

Reverted the alpha.5 spec bump (7477fb6836) because it introduced broader failures than it fixed. Summary of what I found while investigating:

Why spec tests fail on this PR (alpha.3 baseline, commit 1681b0da18):
23 gloas fork_choice tests fail (get_head/filtered_block_tree, on_block/justification_*, reorg/*, withholding/*) — all Error: Invalid head at step N assertions. These test fixtures encode the pre-alpha.5 anchor semantics, so the PR's revert (which aligns us with the new alpha.5 semantics per consensus-specs #5067 "Treat genesis block as empty payload parent") is inherently incompatible with them.

Why bumping to alpha.5 wasn't a drop-in (47f27f4158, now reverted):

  • alpha.5 introduces a new fast_confirmation test runner → Spec tests (24) still fails with No test runner for fast_confirmation.
  • alpha.5 renames (on_execution_payloadon_execution_payload_envelope) plus new gloas/phase0 helpers → check-specrefs fails with a wide wave of empty-source requirements in specrefs/functions.yml (~1200+ lines of updates locally, still not clean).
  • Pinning the specific anchor-fix consensus-specs commit (c7a0a852…) instead of the tag doesn't help — that commit already includes the full fast_confirmation + on_execution_payload_envelope surface.

Scope summary: Making this PR pass spec tests requires a genuine alpha.5 integration — new runner implementation + specrefs/source-mapping updates + rename cascade. That's substantially out of scope for this focused revert.

Options:

  1. Land alpha.5 integration as a separate PR first, then rebase this one.
  2. Skip the 23 gloas fork_choice tests on alpha.3/alpha.4 with a tracking reference (quality tradeoff — loses coverage for the exact area this PR touches).
  3. Close this PR pending alpha.5 integration.

Which direction would you prefer? Happy to pick up any of them.

@nflaig
Copy link
Copy Markdown
Member

nflaig commented Apr 23, 2026

@lodekeeper we have merged alpha.5 into unstable, please update your branch, also double check if the changes are still needed and make sure you are passing spec tests

@lodekeeper
Copy link
Copy Markdown
Contributor Author

Thanks @nflaig — double-checked against alpha.5 and the anchor PTC override should stay, so this PR is no longer needed.

get_forkchoice_store at v1.7.0-alpha.5 (spec link):

payload_timeliness_vote={
    anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))
},
payload_data_availability_vote={
    anchor_root: Vector[boolean, PTC_SIZE](True for _ in range(PTC_SIZE))
},

The spec fix consensus-specs#5135 ("Remove incorrect anchor seed for payload votes") did NOT make the alpha.5 cut — it's only on master. So the anchor all-true seeding is still required for alpha.5 spec tests, which #9188 correctly added (and unstable now points the spec URL at alpha.5 in 6b7eebb).

Closing this PR. Will revisit when a spec tag picks up #5135.

Side note: alpha.5 also seeds payload_data_availability_vote to all-true for the anchor, but ProtoArray.initialize() only seeds ptcVotes (payload_timeliness_vote). Whether that matters for us depends on if/how the DA-availability vote map is consulted in fork-choice — I'll file a separate issue if there's a gap.

@lodekeeper
Copy link
Copy Markdown
Contributor Author

Closing: alpha.5 spec still requires anchor PTC all-true seeding (see previous comment). Revisit when spec tag picks up consensus-specs#5135.

@lodekeeper lodekeeper closed this Apr 23, 2026
@nflaig
Copy link
Copy Markdown
Member

nflaig commented Apr 23, 2026

Closing: alpha.5 spec still requires anchor PTC all-true seeding (see previous comment). Revisit when spec tag picks up consensus-specs#5135.

@lodekeeper please check the master branch of the consensus specs repo

@lodekeeper
Copy link
Copy Markdown
Contributor Author

Checked master — consensus-specs#5135 (Remove incorrect anchor seed for payload votes, c7a0a8527) is present on master and get_forkchoice_store there initializes both payload_timeliness_vote={} and payload_data_availability_vote={}.

master is 21 commits ahead of v1.7.0-alpha.5; no newer tag exists yet. So the situation is:

  • alpha.5 (current spec-test target): anchor PTC seed still all-true → override must stay.
  • master (future): anchor seed empty → override becomes obsolete.

I'll reopen a PR of the same shape once a spec tag (alpha.6 or later) includes #5135. Until then no branch action needed, and #9244 stays closed.

Related: I also noticed alpha.5 seeds payload_data_availability_vote={anchor_root: all-true} but ProtoArray.initialize() only seeds ptcVotes (payload_timeliness_vote). Whether this matters depends on if/how the DA-availability vote is consulted in our fork-choice — I'll look and file a separate issue if there's a real gap.

@nflaig
Copy link
Copy Markdown
Member

nflaig commented Apr 23, 2026

@lodekeeper please make sure we follow latest master branch of the spec! either update this PR or open a new one

@lodekeeper
Copy link
Copy Markdown
Contributor Author

Re-opened as #9264 on top of current unstable (alpha.5 spec-test target), per @nflaig's direction to follow latest master branch of the spec.

Change is identical — removes the 8-line anchor-PTC-all-true override in ProtoArray.initialize() so get_forkchoice_store matches consensus-specs master (payload_timeliness_vote={}, payload_data_availability_vote={}) post consensus-specs#5135.

Noted in the new PR that alpha.5 still carries the old seed, so fork-choice spec tests covering the anchor boundary may flag until spec-tests-version.json bumps to a tag that picks up #5135.

nflaig pushed a commit that referenced this pull request Apr 23, 2026
## Summary

Revert the anchor-block PTC vote all-true override added in #9188.

Upstream `consensus-specs` `master` fixed the underlying spec bug in:

-
[`c7a0a8527`](ethereum/consensus-specs@c7a0a8527)
— **Remove incorrect anchor seed for payload votes (#5135)**


[`specs/gloas/fork-choice.md#get_forkchoice_store`](https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/fork-choice.md#modified-get_forkchoice_store)
on `master` now initializes:

```python
payload_timeliness_vote={},
payload_data_availability_vote={},
```

So Lodestar should stop force-seeding the anchor block's PTC votes to
all-true in `ProtoArray.initialize()` — anchor initialization returns to
the default `onBlock()` behavior.

This PR removes the temporary 8-line override.

## Tag vs. master

\`v1.7.0-alpha.5\` (current spec-test target) still carries the old
anchor all-true seed — #5135 landed on spec master **after** the alpha.5
cut. Aligning Lodestar with `master` now means:

- the fix is in place for the next spec tag (alpha.6+) picking up #5135
- fork-choice spec tests generated from alpha.5 may flag the anchor-seed
boundary until `spec-tests-version.json` bumps to that next tag

Per direction from @nflaig to follow latest `master` of the spec.

## Context

- Supersedes closed PR #9244 (closed when alpha.5 was still the current
tag and kept the old seed).
- Fixes #9239.

## AI assistance

Drafted and validated with AI assistance.

Co-authored-by: lodekeeper <lodekeeper@users.noreply.github.com>
@lodekeeper
Copy link
Copy Markdown
Contributor Author

Done in #9264 — same-shape single-commit revert of the anchor PTC all-true override, aligned to consensus-specs master (#5135 / c7a0a85274).

CI is green on #9264 including the Spec tests (24) job against v1.7.0-alpha.5 — turns out the anchor PTC seed isn't exercised by any alpha.5 fork-choice test currently run, so no further boundary handling needed. PR body explicitly documents the tag-vs-master gap.

Keeping #9244 closed as the superseded draft.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

spec-gloas Issues targeting the Glamsterdam spec version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Revert PR #9188 (anchor block PTC votes to all-true) once consensus-specs gloas get_forkchoice_store bug is fixed

3 participants