You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix: serve genesis block on BlocksByRoot with structurally-valid blank XMSS signature (lambdaclass#371)
## Summary
Two related fixes that let peers (and our HTTP API) receive the genesis
block in response to BlocksByRoot / \`/lean/v0/blocks/finalized\`.
### 1. \`fix(storage): synthesize empty BlockSignatures for genesis on
read\`
\`store.get_signed_block(root)\` previously short-circuited to \`None\`
when the \`BlockSignatures\` row was missing. That row is intentionally
absent for genesis-style anchor blocks (no proposer ever signed them, no
attestations exist) — but it also meant we silently dropped the genesis
chunk from BlocksByRoot responses. The lean spec response container is
\`List[SignedBlock]\` and peers (notably the ethereum/hive lean
simulator's \`blocks_by_root/multiple_known_blocks\` test) expect one
chunk per requested root.
Synthesize an empty \`BlockSignatures\` when the header exists but no
signature row was written, so the fork-choice view and BlocksByRoot
agree on what the node will serve. Same shape ream pre-materializes at
write time (\`bin/ream/src/main.rs:301-306\`); we synthesize on read
because our split storage layout (\`BlockHeaders\` / \`BlockBodies\` /
\`BlockSignatures\`) would otherwise pin a 2536-byte zero blob per
anchor.
### 2. \`fix(types): use SSZ-structurally-valid blank XMSS signature\`
The placeholder \`proposer_signature\` in (1) — and a handful of test /
RPC stubs scattered around the workspace — was built as 2536 raw zero
bytes. That works on the wire because parent containers treat
\`XmssSignature\` as an opaque fixed-size blob (leanSpec
\`xmss/containers.py::Signature.is_fixed_size\` returns \`True\`), but
the bytes are not a valid SSZ encoding of the inner \`Signature\`
container: all offsets are zero. Any consumer that round-trips the
placeholder through the inner-container decoder (e.g. leanSpec
API-output validators) would reject it.
Match ream's
[\`Signature::blank()\`](https://github.com/ReamLabs/ream/blob/master/crates/crypto/post_quantum/src/leansig/signature.rs#L36-L48)
(post devnet4 alignment): write the three SSZ offsets at fixed positions
(36, 1064, 4) so the same 2536-byte blob decodes back to:
\`\`\`
Signature {
path: HashTreeOpening { siblings = [0; 32] },
rho: 0,
hashes: [0; 46],
}
\`\`\`
Centralize the construction in a single
\`attestation::blank_xmss_signature()\` helper and point every existing
call site at it (storage helper, blockchain/p2p/rpc test stubs,
fork-choice spec-test fixture builder). Also:
- fix the stale \`(3112 bytes)\` doc on \`XmssSignature\` (we're 2536 —
DIM=46);
- update the HTTP test that asserted 404-on-genesis
(\`get_latest_finalized_block\`) to assert 200 instead, so it stays
consistent with the BlocksByRoot path.
## Why bundled
(2) depends on (1) — the \`empty_block_signatures()\` helper it touches
is introduced by (1). Keeping them in one PR avoids a half-step where
genesis is served on the wire with bytes that no one can decode back to
a \`Signature\` container.
## Test plan
- [x] \`make fmt\`
- [x] \`make lint\` (\`-D warnings\`)
- [x] \`cargo test --workspace\` — all suites green locally
- [x] hive lean \`reqresp\` suite — 22/22 pass against an image built
from this branch (verified previously when only (1) was in place; (2) is
a wire-format refinement of the same placeholder)
## Cross-client context
ream pre-materializes the same placeholder at write time when
constructing the anchor SignedBlock (\`bin/ream/src/main.rs:283-310\`
calls \`Signature::blank()\`). Lighthouse does the equivalent for the
beacon chain — see
[\`beacon_node/beacon_chain/src/builder.rs::genesis_block\`](https://github.com/sigp/lighthouse/blob/stable/beacon_node/beacon_chain/src/builder.rs)
which wraps the genesis block in
\`SignedBeaconBlock::from_block(genesis_block, Signature::empty())\`
with the inline comment *"Empty signature, which should NEVER be read.
This isn't to-spec, but makes the genesis block consistent with every
other block."* The Ethereum consensus-specs are silent on what shape
this placeholder takes (\`specs/phase0/p2p-interface.md\`
\`BeaconBlocksByRoot\` allows peers to silently skip out-of-range
roots), so this is by-convention across clients rather than a spec
requirement.
## Risk
- Wire format unchanged for already-signed blocks; only the placeholder
bytes change.
- Genesis is now served via BlocksByRoot and
\`/lean/v0/blocks/finalized\` instead of being silently dropped / 404'd.
Any consumer that *required* the 404 to detect "node hasn't progressed
past genesis" needs to switch to checking \`latest_finalized.slot ==
0\`. I'm not aware of one in this codebase.
---------
Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
0 commit comments