[runtime]: Glamsterdam (Gloas) support for the sync committee client - #1056
Draft
dharjeezy wants to merge 31 commits into
Draft
[runtime]: Glamsterdam (Gloas) support for the sync committee client#1056dharjeezy wants to merge 31 commits into
dharjeezy wants to merge 31 commits into
Conversation
Wizdave97
reviewed
Jul 20, 2026
Wizdave97
reviewed
Aug 15, 2026
| @@ -459,4 +492,25 @@ pub struct BeaconState< | |||
| #[cfg(not(feature = "nofulu"))] | |||
Member
There was a problem hiding this comment.
Remove this feature flag no longer useful
Suggested change
| #[cfg(not(feature = "nofulu"))] |
Wizdave97
reviewed
Aug 15, 2026
|
|
||
| if !is_merkle_branch_valid { | ||
| Err(Error::InvalidMerkleBranch("Execution payload branch".into()))?; | ||
| if header.state_root.0 != execution_payload.state_root.0 || |
Member
There was a problem hiding this comment.
This check is not necessary here, if we verify the header hash successfully we don't need this check
Member
|
@dharjeezy track activation on sepolia here: https://forkcast.org/upgrade/glamsterdam/ |
…hthouse ssz stack
…/glamsterdam-upgrade # Conflicts: # Cargo.lock # Cargo.toml # modules/utils/bls-utils/src/ssz/byte_vector.rs # modules/utils/serde/src/lib.rs # tesseract/consensus/bsc/Cargo.toml # tesseract/consensus/bsc/src/host.rs
Member
Blocking
Design gaps for the real forkLet's investigate if we can do this so we can avoid feature flags
|
…f in the ssz forks
This reverts commit 31a40f6.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prepares the Ethereum sync committee client for Glamsterdam (CL name gloas, EIP-7732 ePBS), moves the SSZ layer from
ssz-rsto Lighthouse's stack, and picks the fork at runtime rather than at build time.Closes #1011.
The consensus problem
ePBS removes
BeaconState.latest_execution_payload_header, the field the verifier proves today forstate_root,block_numberandtimestamp. It is replaced bylatest_block_hash, and the realExecutionPayloadmoves to a separately gossiped envelope the sync committee never signs. After Gloas there is no SSZ path from the signed header to the execution state root.The fix is to prove
latest_block_hash, have the relayer supply the EL block header, and checkkeccak256(rlp(header)) == block_hashbefore decoding the fields out of it. Same trust assumptions as today. Verifying the payload envelope through the PTC was rejected: the PTC attests to payload timeliness, not the state root, so it needs the same block-hash-to-state-root binding anyway while adding a second committee path.The post-Glamsterdam EL header has 23 RLP fields, not 21: it appends
block_access_list_hash(EIP-7928) andslot_number(ePBS). The 21 and 22 field encodings both produce the wrong hash.No feature flag: the fork is data
Gloas does not extend the pre-Gloas state, it reshapes it. The payload header becomes a block hash, eight builder and payload-timeliness fields are appended, twelve lists become
ProgressiveList, and the container merkleizes progressively. Those are two different SSZ types and no single Rust struct is both.They were briefly one struct behind a
glamsterdamfeature, which meant the fork was chosen when the binary was built: a relayer had to be rebuilt at the fork, and a binary built for one side could not read a state from the other. The beacon api already reports which shape it is sending, soBeaconStateElectraandBeaconStateGloasare now separate types behind an enum, selected from theversionfield:Blocks needed no such split. Only four of their fields are ever read here and none are merkleized, so one lean
BeaconBlockSummarydeserializes either fork.The verifier and ISMP client were already fork-agnostic: they dispatch on the epoch of the signed slot, so an update cannot select its own verifier, and a variant that disagrees with the fork is a hard
InvalidUpdate.Generalized indices
Gloas adopted progressive SSZ merkleization (EIP-7688, EIP-7916), so every index moved:
finalized_checkpointnext_sync_committeeBranch depths are no longer uniform.
is_valid_merkle_branchnow takes the generalized index alone and derives the depth from it, so the per field*_INDEX_LOG2constants are gone.Rootkeeps its wire formatssz-rs'sNodeSCALE-encodes as 32 raw bytes. RoutingRootthroughByteVector<U32>would have encoded it as aVec<u8>, adding a compact length prefix: 33 bytes leading0x80.VerifierState.finalized_headercarries three of them, so every stored Ethereum and GnosisConsensusStatewould have failed to decode after a runtime upgrade, and relayers still on the old format would have failed withDecodeBeaconClientUpdate.Rootis therefore a newtype whose SCALE impl writes the 32 bytes raw, with tests pinning that standalone and nested inside a struct, which is how it actually arrives in a consensus update. It isCopyagain too, asNodewas.The fork dependencies
ssz-rshas no progressive support, so rather than maintain our own the client moved to the crates Sigma Prime maintains for Lighthouse. That stack had gaps, filled in five forks underpolytope-labs, all now merged:ContainerFields/TreeHashFieldssplit so a progressive container cannot reach the balanced multiproof builder.ProgressiveList<T>, which exists nowhere upstream, plus no_std.DefaultforBitList, and derives that emitcorepaths so no_std consumers can use the generated code.SCALE lives here, not in the forks
The forks originally carried the SCALE impls for the ssz containers and bitfields. That is the one change upstream will never take, since it puts a Substrate dependency inside an SSZ library, and it meant the forks could never be retired: every upstream security patch had to be merged by hand rather than rebased.
The codec now sits on types this repo owns, so the ssz crates need no codec at all.
Rootprimitives/src/ssz/root.rsByteVector<N>bls-utilsVec<u8>SyncCommittee<N>primitives/src/scale.rsVec, then the aggregateSyncAggregate<N>primitives/src/scale.rsVec<bool>, then the signatureThose are the only types reachable from the runtime boundary that hold an ssz container. That was established by walking the type graph out from
VerifierState,VerifierStateUpdateandConsensusState, not by inspection, and the 26 codec derives that turned out to be unreachable are gone.The encodings are byte for byte what the derives produced, which in turn matched
ssz-rs. A bitfield still encodes asVec<bool>, one byte per bit, which is wasteful but is what previously encoded updates contain. Decoding re-applies each container's length rule, so a wrongly sized committee or an over long bitfield is rejected at the boundary rather than surviving as an invalid value. That check matters most forByteVector: an over length value merkleizes to the same root, so a consumer authenticating only byhash_tree_rootwould not notice.No crate in the workspace enables a
scalefeature any more, andparity-scale-codecno longer appears in the resolved dependency graph of either ssz crate.The pins here point at the fork mains, which carry everything this client needs: no_std,
ProgressiveList, the proof generation and the container changes.Pinning the upstream review branches instead was tried and reverted. Two reasons. They move whenever a maintainer asks for a change, and more decisively, ssz_types#85 was cut down to no_std only at the maintainer's request because #84 already covers EIP-7916, so that branch no longer contains
ProgressiveListat all. The upstream branches serve the upstream PRs; the forks serve this client until the crates are released.While the fork mains still carry the scale code, it is inert: nothing enables the feature, so none of it compiles. ethereum_ssz#2 and ssz_types#2 remove it, which also drops
parity-scale-codecfrom the resolved dependency graph.Migration notes for reviewers
Most of the diff is one mechanical change with awkward consequences:
ssz_typestakes its bounds as type level integers wheressz-rsusedconst N: usize.pub typealiases. Rust keeps types and values in separate namespaces, soVariableList<T, MAX_DEPOSITS>picks up the type andvec![0; MAX_DEPOSITS]the constant, and no use site had to change.Send + Sync + 'static: they now flow through aPhantomDatainto an async host.hash_tree_root()returned aResult;tree_hash_root()is infallible.Validation
Both forks, live chains, from the same binary:
beacon_state_hashes_to_the_signed_headeris the one that matters: it merkleizes the full state and compares against the root the node signed, so it validates our merkleization against an independent implementation rather than against itself. It passes on both devnets from one build, merkleizing an Electra state as a balanced container and a Gloas state as a progressive one, which is the runtime dispatch demonstrated rather than asserted.Workspace, on this branch:
cargo test --all-targets --lockedcargo check --all --benches --lockedgargantua-runtimeandnexus-runtime,wasm32-unknown-unknown, no_stdETH0consensus state decodes and re-encodes byte identicallyThe stored state test is what guards the wire format. It is 50,412 bytes taken straight from
Ismp::ConsensusStateson live Gargantua, so it exercisesRoot,ByteVectorandSyncCommitteeagainst bytes a running chain actually wrote, and it asserts the re-encode matches so a later write cannot corrupt storage either.SyncAggregateis not present in stored state, so it is covered instead by the gloas verifier tests above and by unit tests pinning theVec<bool>encoding.The forks pass their own suites (tree_hash 114, ethereum_ssz 123, ssz_types 73, ethereum_hashing 7, ethereum_serde_utils 40) and build for
wasm32-unknown-unknownwithno_std, verified by a probe crate that declares its own#[panic_handler]and derives the ssz traits, since checking the libraries alone misses both linked-std and derive-emitted-std::failures.Not covered: the
--ignoredintegration suites that need their own devnets or RPC secrets (bsc-prover, tendermint-prover, simtests, pharos). BSC's unit tests do run as part of the workspace suite above.test_switch_provider_middlewarefails locally because it hardcodeslocalhost:53001while our devnet runs elsewhere.Merge order
This does not block on anything.
parity-scale-codecleaves the dependency graph entirely and the forks rebase cleanly onto upstream releases.ProgressiveListhere will follow whatever that lands, including its name. As each crate is released, the matching pin moves from a git revision to a crates.io version and that fork retires.