Skip to content

feat(crypto)!: track leanVM main (internalized XMSS + reworked aggregation) - #539

Open
MegaRedHand wants to merge 18 commits into
mainfrom
build/leanvm-track-main
Open

feat(crypto)!: track leanVM main (internalized XMSS + reworked aggregation)#539
MegaRedHand wants to merge 18 commits into
mainfrom
build/leanvm-track-main

Conversation

@MegaRedHand

@MegaRedHand MegaRedHand commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

What

Moves the leanVM dependency off the devnet4-era lean-multisig + leansig_wrapper crates and onto current main, pinned at a5909d1.

leanVM's main removed the crates ethlambda imported, internalized XMSS in its own xmss crate (dropping the external leanSig dependency), and reworked the recursive-aggregation API. So this is a migration of the whole signature stack, not a version bump: cargo update fails outright (no matching package named leansig_wrapper).

It also takes the prover off leanVM's bump arena by default, which is what stops a node's RSS from ratcheting upward for its whole lifetime.

Changes

Area Before (rev e2592df) After (rev a5909d1)
Deps lean-multisig, leansig_wrapper, leansig (devnet4) xmss + lean-multisig (leanVM's facade), ssz (ethereum_ssz), postcard
Key/sig types leanSig devnet4 leanVM's own xmss crate
Proof wire form compress_without_pubkeys() to_bytes_without_pubkeys()unchanged shape: pubkeys stay off the wire
Pubkey size 52 bytes 32 bytes (PUB_KEY_SSZ_LEN)
Signature size 2536 bytes 1208 bytes (SIGNATURE_SSZ_LEN)
Prover allocator leanVM arena system allocator, arena behind --prover-arena
Backend init lazy, on first proof explicit init_leanvm(use_arena) at startup
Secret-key window sliding two-tree preparation window fixed activation range (advance_preparation is now a no-op)
Proof verification pool-dispatched single-threaded
  • types::signature: rebuilt ValidatorPublicKey/ValidatorSignature/ValidatorSecretKey on xmss. Pubkeys/sigs (de)serialize via SSZ; secret keys via postcard. SIGNATURE_SIZE + PUBLIC_KEY_SIZE are static-asserted against the scheme constants, so a leanVM bump that changes them breaks the build instead of silently producing wrong-length keys.
  • crypto: rewired aggregation/verification against leanVM's facade. The wire format and every public signature are kept as on main: proofs serialize without their participant pubkeys, and the caller attaches the resolved signer set at decode (from_bytes_without_pubkeys). A set other than the one aggregated attaches fine but fails verification, since the proof binds a hash of the set — covered by a new test_verify_wrong_pubkey_set_fails.
  • Validator pubkeys 52 → 32 bytes: genesis/state layouts, deser messages, and test fixtures updated; pinned genesis state/block roots recomputed.
  • VerificationError::ProofError(#[from] ProofError) becomes VerificationFailed(String): leanVM's ProofError lives in its backend crate, which the facade imports but does not re-export, so it is not nameable here.

Memory: off the arena by default

leanVM's setup_prover engages a bump arena that never returns pages to the OS — free is a no-op, and a phase reset only rewinds the per-thread bump pointers (zk-alloc: no MADV_DONTNEED, no munmap, and on Linux enable_arena also disables heap trimming and large-allocation mmap process-wide). RSS therefore climbs to the process's allocation high-water mark and stays there, which is the unbounded growth seen on long-lived devnet nodes.

a5909d1 adds setup_prover_without_arena, which runs the same prover on the system allocator: slower, bounded, identical proofs. That is now the default, with --prover-arena to opt back in on hosts with memory to spare where proving latency is the constraint.

Prover setup and the proving permit

Setup used to be lazy and reached through the permit helper, which coupled two unrelated things: taking the prove lock also triggered backend init, so the decoding paths had to hold the lock across work that needs no lock, purely to get the aggregation bytecode compiled first.

  • init_leanvm(use_arena) does both halves in one place; the binary calls it straight after parsing arguments, ahead of the Hive test-driver branch (which verifies signatures).
  • acquire_prover() is just the permit again, so every entry point takes it immediately before the prove call rather than across argument conversion and proof decoding.
  • ensure_verifier_ready is gone — nothing needs a lazy hook anymore.

This moves ~2.3s of bytecode compilation (measured, 3 runs, both allocators) from the first proof or verification into startup. Nodes paid it either way; it is now off the first duty's critical path, at the cost of a slower boot for nodes that never prove.

⚠️ Breaking / follow-ups

The wire format of the proof contents and the genesis key format change, even though the pubkey-less framing matches main:

  • Interop-incompatible with clients still on the previous scheme.
  • Requires regenerated genesis keys (32-byte xmss pubkeys, postcard secret keys).
  • Fixture-driven spec tests need regenerated fixtures; the currently released ones are the old format and fail while deserializing, before reaching any consensus or crypto code:
Harness Result Error
forkchoice_spectests 122 failed ValidatorPubkey length != 32
stf_spectests 73 failed, 1 passed same
ssz_spectests 7 failed, 112 passed same, plus expected 1208, got 2536
signature_spectests 3 failed same

Everything else in the workspace passes, including all 11 ethlambda-crypto tests (the slow ignored ones plus a new integration test covering the arena path).

Landing this needs ecosystem coordination; it is not a drop-in for the live devnet.

…ation)

Point the leanVM dependency at `main`. leanVM's `main` ("Xmss api rework")
removed the `lean-multisig` and `leansig_wrapper` crates ethlambda imported,
internalized XMSS in its own `xmss` crate (dropping the external leanSig
dependency), and reworked the recursive-aggregation API. Tracking main is
therefore a migration of the whole signature stack, not a version bump.

Changes:
- Cargo: replace `leansig`/`lean-multisig`/`leansig_wrapper` with `xmss` +
  `rec_aggregation` (git, branch=main), plus `ssz` (ethereum_ssz, used by the
  xmss SSZ impls) and `postcard` (secret-key format).
- types::signature: rebuild ValidatorPublicKey/Signature/SecretKey on leanVM's
  `xmss` crate. Keys/signatures (de)serialize via SSZ (`PUB_KEY_SSZ_LEN` /
  `SIGNATURE_SSZ_LEN`); secret keys via postcard. `SIGNATURE_SIZE` and the new
  `PUBLIC_KEY_SIZE` are sourced from the xmss scheme constants. The sliding
  preparation-window API becomes a fixed activation range (`advance_preparation`
  is now a no-op; keys warm their signing cache on demand).
- crypto: rewrite aggregation/verification against `rec_aggregation`. Proofs now
  serialize with `to_bytes()`/`from_bytes()` and embed participant pubkeys, so
  the verify paths cross-check the embedded set against the expected validators
  instead of attaching them at decode time. `setup_prover`/`setup_verifier` are
  replaced by the idempotent `init_aggregation_bytecode`.
- Validator public keys are now 32 bytes (was 52); genesis/state layouts and
  fixtures updated accordingly.

BREAKING: this changes the on-wire signature/proof formats and the genesis key
format. It is interop-incompatible with clients still on the previous scheme and
requires regenerated genesis keys; the fixture-driven spec tests need
regenerated fixtures. Landing this needs ecosystem coordination.
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

Security & Correctness

  1. Dependency Pinning (Critical)
    Cargo.toml tracks leanVM dependencies by branch = "main" rather than a specific rev. While Cargo.lock currently pins commit a73ab114, this is fragile—accidental lockfile updates could pull in untested VM changes.
    Suggestion: Pin to a specific git revision in Cargo.toml for reproducibility:

    xmss = { git = "https://github.com/leanEthereum/leanVM.git", rev = "a73ab114..." }
  2. Canonical Pubkey Ordering
    crates/common/crypto/src/lib.rs:113-121 (sorted_dedup_pubkeys) sorts and deduplicates pubkeys before comparison. This assumes leanVM’s aggregation prover applies the exact same canonicalization when embedding info.pubkeys in the proof.
    Action: Verify that leanVM’s rec_aggregation crate sorts pubkeys with the same Ord implementation (standard lexicographic on the underlying bytes). A mismatch here would cause valid proofs to fail verification.

  3. Overflow in Slot Range
    crates/common/types/src/signature.rs:157

    (*range.start() as u64)..(*range.end() as u64 + 1)

    If range.end() is u32::MAX, the addition overflows (panic in debug, wrap in release). While Ethereum slots won’t reach this in practice, consider using checked_add or RangeInclusive to be defensive.

  4. Zero-Signature Safety
    crates/common/types/src/attestation.rs:66-68 defines blank_xmss_signature() as all-zero bytes. The comment states this decodes as “structurally valid (but unverifiable)”. Ensure leanVM’s XmssSignature SSZ deserialization rejects signatures with all-zero WOTS+ values during verify, otherwise a malicious genesis/config could inject a seemingly valid but trivially forgeable signature.
    File: crates/common/types/src/attestation.rs:66

Performance & Memory

  1. Allocation in to_bytes
    crates/common/types/src/signature.rs:53 and 61 allocate Vec<u8> for every serialization. For hot paths (e.g., signing thousands of attestations), consider exposing ssz::Encode directly to callers to allow stack-allocated buffers or zero-copy serialization where possible.

  2. Lazy Initialization
    The replacement of std::sync::Once with leanVM’s internal OnceLock (init_aggregation_bytecode) is idiomatic and safer. The #[ignore = "slow"] tests correctly gate the expensive bytecode compilation.

Code Quality & Maintainability

  1. Error Type Erasure
    crates/common/crypto/src/lib.rs:304 and 469 convert leanVM errors to strings:

    .map_err(|err| VerificationError::VerificationFailed(format!("{err:?}")))?;

    This loses structured error information. If leanVM exposes specific error enums (e.g., InvalidProof, SlotMismatch), map them explicitly to VerificationError variants for better diagnostics.

  2. Consistent Constant Usage
    Good: PUBLIC_KEY_SIZE is now sourced from xmss::PUB_KEY_SSZ_LEN (crates/common/types/src/signature.rs:28).
    Nit: Ensure SIGNATURE_SIZE is used everywhere instead of magic numbers (currently consistent across the diff).

  3. Doc Comment Accuracy
    crates/common/crypto/src/lib.rs:482-484 mentions “the caller supplies the expected message” but the function signature no longer takes pubkeys_per_component (removed in this PR). Update the doc to reflect that pubkeys are now embedded in the proof.

Testing

  1. Genesis State Root Update
    The hardcoded state root in crates/common/types/src/genesis.rs:152 changed due to the 52→32 byte pubkey resize. Confirm that this new root matches the output of an independent leanVM-based genesis generator to prevent consensus split on the devnet.

  2. Dummy Signature Robustness
    Tests in crates/storage/src/store.rs:2487 and crates/blockchain/src/aggregation.rs:794 now use all-zero signatures. Add a comment or assertion ensuring these are never used in verification paths (only deserialization), as they would fail actual XMSS verification.

Summary

The migration from the external leanSig crate to leanVM’s internalized xmss/rec_aggregation is well-executed. The shift to self-contained proofs (pubkeys embedded) is architecturally superior. Address the dependency pinning (Item 1) and verify the pubkey sorting canonicalization (Item 2) before merging.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Migrates the validator signature stack to leanVM’s internal XMSS and reworked recursive aggregation APIs.

  • Replaces legacy leanSig wrappers with XMSS SSZ public-key/signature encoding and postcard secret-key persistence.
  • Updates proof initialization, serialization, aggregation, verification, and concurrency handling.
  • Changes validator public keys and signatures to the new fixed wire sizes across state, genesis, storage, RPC, and fixtures.
  • Adds configurable leanVM arena initialization and updates associated tests and pinned roots.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure eligible for this follow-up review remains.

No blocking failure remains.

Important Files Changed

Filename Overview
crates/common/crypto/src/lib.rs Migrates aggregation and verification to the new leanVM proof APIs, adds explicit initialization, and serializes proofs without participant public keys.
crates/common/crypto/src/signature.rs Rebuilds validator key and signature wrappers around leanVM XMSS with SSZ wire encoding and postcard secret-key persistence.
crates/common/types/src/state.rs Changes validator public keys from 52 to 32 bytes and updates the consensus-state wire layout accordingly.
crates/common/types/src/attestation.rs Updates the fixed XMSS signature wire size and blank-signature representation for the internal XMSS scheme.
bin/ethlambda/src/main.rs Initializes leanVM before cryptographic paths and wires the configurable prover allocator into startup.
crates/common/types/src/genesis.rs Updates genesis public-key parsing, test vectors, and pinned state and block roots for the new key format.
crates/storage/src/state_diff.rs Updates storage reconstruction fixtures to use the new validator public-key width.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Startup[Node startup] --> Init[Initialize leanVM bytecode and allocator]
  Keys[Postcard XMSS secret keys] --> Sign[XMSS signing]
  State[Validator registry: 32-byte public keys] --> Resolve[Resolve participant public keys]
  Sign --> Type1[Create Type-1 aggregate]
  Resolve --> Type1
  Type1 --> Wire1[Public-key-free proof bytes]
  Wire1 --> Type2[Merge Type-1 proofs into Type-2]
  Type2 --> Block[Signed block]
  Block --> Decode[Attach resolved signer sets]
  State --> Decode
  Decode --> Verify[Verify bindings and recursive proof]
Loading

Reviews (2): Last reviewed commit: "Merge branch 'main' into build/leanvm-tr..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. read_validator_keys still treats pubkey_hex as a syntax check only, then discards it before loading the secret keys. With the new API you can derive the public key (ValidatorSecretKey::public_key()), so a swapped or wrong key file will now be accepted at startup and only show up later as invalid attestations/proposals. That is a consensus-liveness regression and an avoidable operator-footgun. See bin/ethlambda/src/main.rs, bin/ethlambda/src/main.rs, and bin/ethlambda/src/main.rs.

  2. The loader also never enforces the dual-key invariant after parsing the files. The protocol docs in crates/common/types/src/state.rs require distinct attestation and proposal XMSS keys to avoid OTS reuse in the same slot, but bin/ethlambda/src/main.rs accepts whatever two secret-key files are present. If both entries resolve to the same key material, a validator that both attests and proposes in one slot will reuse the same XMSS leaf across two different messages. This should be rejected at startup by comparing the derived pubkeys for both roles.

Aside from that, the signature/aggregation migration looks internally consistent: the proof-verification paths now bind embedded pubkeys back to the expected validator sets, and the reaggregation path still sits behind full block-proof verification.

I couldn’t run cargo test here because the pinned toolchain resolution tries to write under the read-only ~/.rustup in this environment.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

MegaRedHand and others added 5 commits July 24, 2026 13:09
…e branch

Tracking `branch = main` re-resolves on every fetch, so the build could
change under us. Pin the current main HEAD for reproducibility; the comment
notes it is main's tip and the rev can be bumped to move forward.
Moves the pin two commits forward from a73ab11 (`Xmss api rework`, #262):

- `105c5c69` rec_aggregation: optional pubkey-less aggregate serialization
- `c83b40f0` fully single-threaded verifier (`Vec` instead of `ArenaVec`),
  plus a `forbid-parallelism` feature that asserts it

`105c5c69` split `SingleMessageInfo` into a `SingleMessageCore` (message,
slot, bytecode claim) plus the pubkey set, so the binding checks in the
Type-1 and Type-2 verify paths reach through `.core`. The `xmss` crate is
byte-identical between the two revs, so key and pubkey formats are
unchanged: the genesis keys generated for a73ab11 verify as-is.

`c83b40f0` makes proof verification sequential rather than pool-dispatched.
The `forbid_parallelism()` guards it adds are inert unless leanVM's
`forbid-parallelism` feature is enabled, which we do not enable.

Validated on a 3-node all-ethlambda devnet with fresh new-format genesis:
real leanVM proving on every block, Type-2 aggregates verified from gossip
in ~50ms, finality advancing one slot per slot, no errors.
leanVM `105c5c69` restored optional pubkey-less aggregate serialization, so
the wire format no longer has to change relative to `main`.

Proofs serialize via `to_bytes_without_pubkeys()` and the caller attaches the
resolved signer set at decode with `from_bytes_without_pubkeys()`, exactly as
`compress_without_pubkeys()`/`decompress_without_pubkeys()` did before the
migration. Every public signature in the crypto crate now matches `main`
again, including `split_type_2_by_message`, which regains its
`pubkeys_per_component` argument (`reaggregate.rs` already resolves that
layout for its merge step, so the call site just passes it through).

Attaching the caller's set is what binds a proof to its participants: leanVM
sorts and de-duplicates it, then the SNARK checks it against the hash the
proof commits to. So the explicit `PublicKeySetMismatch` comparison the
embedded-pubkey form needed is gone, and a wrong set now fails inside the
verifier — covered by a new `test_verify_wrong_pubkey_set_fails`.

Validated on a 3-node all-ethlambda devnet: 114 block Type-2 aggregates
decoded and verified through the pubkey-less path (111 carrying attestation
components), p50 43.7ms, finality tracking head at 3 slots, no errors.
Reaggregation never triggered there, so `split_type_2_by_message` is covered
by the Type-2 merge/verify/split round-trip test instead.
`ensure_prover_ready` only called `init_aggregation_bytecode`, so the arena was
never engaged and every prover buffer used the system allocator. `setup_prover`
lives in the `lean-multisig` facade, not in `rec_aggregation`, so importing crate
by crate hid it.

Engaging the arena arms a panic: only one proof may run at a time. Take a permit
around each proving entry point; without it 5 of the 7 crypto tests panic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MegaRedHand
MegaRedHand marked this pull request as draft July 28, 2026 15:50
Resolves the overlap with #541, which moved the signature primitives out of
`ethlambda-types` and into `ethlambda-crypto` while this branch was rewriting
those same primitives for leanVM's internalized XMSS.

The two wire-size constants cannot follow `signature.rs` into
`ethlambda-crypto`: `types::attestation::XmssSignature` and
`types::state::ValidatorPubkeyBytes` are defined in terms of them, and
`ethlambda-crypto` already depends on `ethlambda-types`. #541 hit the same
constraint and hardcoded `SIGNATURE_SIZE` in `types`. Keep that placement, but
source both from leanVM's xmss crate so they track the scheme parameters:
main's literal 2536 is the old leanSig size and is wrong for leanVM's wire
format.

`ethlambda-types` therefore keeps a narrow `xmss` dependency for the two
constants only, and `ssz`/`postcard` move to `ethlambda-crypto` along with the
signing code that needed them.
`ethlambda-types` pulled in leanVM's `xmss` crate solely to source two
integers, dragging the whole signing backend into the dependency graph of a
crate that only describes wire formats.

Hardcode `SIGNATURE_SIZE` and `PUBLIC_KEY_SIZE` there instead, and pin them to
the scheme in `ethlambda-crypto` -- the one crate that sees both sides -- with
const assertions. A leanVM bump that changes the XMSS parameters now fails to
compile with a named error rather than silently resizing every signature and
pubkey on the wire.
The const assertions named the mismatched constants but not their values: a
const panic message must be a string literal, so it cannot interpolate the two
numbers a reader needs in order to fix the constant.

Express the check as an array-length mismatch instead. rustc then evaluates both
sides and prints them ("expected an array with a size of 2536, found one with a
size of 1208"), so the corrected value is right there in the error.

A named constant would read better than `const _` but trips `dead_code`, which
is denied in CI, so the intent lives in the comment -- including a warning not
to take rustc's suggestion to edit the array length, which would silence the
guard rather than fix the constant.
leanVM's `setup_prover` engages a bump arena whose slabs never go back
to the OS: `free` is a no-op and `begin_phase` only rewinds the per-thread
bump pointers, so a node's RSS ratchets up to its allocation high-water
mark and stays there whether or not it keeps proving.

The new rev adds `setup_prover_without_arena`, which initializes the same
prover on the system allocator. Proving is slower; memory is bounded and
proofs are identical, which is the trade a long-lived node wants.
Every prover entry point paired `ensure_prover_ready()` with
`prover_permit()`, and neither is useful without the other: setup is a
no-op after the first call, and proving without the permit is exactly the
concurrent-proof case leanVM panics on. Fold them into `acquire_prover()`,
which returns the guard, so a caller cannot reach the prover half-armed.

Setup now runs under the permit, which serializes the one-time bytecode
compile instead of racing every waiting caller into the same `OnceLock`.

The idempotency test drops each guard before taking the next: the permit is
not reentrant, so holding two at once in one thread would deadlock.
The permit was taken at the top of each entry point, so a caller sat on the
process-wide prover lock while it converted pubkeys, zipped signatures and
postcard-decoded child proofs: work that touches no prover state and that
grows with the number of aggregated signatures. Take it immediately before
the aggregate/merge/split call instead.

Decoding does need one thing setup provided: a Type-2 decode rebuilds the
bytecode claim, and without the aggregation bytecode it returns `None`,
which surfaces as a bogus `DeserializationFailed`. The decoding paths now
call `ensure_verifier_ready()`, its documented prerequisite, which is a
`OnceLock` init rather than a lock.
The previous commit had the decoding entry points call
`ensure_verifier_ready()` and then `acquire_prover()`, which is the
two-call pattern the merge was meant to remove: it splits one setup across
two names and invites a caller to assume decoding is verifier-only work.
Since those paths need setup before they can decode, they take the permit
from the start and let it cover the decode.

`aggregate_signatures` keeps the narrow scope: it only reshapes its inputs
and never decodes, so nothing there needs setup.
The system allocator is the right default (bounded RSS), but it costs proving
throughput, and a host with memory to spare should be able to buy that back.
A `OnceLock<bool>` carries the choice: `enable_prover_arena()` sets it, and the
first `acquire_prover()` latches it via `get_or_init(|| false)`, so the
allocator cannot change under a prover that already ran.

The setter reports whether it won the latch instead of failing silently, since
being called too late is the likely mistake and it is invisible otherwise.

Covered by an integration test rather than a lib test: the latch is
process-wide, so enabling the arena in the lib binary would change the
allocator under every other test and make the result ordering-dependent.
Gives operators the memory/throughput trade at boot: off keeps RSS bounded on
the system allocator, on buys proving speed with memory that is never returned
to the OS.

Applied immediately after parsing, ahead of every other init, since the
allocator choice latches on the first proof. The too-late branch warns rather
than passing silently; it should be unreachable from here, but a future init
that proves earlier would otherwise flip the node to the wrong allocator with
no trace.
Setup was reached through `acquire_prover`, which coupled two unrelated
things: a caller wanting the prove lock also triggered backend init, so the
decoding paths had to hold the lock across work that needs no lock purely to
get the bytecode compiled first.

`init_leanvm(use_arena)` now does both halves in one place and binaries call
it right after parsing arguments. `acquire_prover` is just the permit again,
so every entry point takes it immediately before proving, and
`ensure_verifier_ready` is gone: nothing needs a lazy hook anymore. The arena
`OnceLock` goes too, since the choice is a parameter rather than latched
global state.

Ahead of the Hive test-driver branch in main, which verifies signatures.
Tests that touch a proof call `init_leanvm` themselves, standing in for the
startup call.

This moves ~2.3s of bytecode compilation from the first proof or verification
into startup. Nodes paid it either way; it is now off the first duty's
critical path, at the cost of a slower boot for nodes that never prove.
@MegaRedHand
MegaRedHand marked this pull request as ready for review July 30, 2026 19:12
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

This PR migrates the XMSS signature scheme from the standalone leansig crate to the integrated xmss module within leanVM, reducing public keys from 52 to 32 bytes and signatures from 2536 to 1208 bytes. It introduces process-wide initialization requirements and prover concurrency controls.

Critical: Initialization Order & Concurrency

  1. crates/common/crypto/src/lib.rs:72-86: The new init_leanvm(use_arena: bool) function must be called before any proving/verification. It is idempotent (backed by OnceLock), but there is no runtime assertion in downstream functions to catch missed initialization. Consider adding a debug_assert! in aggregate_signatures and verify_aggregated_signature that checks a static atomic flag set by init_leanvm to fail fast during development.

  2. crates/common/crypto/src/lib.rs:88-101: The acquire_prover() mutex correctly enforces leanVM's single-proof-per-process limitation. The poison recovery logic (unwrap_or_else) is appropriate for consensus-critical code to prevent a panic from bricking the node, but ensure the error! log is hooked into metrics/alerts. Item 1 from the safety guidelines is satisfied, but document the expected latency impact of holding this lock during aggregation.

Consensus & SSZ Correctness

  1. crates/common/types/src/state.rs:94 & crates/common/types/src/attestation.rs:63: The size constants PUBLIC_KEY_SIZE (32) and SIGNATURE_SIZE (1208) are hardcoded here and statically asserted in crates/common/crypto/src/signature.rs:25-26 using the const _: [(); N] pattern. This is excellent defensive programming—it ensures a leanVM upgrade changing scheme parameters breaks the build rather than corrupting the SSZ wire format.

  2. crates/common/crypto/src/signature.rs:122-126: The ValidatorSecretKey::from_bytes now uses postcard for deserialization, while the wire types use SSZ. Ensure the genesis key generation tooling outputs postcard-encoded secret keys, or this will fail at startup.

  3. crates/common/types/src/genesis.rs: The state root and block root hashes in tests have been updated to reflect the new validator pubkey sizes (32 bytes vs 52). This is expected, but verify that these new roots match the specification's test vectors if available.

API Compatibility & Code Quality

  1. crates/common/crypto/src/signature.rs:138-140: advance_preparation is now a no-op. Retain it for API compatibility as noted, but mark it #[deprecated = "leanVM uses fixed activation ranges; preparation is automatic"] to signal to callers that it has no effect.

  2. crates/common/crypto/src/lib.rs:118-119: The error variant ProofError was replaced with VerificationFailed(String). Mapping via format!("{err:?}") loses structured error information. If lean_multisig returns a structured error, preserve it rather than stringifying, to allow downstream matching on specific failure modes (e.g., InvalidProof vs InvalidPublicKeySet).

Testing

  1. crates/common/crypto/tests/arena.rs: Good isolation of the arena allocator test in a separate integration test binary to prevent allocator pollution. The #[ignore = "too slow"] attribute is appropriate for XMSS operations.

  2. crates/blockchain/tests/signature_spectests.rs:22-23: The test now calls init_leanvm(false). Ensure this is also added to any other test files that touch signature verification (e.g., crates/blockchain/src/aggregation.rs tests), as leanVM proving will panic without initialization.

Minor

  1. bin/ethlambda/src/main.rs:93: The init_leanvm call is correctly placed after argument parsing but before network initialization. Consider moving it even earlier (before metrics init) if any metric initialization could theoretically trigger crypto operations, though the current placement appears safe.

Summary: The PR correctly handles the cryptographic migration with compile-time size checks and proper concurrency controls. Address Item 1 (debug assertions for initialization) and Item 7 (structured error preservation) before merging. The consensus-layer changes (key/signature sizes) are breaking but internally consistent.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. crates/common/types/src/block.rs:54 and crates/common/types/src/block.rs:123 now document SingleMessageAggregate / MultiMessageAggregate as storing leanVM to_bytes() data with embedded pubkeys, but every actual producer/consumer still uses the pubkey-free wire form: crates/common/crypto/src/lib.rs:164, crates/common/crypto/src/lib.rs:169, crates/common/crypto/src/lib.rs:175, crates/common/crypto/src/lib.rs:376, crates/common/crypto/src/lib.rs:469, crates/common/crypto/src/lib.rs:524. That leaves the consensus wire contract self-contradictory. If another client/tooling path follows the new type docs, it will serialize the wrong bytes and fail interop or verification. The docs or the encode/decode path need to be made consistent.

  2. crates/common/types/src/attestation.rs:303 claims the blank placeholder “decodes back as a structurally valid signature”, but the new test at crates/common/types/src/attestation.rs:306 only checks length and all-zero contents. The previous regression coverage for inner-structure validity is gone. Since blank_xmss_signature() is relied on as a genesis/anchor placeholder, add an explicit decode assertion through the real parser so a future leanVM parameter change cannot silently make those placeholders undecodable.

Open question: crates/common/crypto/src/signature.rs:102 plus bin/ethlambda/src/main.rs:601 switch validator secret keys to postcard-encoded leanVM keys. I did not find key-generation code in this repo, so this needs rollout confirmation with the external genesis/keygen pipeline; otherwise existing hash-sig-keys assets will fail at startup.

I did not find a consensus-state/fork-choice correctness bug in the touched STF or attestation-processing paths themselves.

I could not run cargo test in this sandbox: cargo needs a writable ~/.cargo git cache for the leanVM dependency, which is blocked here.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Review Summary

This is a large, well-documented dependency migration (leanSig+lean-multisig devnet4 → leanVM's internalized xmss + rec_aggregation on main), and it's executed carefully:

Strengths:

  • The PR body and commit history are exemplary — every design decision (arena vs. system allocator, permit scoping, pubkey-less wire form) is explained with rationale.
  • test_verify_wrong_pubkey_set_fails is a good addition covering a real cross-client attack surface (proof binding to signer set).
  • The const _: [(); SIGNATURE_SIZE] = [(); SIGNATURE_SSZ_LEN]; pattern to statically pin ethlambda-types' hardcoded wire sizes against leanVM's actual scheme constants is a nice defense against silent wire-format drift on a future leanVM bump — better than a runtime assert or nothing at all.
  • The prover-permit design (acquire_prover, held only around the actual aggregate_single_message_signatures/merge_single_message_aggregates/split_multi_message_aggregate calls, not around decode/argument-conversion) matches leanVM's documented one-proof-at-a-time constraint without over-serializing.
  • Test updates (52→32 byte pubkeys) are mechanical and consistent across the codebase.

Findings (see structured list above):

  1. Stale documentation in block.rs (MultiMessageAggregate/SingleMessageAggregate doc comments) still describes the intermediate "pubkeys embedded in proof" design from an earlier commit in this same PR, which was explicitly reverted in "feat(crypto)!: keep pubkeys off the proof wire". The actual code consistently uses to_bytes_without_pubkeys()/from_bytes_without_pubkeys(). Worth a follow-up doc fix since it misdescribes the wire format and the validation mechanism (implicit SNARK binding vs. an explicit pubkey-set comparison).
  2. Missed opportunity, not a regression: ValidatorSecretKey::public_key() was added specifically because leanVM (unlike leanSig) exposes pubkey derivation, but main.rs's AnnotatedValidator::_pubkey_hex is still parsed-and-discarded rather than cross-checked against the loaded key. Pre-existing gap, now technically closable.
  3. Minor robustness question: recovering from a poisoned PROVER_PERMIT mutex assumes the underlying leanVM prover/arena state is safe to reuse after a panic mid-proof. Given the arena's bump-pointer/phase semantics described in the commit messages, this is worth a second look (or at least a comment confirming leanVM's panic-safety guarantees), though it's speculative without visibility into leanVM's internals.

No issues found in the actual fork-choice, attestation, or state-transition logic — this PR is scoped tightly to the signature/aggregation backend swap and pubkey size change, and the cryptographic call sites (block_builder.rs, aggregation.rs, reaggregate.rs, store.rs) that consume these APIs are untouched, which is consistent with the wire format being preserved.


Automated review by Claude (Anthropic) · sonnet · custom prompt

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.

2 participants