Skip to content

Make ExecutionHash not option in ForkChoiceParams#8901

Open
dapplion wants to merge 5 commits into
sigp:unstablefrom
dapplion:execution-hash-not-option
Open

Make ExecutionHash not option in ForkChoiceParams#8901
dapplion wants to merge 5 commits into
sigp:unstablefrom
dapplion:execution-hash-not-option

Conversation

@dapplion

Copy link
Copy Markdown
Collaborator

Issue Addressed

Builds on #8761. Now that merge transition code is being removed, execution block hashes no longer need to be Option types throughout the fork choice and beacon chain.

Two key changes:

  1. Blocks before the merge are just marked as valid. This is only relevant for genesis sync. Setting the those block statuses to valid instead of irrelevant is a rename simplification for a rarely used path
  2. If the finalized or justified checkpoint point to an unknown block root in fork-choice that becomes a hard error. Before in unstable, this failure was confusing / hidden since it just returned None = as blocks before the merge

  • This PR helps make things simpler for Safe non-finalized checkpoint sync #8382. In that PR it is possible for the justified and finalized checkpoints to point to unknown blocks, so we want to make that failure case clear and explicit

@dapplion dapplion added the ready-for-review The code is ready for review label Feb 26, 2026

@dapplion dapplion left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Deleted

///
/// This `bool` only exists to satisfy our SSZ implementation which requires all variants
/// to have a value. It can be set to anything.
Irrelevant(bool),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@michaelsproul we know that no blocks in fork-choice have this status. If we remove this variant knowing that it is un-used, we are fine right?

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.

I think we should update the v29 schema migration to do Irrelevant -> Valid(0x00). That would require keeping this variant around a little longer too.

@dapplion

Copy link
Copy Markdown
Collaborator Author

Let's hold up to have a design of fork-choice Gloas

@michaelsproul michaelsproul added fork-choice and removed ready-for-review The code is ready for review labels Mar 10, 2026
@dapplion dapplion force-pushed the execution-hash-not-option branch 3 times, most recently from 6fde121 to 353a886 Compare April 13, 2026 20:11
- Remove ExecutionStatus::Irrelevant and make execution hashes non-optional
- Handle pre-merge blocks with default payloads as valid
- Remove cached head state from fork choice
- Replace unwrap_or_default with proper error handling for block lookups
- Dedup forkchoice_update_parameters into helper function
- Handle pre-merge anchor blocks without execution payloads
- Handle pre-merge blocks without execution payloads
- Document forkchoice_update_parameters failure conditions
@dapplion dapplion force-pushed the execution-hash-not-option branch from 353a886 to 06c94b1 Compare April 13, 2026 20:28
Comment on lines +112 to +117
/// The `execution_payload.block_hash` of the block at the head of the chain.
head_hash: ExecutionBlockHash,
/// The `execution_payload.block_hash` of the justified block.
justified_hash: ExecutionBlockHash,
/// The `execution_payload.block_hash` of the finalized block.
finalized_hash: ExecutionBlockHash,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is the key change: when the head changes we MUST know the execution hashes of its head / just / fin blocks

.get_block_execution_block_hash(&fork_choice.finalized_checkpoint().root)
.ok_or(Error::BlockMissingFromForkChoice(
fork_choice.finalized_checkpoint().root,
))?,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We expect fork-choice to contain nodes for the head + justified block + finalized block. This invariant is always true and pruning removes nodes prior to finalized and justified is always a descendant of finalized. When we initialize fork-choice there's always a node for the justified and finalized checkpoint

// Gloas blocks don't contain an execution payload.
Some(PayloadVerificationStatus::Irrelevant)
} else if is_execution_enabled(state, block.message().body()) {
Some(PayloadVerificationStatus::Verified)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We coerce Irrelevant status to Verified to not have to deal with Option return types. Semantically it's the same.

/// Attestations that arrived at the current slot and must be queued for later processing.
queued_attestations: Vec<QueuedAttestation>,
/// Stores a cache of the values required to be sent to the execution layer.
forkchoice_update_parameters: ForkchoiceUpdateParameters,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Caching this here is unnecessary. The values are already cached in canonical head. Removing them from here simplifies reasoning about them and moves the error condition (justified / finalized nodes not present) outside of the fork-choice code

.ok_or(Error::MissingPersistedForkChoice)?;
let current_slot_for_head = fork_choice.fc_store().get_current_slot();
let (_, head_payload_status) = fork_choice.get_head(current_slot_for_head, spec)?;
let fork_choice_view = fork_choice.cached_fork_choice_view();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No cached view, we recompute the head on init

ExecutionStatus::Valid(ExecutionBlockHash::zero()),
None,
None,
)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fork-choice does not process pre-merge blocks, and this change is cosmetic. If we accept having Valid(0) for this case, the simplicity of not having to think about Option in downstream code is worth it.

// V29 (Gloas) nodes don't have execution_status. Use
// execution_payload_block_hash which is always present on V29
// (the zero fallback is cosmetic and unreachable).
execution_status: block.execution_status().unwrap_or_else(|_| {

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.

Could maybe use None for Gloas instead, to prevent misuse like:

  • get_head -> block root B
  • get_block(B)
  • look at block_B.execution_status
  • assume block_B.execution_status is valid (e.g. send to EL in fcU), even though we may not have seen this block

Or another option could be to check payload_received and set Valid if it is true, or Optimistic otherwise. Although this is also a conflation.

Replace the `Valid(0)` coercion at the canonical-head / fork-choice-update
boundary with an explicit `FcuHash { PreMerge, Hash }` enum.

`ForkchoiceUpdateParameters` and `CachedHead`'s head/justified/finalized
hash fields move from `ExecutionBlockHash` to `FcuHash`. The Engine API
boundary still uses zero as the documented sentinel for "no terminal
block known"; convert with `FcuHash::to_engine_hash` only there.

Pre-merge detection switches from `== ExecutionBlockHash::zero()` to
`is_pre_merge()` in `is_healthy` and the startup fcU guard.

Internally proto-array still stores `ExecutionStatus::Valid(zero)` for
pre-merge blocks; pushing `PreMerge` deeper would touch SSZ on-disk
schema and is left as a follow-up.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants