Skip to content

Update state transition for spec v1.7.0-alpha.14 - #841

Merged
pk910 merged 2 commits into
masterfrom
pk910/1.7.0-alpha.4
Aug 20, 2026
Merged

Update state transition for spec v1.7.0-alpha.14#841
pk910 merged 2 commits into
masterfrom
pk910/1.7.0-alpha.4

Conversation

@pk910

@pk910 pk910 commented Aug 20, 2026

Copy link
Copy Markdown
Member

Upgrades the state transition in indexer/beacon/statetransition/ from consensus-specs v1.7.0-alpha.13 to v1.7.0-alpha.14.

Spec delta

The raw alpha.13 → alpha.14 diff is ~5,900 lines, but nearly all of it is cosmetic: the spec now declares named SSZ type aliases (Balances, Withdrawals, PTC, PTCWindow, ProposerIndices, …) and containers reference those instead of inline ProgressiveList[…] / Vector[…]. Diffing the spec's Python blocks function-by-function across every fork shows phase0 through fulu have zero functional changes.

Only two changes affect the state transition, both Gloas.

1. upgrade_to_gloas populates the full latest_execution_payload_bid

Previously only block_hash, gas_limit and execution_requests_root were seeded. alpha.14 also sets parent_block_hash (from the pre-state payload header), parent_block_root and slot (from pre.latest_block_header), prev_randao, and builder_index = BUILDER_INDEX_SELF_BUILD.

This changes the post-fork state root, so it is required for correct replay across the Gloas fork boundary. Adds a BuilderIndexSelfBuild constant (UINT64_MAX).

2. Gloas process_attestation gains the had_no_participation gate

Builder payment weight now also requires that the validator held no participation flags for that epoch before the attestation was applied. alpha.13's will_set_new_flag check alone was insufficient: when two aggregates for the same slot each set a different new flag, the validator's effective balance was added to the payment weight twice.

Not covered here (non-STF)

  • GAS_LIMIT_SCHEDULE config and get_scheduled_gas_limit — block-building only, no state effect.
  • MAX_BYTES_PER_INCLUSION_LISTMAX_TRANSACTIONS_BYTES_PER_INCLUSION_LIST — not referenced by dora.
  • New EIP8321 feature fork — not a scheduled fork.
  • Heze InclusionList.inclusion_list_committee_rootdependent_root — needs a go-eth2-client rename first, then handlers/slot.go and handlers/api/slot_inclusion_lists_v1.go follow. SSZ shape is unchanged (RootRoot), so nothing breaks in the meantime.

