simulators/lean: correctly decode devnet5 blocks - #1542
Merged
KolbyML merged 2 commits intoJun 15, 2026
Conversation
MegaRedHand
force-pushed
the
lean-sim-merged-block-proof
branch
from
June 12, 2026 18:35
20ef450 to
1b34979
Compare
Devnet5 replaced the per-attestation signature list on the signed-block
envelope with a single merged multi-message aggregate proof, matching
leanSpec's SignedBlock { block, proof: MultiMessageAggregate }. The
simulator still decoded every signed block with the devnet4 shape
SignedBlock { block, signature: BlockSignatures }, so reqresp
blocks_by_root tests failed against devnet5 clients with
OffsetIntoFixedPortion(4): the merged-proof container opens with offset
4, which lands inside the devnet4 signatures container's fixed portion.
The rpc-compat finalized-block decoder had the same mismatch.
Keep both envelope shapes as wire-only types and select between them
from the devnet under test, mirroring how the state decoder already
splits devnet formats. Scenario code now works with the inner block
only: decoding validates the envelope structurally, then returns the
block, since no test inspects signature or proof contents.
MegaRedHand
force-pushed
the
lean-sim-merged-block-proof
branch
from
June 12, 2026 18:42
1b34979 to
1770660
Compare
MegaRedHand
marked this pull request as ready for review
June 12, 2026 18:43
MegaRedHand
force-pushed
the
lean-sim-merged-block-proof
branch
from
June 12, 2026 19:22
2f0209a to
6480eb4
Compare
…e helper leanSpec moved its packages from subspecs/types to node/spec (leanSpec PR ethereum#788 and follow-ups), so a devnet5 helper built from leanSpec main crashes on startup with ModuleNotFoundError and every helper-dependent devnet5 test fails at setup. The devnet4 helper stays pinned on the old layout, so the runner now tries the new import locations first and falls back to the old ones, both for static imports and the dynamically imported modules used by the compatibility patches. The trusted gossip attestation override additionally follows two API renames on the new layout: the voter field (validator_id -> validator_index) and the registry-bounds predicate (is_valid -> is_within_registry).
MegaRedHand
force-pushed
the
lean-sim-merged-block-proof
branch
from
June 12, 2026 19:48
6480eb4 to
4ab4cfd
Compare
3 tasks
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.
Problem
The lean simulator decodes every
SignedBlockwith the devnet4 wire shape:Devnet5 (leanSpec main, since leanEthereum/leanSpec#717) replaced the per-attestation signature list with a single merged multi-message aggregate proof:
Against a devnet5 client the
reqresp/blocks_by_root/*tests therefore fail with:The fingerprint is exact: the merged-proof container opens with SSZ offset
4(its only field is variable), which lands inside the devnet4 signatures container's fixed portion (4-byte offset + 2536-byte proposer signature). Observed againstethlambda_devnet5in this run (reqresp/blocks_by_root/multiple_known_blocks). The rpc-compat finalized-block decoder (/lean/v0/blocks/finalized) has the same mismatch.Note the simulator was internally inconsistent: its own devnet5 helper builds leanSpec from
main, which already produces the merged-proof shape the mock types could not decode.Fix
Keep both envelope shapes as wire-only types and select between them from the devnet under test, mirroring the existing devnet4/devnet5 split for states (
LeanStateDevnet4):LeanSignedBlockis the merged-proof envelope; the devnet4 envelope moves toLeanSignedBlockDevnet4LeanBlockonly.LeanBlock::from_signed_wire_ssz_bytesdecodes the devnet-appropriate envelope (validating the signature payload structurally) and returns the block; no placeholder signature or proof values are stored, since no test inspects themLeanBlock::to_signed_wire_ssz_bytes/encode_gossip_blockwrap a block in the devnet-appropriate envelope for gossip publishes (encode_gossip_datawas only ever used for blocks, so it becameencode_gossip_block)LeanRpcSignedBlockDevnet4/LeanRpcSignedBlock) anddecode_finalized_blockreturns the innerLeanRpcBlockWorking with the inner block is sound because the decode and encode paths never meet. Decode sites (reqresp, rpc-compat) only assert on block fields and never re-send what they received. Encode sites (gossip, validation) publish synthetic blocks built from scratch: the mock has no validator keys, so it has always published default (unverifiable) signatures, and the empty envelope produces the same bytes as before. There is no decode-then-re-encode path that would lose signature data; if a future test needs to relay a received signed block verbatim, both envelope types stay
pubfor decoding in full (or the raw payload bytes can be forwarded unchanged).Devnet4 runs are unaffected: they keep encoding and decoding the old shape, bit-identical to before.
Testing
cargo testinsimulators/leanpasses (client_interop::two_subnet_node_builder_accepts_all_devnet4_lean_clientsfails identically on master in my environment)cargo clippyintroduces no new warnings;cargo fmtcleanRelated: leanEthereum/leanSpec#974 restores the
/lean/v0/blocks/finalizedhelper endpoint this suite's checkpoint-sync setup probes; the two fixes are independent.Second commit: helper crashes against leanSpec main
While verifying the fix locally, a second devnet5 breakage surfaced: the lean-spec helper is built from leanSpec
main, but leanSpec restructured its packages (subspecs/types→node/spec, leanEthereum/leanSpec#788 and follow-ups). A freshly built devnet5 helper now crashes on startup withModuleNotFoundError: No module named 'lean_spec.subspecs', taking down every helper-dependent devnet5 test at setup. (Production results on hive.leanroadmap.org still show the helper running, which means the deployed sim image carries a pre-restructure leanSpec snapshot; a genuine rebuild would hit this.)The second commit makes the helper runner work with both layouts: new import locations are tried first, the old ones remain as fallbacks for the pinned devnet4 helper. Verified by importing the runner in venvs of both leanSpec
main(1b02fd1) and the devnet4 pin (db21cc24).Verification
Ran
hive --sim lean --sim.limit 'reqresp/blocks_by_root/multiple_known_blocks$'locally against an ethlambda devnet5 client with the helper built from leanSpecmain(1b02fd1): the previously failing test passes. The devnet4 helper path is covered by import smoke tests against the pinned checkout (db21cc24).One caveat surfaced during verification: the currently published
ghcr.io/lambdaclass/ethlambda:devnet5image (built from ethlambda'sdevnet5-bump-leanmultisigbranch) serves the genesis anchor block with a zero-byte proof, which is invalid SSZ underMultiMessageAggregate(its offset alone needs 4 bytes). The simulator now correctly rejects that withInvalidLengthPrefix { len: 0, expected: 4 }. ethlambdamainalready encodes the genesis proof correctly (verified passing with an image built from ethlambda main), so this resolves itself when the published devnet5 image is rebuilt.Not addressed here: the simulator Dockerfile's metadata
ADDlines hit GitHub's unauthenticated API rate limit on local builds; worked around locally, left untouched since the hosted builder presumably authenticates.