Skip to content

Support Glamsterdam HF - #1

Open
yoshidan wants to merge 21 commits into
mainfrom
feature/initial
Open

yoshidan wants to merge 21 commits into
mainfrom
feature/initial

Conversation

@yoshidan

@yoshidan yoshidan commented May 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Support the Gloas (Glamsterdam) HF. For Gloas and later forks, LightClientHeader carries only execution_block_hash instead of the full ExecutionPayloadHeader (EIP-7732), so the execution update is built from and verified against the RLP-encoded execution block header instead of SSZ merkle proofs.

Requires the Gloas-aware verifier (datachainlab/ethereum-light-client-rs#29).

Changes

Rust (type / proto)

  • proto: append rlp = 7 to ExecutionUpdate and execution_block_hash_gindex = 7 to ForkSpec. Both are appended so existing encodings remain valid.
  • ExecutionUpdateInfo: add rlp and implement ExecutionUpdate::rlp(). For Gloas the verifier checks keccak256(rlp) == execution_block_hash and that state_root / block_number match the RLP header.
  • validate_execution_update: skip the block hash merkle validation for Gloas (fork_spec.is_gloas()); for pre-Gloas, derive the in-payload block hash gindex from the block number gindex. Remove the unused validate_execution_update_with_root.
  • Bump ethereum-light-client-rs to the Gloas-aware revision.

Go (prover)

  • beacon: parse the Gloas LightClientHeader form (execution_block_hash) and add IsGloas().
  • execution: add RPCClient and GetRawHeader (fetches the RLP-encoded header via debug_getRawHeader).
  • relay:
    • GloasSpec with the latest spec gindices (finalized_root=735, current/next_sync_committee=2945/2946, execution_block_hash=2856).
    • BuildExecutionUpdateFromFinalizedHeader now takes an execution RPC client and branches: Gloas builds the update from the RLP header (BuildExecutionUpdateFromBlockHash), pre-Gloas keeps the SSZ merkle proofs.
    • GetForkParameters handles the minimal-ethpandaops network (ethpandaops fork-version scheme used by kurtosis devnets) and the Gloas fork schedule.

Notes

  • BuildExecutionUpdateFromFinalizedHeader signature change is breaking; ethereum/optimism relay provers have been adapted on their glamsterdam branches.

@yoshidan
yoshidan force-pushed the feature/initial branch 2 times, most recently from 0b5a67a to 8729311 Compare May 30, 2026 08:03
yoshidan and others added 16 commits August 3, 2026 13:42
Inserting rlp at field 5 renumbered block_hash/block_hash_branch and
broke decoding of payloads encoded with the pre-Gloas schema
(block_hash_branch decoded as empty). Append rlp as field 7 instead so
existing encodings remain valid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Align the field order with the proto definition (rlp = 7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t for Gloas

For Gloas the execution root is the block hash itself, so
validate_execution_update requires block_hash to equal it instead of
skipping the validation, and the relayer populates block_hash from the
execution block hash. This keeps block_hash a verified value across
forks for L2 clients that use it as the L1 head (e.g. optimism-elc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gloas proves the execution header by hashing its RLP instead of walking SSZ
merkle branches, so BuildExecutionUpdateFromBlockHash leaves StateRootBranch
and BlockNumberBranch unset. ValidateBasic still required both, so every
relayer update on a post-Gloas chain failed with
"state root branch cannot be nil".

The e2e did not catch this because it builds the request directly and never
goes through ValidateBasic, which is only reached from the relayer path
(ethereum-ibc-relay-prover: Header.ValidateBasic).

Accept an execution update without those branches when Rlp is present, and
check BlockHash instead since that is what the verifier compares against the
consensus update's finalized execution root.
Post-Gloas the execution block a light client header references is the one
`signed_execution_payload_bid.message.parent_block_hash` points at, i.e. the
parent of the block produced at the finalized slot. Its timestamp is therefore
not `compute_timestamp_at_slot(finalized_slot)`, so every relayer update on a
post-Gloas chain failed with UnexpectedTimestamp, off by one slot.

It cannot be compared against `finalized_slot - 1` either, since slots may be
skipped and the parent may be several slots back. Compare it against the
timestamp inside the RLP execution header instead: the consensus verifier
authenticates the RLP via `keccak256(rlp) == execution_block_hash`, so this
pins the timestamp the same way `state_root` and `block_number` are already
pinned, and keeps the check strict rather than relaxing it.

`validate_execution_header_timestamp` selects the rule from
`fork_spec.is_gloas()` so that every light client on this crate, including
optimism-elc, branches the same way. Both timestamp checks now live in
validate.rs next to the other execution update validation, leaving time.rs as
plain timestamp arithmetic with no ethereum_consensus dependency.
The consensus state records the finalized header's slot and pins its sync
committees to that slot's period, but the prover derived the period from the
client state height, which is an execution block number. Post-Gloas the light
client header references `signed_execution_payload_bid.message.parent_block_hash`,
so that block was produced one slot earlier than the finalized slot. Whenever the
finalized slot is the first slot of a period the derived period was one behind,
and the prover then sent the previous period's committee, failing with
InvalidNextSyncCommitteeKeys.

GetConsensusStateSlotWithBlockNumber resolves the slot the consensus state
actually records: pre-Gloas the execution block's own slot, post-Gloas the next
slot that has a block. It searches rather than adding one because slots may be
skipped and the parent may be several slots back.

GetPeriodWithBlockNumber now takes the minimal fork schedule so it can tell which
rule applies. Gloas is detected via `execution_block_hash_gindex`, the same
discriminator the Rust verifier uses.
Taking the next slot that has a block assumes it bids on the block being
looked up. Compare the bid's parent_block_hash instead so a divergence
surfaces as an error rather than a wrong sync committee period.
Fork resolution belongs to ForkParameters rather than a helper that takes a
network name and the minimal fork schedule, so the slot helpers now accept
the resolved parameters.
Comment thread type/Cargo.toml Outdated
@yoshidan
yoshidan marked this pull request as ready for review September 15, 2026 00:45
The gloas work was merged and tagged upstream, so stop pinning a commit on the
fork. v0.3.0 is the fork's fd7fe9a plus the no_std CI toolchain pin and the gloas
verifier tests, so no production code changes.

Downstream crates (ethereum-elc, optimism-elc) must move to the same tag in the
same step: cargo treats the fork and upstream URLs as distinct sources, so a mixed
graph builds two ethereum-consensus crates and fails to typecheck.
@yoshidan
yoshidan requested a review from siburu October 1, 2026 07:02
eth-clients/sepolia now schedules it at epoch 353024, which is timestamp 1791294816
and matches geth's SepoliaChainConfig AmsterdamTime. Mainnet stays unscheduled.
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.

1 participant