Verification

  • Both changed functions checked field-by-field against the alpha.14 spec text.
  • Confirmed every newly-set bid field actually reaches the hash tree root through the all.ExecutionPayloadBid wrapper — each field independently changes the root, and nil vs. empty blob_kzg_commitments hash identically.
  • Audited dora's coverage of all 56 Gloas spec functions. The only gaps are assert-only or signature-verification helpers (process_payload_attestation mutates no state, can_builder_cover_bid, the two signature verifiers), which is consistent with replaying canonical blocks.
  • Cross-checked process_execution_payload_bid against alpha.14 — state effects match (dora reads s.Slot / block.ProposerIndex where the spec reads bid.slot / get_beacon_proposer_index, equivalent under the spec's own asserts).
  • go build, go vet, gofmt and go test ./indexer/beacon/... all clean.
  • Pre-Gloas replay on glamsterdam-devnet-8 (alpha.14, slots 45601–45617): every state root matched.

Known gap

The new upgrade_to_gloas bid has not been validated against a real post-fork state root yet. devnet-7 is still on alpha.13; glamsterdam-devnet-8 is on alpha.14 but its GLOAS_FORK_EPOCH is 1536 and head was around epoch 1425 at the time of writing. The definitive check is to replay devnet-8 across slot 49152 with cmd/statetransition-test once it forks.

@redpandabot redpandabot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR ports the two functional state-transition changes from consensus-specs alpha.13 to alpha.14: upgrade_to_gloas now seeds the full latest_execution_payload_bid (parent_block_hash/parent_block_root/prev_randao/slot and BUILDER_INDEX_SELF_BUILD), and process_attestation gates builder payment weight on had_no_participation. I verified both functions field-by-field against the alpha.14 spec text, confirmed every newly-set bid field exists on the go-eth2-client types and reaches the state HTR via the all.ExecutionPayloadBid wrapper, and traced the fork-upgrade timing and the downstream consumer. The changes match the spec exactly with no regressions found.


Reviewed 3 changed file(s) @ 157c3d4f — no blocking issues found.
"Weeks of coding can save you hours of planning."

@redpandabot

redpandabot Bot commented Aug 20, 2026

Copy link
Copy Markdown

Summary

Bumps the Gloas state transition to consensus-specs v1.7.0-alpha.14: upgrade_to_gloas now seeds the full latest_execution_payload_bid (verified field-by-field against the alpha.14 fork.md, including BUILDER_INDEX_SELF_BUILD), process_attestation gains the had_no_participation gate exactly as the spec has it, and db/slots.go drops a dead assignment. The two functional changes the description claims are both correct — but the description is incomplete: alpha.14 also changed upgrade_to_gloas to seed builder_pending_payments empty, which this PR leaves at alpha.13's 2*SLOTS_PER_EPOCH entries.

Issues

  • 🟡 indexer/beacon/statetransition/fork.goupgrade_to_gloas still seeds builder_pending_payments per alpha.13, not alpha.14 — The alpha.13→alpha.14 upgrade diff has a third functional change the PR didn't cover and its description denies: builder_pending_payments, seeded [BuilderPendingPayment() for _ in range(2 * SLOTS_PER_EPOCH)] in alpha.13, is BuilderPendingPayments() (empty) in alpha.14 (fork.md). fork.go:84-88 still allocates 2*SLOTS_PER_EPOCH empty payments, so the post-fork state HTR still diverges from an alpha.14 chain as written — the exact parity the bid-field change is meant to restore. Impact is bounded (dora skips replay across the fork boundary and the fork state is API-loaded; the surplus empty payments carry zero weight, so extracted data is unaffected), and the alpha.14 empty-list text is arguably inconsistent with the rest of the payment pipeline, hence concern rather than blocker — but either it should be switched to an empty list (making the first process_builder_pending_payments grow it) or the discrepancy should be acknowledged as intentional.

Reviewed @ 9c31744d
"Weeks of coding can save you hours of planning."

@pk910

pk910 commented Aug 20, 2026

Copy link
Copy Markdown
Member Author
  • 🟡 indexer/beacon/statetransition/fork.goupgrade_to_gloas still seeds builder_pending_payments per alpha.13, not alpha.14 — The alpha.13→alpha.14 upgrade diff has a third functional change the PR didn't cover and its description denies: builder_pending_payments, seeded [BuilderPendingPayment() for _ in range(2 * SLOTS_PER_EPOCH)] in alpha.13, is BuilderPendingPayments() (empty) in alpha.14 (fork.md). fork.go:84-88 still allocates 2*SLOTS_PER_EPOCH empty payments, so the post-fork state HTR still diverges from an alpha.14 chain as written — the exact parity the bid-field change is meant to restore. Impact is bounded (dora skips replay across the fork boundary and the fork state is API-loaded; the surplus empty payments carry zero weight, so extracted data is unaffected), and the alpha.14 empty-list text is arguably inconsistent with the rest of the payment pipeline, hence concern rather than blocker — but either it should be switched to an empty list (making the first process_builder_pending_payments grow it) or the discrepancy should be acknowledged as intentional.

This finding is incorrect — no change needed.

BuilderPendingPayments is a fixed-length SSZ Vector, not a list. From alpha.14 gloas/beacon-chain.md:

class BuilderPendingPayments(Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH]):

BuilderPendingPayments() is therefore the Vector's default value: 2 * SLOTS_PER_EPOCH default-constructed elements — not an empty list. Verified empirically against remerkleable (the library pyspec uses):

alpha.13 form: len 64, root 87eb0ddba57e35f6
alpha.14 form: len 64, root 87eb0ddba57e35f6
Even BuilderPendingPayments([]) yields a length-64 vector — a Vector cannot be empty and cannot grow. So the alpha.13→alpha.14 edit here is the same cosmetic alias substitution as the rest of that diff (an explicit list comprehension replaced by the alias's default constructor), the post-fork HTR is unchanged, and fork.go:84-88 is already correct.

Three independent cross-checks agree:

  1. process_builder_pending_payments does old_payments = state.builder_pending_payments[SLOTS_PER_EPOCH:] then appends 32 new ones. That only round-trips to a Vector[..., 64] if the field is already length 64 at the fork.
  2. process_execution_payload_bid indexes builder_pending_payments[SLOTS_PER_EPOCH + bid.slot % SLOTS_PER_EPOCH] in the very first post-fork block — out of range on an empty vector.
  3. The proposed remedy (seed empty, let process_builder_pending_payments grow it) isn't expressible in SSZ.

The same pattern appears elsewhere in the alpha.14 diff and is likewise non-functional: heze/fork.md swaps BitVectorINCLUSION_LIST_COMMITTEE_SIZE for InclusionListBits(). By contrast electra/fork.md's pending_deposits=PendingDeposits() is an empty list — because that alias wraps a List, not a Vector. The distinction is the alias's base type, which is why the two-functional-changes count in the PR description holds.

@pk910
pk910 merged commit 1f88c1e into master Aug 20, 2026
5 checks passed
@pk910
pk910 deleted the pk910/1.7.0-alpha.4 branch August 20, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants