feat: add devnet 5 support#378
Conversation
…361) ## 🗒️ Description / Motivation Ports the typed two-level multi-signature envelope introduced by contributor commit [`anshalshukla/leanSpec@0ab09dd`](anshalshukla/leanSpec@0ab09dd) ("dummy type 1 and type 2 aggregation with block proofs") to ethlambda: - `TypeOneMultiSignature` — single-message N-signer proof; replaces `AggregatedSignatureProof` on the `SignedAggregatedAttestation` gossip wire. - `TypeTwoMultiSignature` — merged multi-message proof binding every per-attestation Type-1 plus a singleton proposer Type-1 over the block root. - `SignedBlock.signature: BlockSignatures` → `SignedBlock.proof: ByteListMiB` carrying the SSZ-encoded merged Type-2. The upstream commit is WIP (verify functions are explicit stubs, not yet in canonical `leanethereum/leanSpec`). ethlambda leads the wire-shape migration so the type plumbing is in place when canonical absorbs the refactor and real `lean_multisig` bindings land. **Opening as draft until canonical catches up.** ## What Changed Landed as three commits, one per phase. Each phase compiled and passed `make test` independently. ### Phase 1 — `f2d0fb5` — additive type plumbing - `crates/common/types/src/block.rs` — added `TypeOneInfo`, `TypeOneMultiSignature`, `TypeOneInfos` (SSZ-list limit `MAX_ATTESTATIONS_DATA + 1`), `TypeTwoMultiSignature`, and `BytecodeClaim` (typed alias for `H256`, placeholder until `lean_multisig` defines the trusted evaluation). - SSZ round-trip + capacity unit tests. - Pure additive: no consumers yet. ### Phase 2 — `18a60b5` — gossip-layer pipeline - `crates/common/types/src/attestation.rs` — `SignedAggregatedAttestation.proof` → `TypeOneMultiSignature`. - `crates/blockchain/src/aggregation.rs` — `AggregatedGroupOutput.proof`, `aggregate_job`, `resolve_child_pubkeys`, `select_proofs_greedily` all carry/read Type-1. - `crates/storage/src/store.rs` — `PayloadEntry.proofs: Vec<TypeOneMultiSignature>`; subsumption logic reads `info.participants`. - Block-builder helpers (`compact_attestations`, `extend_proofs_greedily`, `build_block`) operate on Type-1 throughout. - Temporary `to_legacy` / `from_legacy` boundary at block assembly + block-body ingestion so `SignedBlock` wire stayed legacy through Phase 2. ### Phase 3 — `fc9ce1f` — block wire + storage - `SignedBlock.signature: BlockSignatures` → `SignedBlock.proof: ByteListMiB`. Legacy `BlockSignatures` / `AttestationSignatures` / `AggregatedSignatureProof` removed. - `crates/blockchain/src/lib.rs::propose_block` wraps the proposer XMSS as a singleton Type-1, calls `aggregate_type_2`, SSZ-encodes the merged proof, and stashes it on `SignedBlock.proof`. - `crates/blockchain/src/store.rs::verify_signatures` rewritten as a structural-only check (mirrors upstream `verify_type_2` stub): decode the merged proof, assert `info.len() == attestations.len() + 1`, validate per-attestation `(message, slot, participants)` alignment and the trailing proposer entry; no per-Type-1 crypto. - `crates/storage/src/store.rs::write_signed_block` / `get_signed_block` now store `ByteListMiB` blobs in the existing `BlockSignatures` column family (renaming deferred to avoid a CF migration). - `aggregate_type_2` is a no-crypto stub today: it preserves the full `TypeOneInfos` metadata list but leaves `proof: ByteListMiB::default()`. Real merging arrives when `lean_multisig` exposes a merged-proof primitive — the existing `aggregate_proofs` only handles single-message merging. - Test fixtures regenerated from canonical leanSpec (`make leanSpec/fixtures`). The regen also cleared three pre-existing forkchoice spec failures on `main` (`AttestationTooFarInFuture` ×2, `AggregateVerificationFailed(InvalidProof)` on `test_valid_gossip_aggregated_attestation`) — they were stale-fixture artifacts. ## Correctness / Behavior Guarantees **Verified at gossip:** `on_gossip_aggregated_attestation` continues to run real `ethlambda_crypto::verify_aggregated_signature` on every `SignedAggregatedAttestation`. Invalid aggregates are rejected at the gossip boundary just like before. **Block-level becomes structural:** Block-level verification no longer crypto-verifies the merged proof. The merged proof bytes can't be split client-side (the type-2 merging primitive doesn't exist in `lean-multisig` yet — the existing `aggregate_proofs` is single-message only). `verify_signatures` enforces: - `info.len() == attestations.len() + 1`, - each `info[i]` matches the corresponding `block.body.attestations[i]` on `participants`, `slot`, and `message`, - the trailing `info[N]` has `message == block_root`, `slot == block.slot`, single-bit `participants` set to `block.proposer_index`, - all participant indices fit within the validator registry. This is the conscious "mirror upstream stubs" trade-off agreed during planning. When `lean_multisig` ships a real `verify_type_2`, the structural stub is swapped for the real call. **Block-body ingestion preserves fork-choice LMD GHOST inputs:** since the merged proof can't be split, `process_new_block` inserts info-only Type-1 entries (real `(message, slot, participants)`, empty proof bytes) into the payload buffer. `extract_latest_known_attestations` works unchanged. Empty-bytes entries never get fed back into `aggregate_proofs` (that path is only hit when multiple proofs share the same `AttestationData`, in which case at least one came from gossip with real bytes). **Storage:** Table name kept (`BlockSignatures`) to avoid a RocksDB CF migration; doc comment updated. Renaming to `Table::BlockProof` is a follow-up. **Skipped tests, all behind `TODO(type1-type2)`:** - `ssz_spectests.rs`: `SignedBlock`, `BlockSignatures`, `AggregatedSignatureProof`, `SignedAggregatedAttestation` — on-disk SSZ bytes still use the legacy schema since canonical leanSpec hasn't absorbed the refactor. - `signature_spectests.rs`: `test_invalid_proposer_signature` — relies on block-level proposer-signature crypto, which is now a structural stub. Attempted to bump `LEAN_SPEC_COMMIT_HASH` to `anshalshukla/leanSpec@0ab09dd` to regenerate fixtures against the new schema. Reverted: the upstream testing harness in that commit (`leanSpec/packages/testing/src/consensus_testing/keys.py`) still imports `AttestationSignatures`, which the same commit removes — `fill` crashes on module load. Documented in a `NOTE(type1-type2)` in the Makefile. ## Tests Added / Run - Added: SSZ round-trip and capacity unit tests for the new Type-1/Type-2 containers in `crates/common/types/src/block.rs`. - Updated: `verify_signatures_rejects_participants_mismatch`, `build_block_caps_attestation_data_entries`, `on_block_rejects_duplicate_attestation_data`, the `compact_attestations` and `extend_proofs_greedily` tests, all `forkchoice_spectests.rs` step builders, `signature_types.rs` fixture converter, and the `rpc::test_get_latest_finalized_block` test — all rebuilt to construct the new merged-proof shape. - Verified locally: - `make fmt` — clean - `cargo clippy --workspace --all-targets -- -D warnings` — clean - `cargo test --workspace --release` — green (84 forkchoice spec tests, 7 signature spec tests with 1 expected skip, all unit tests pass) ## Related Issues / PRs - Upstream commit being ported: [`anshalshukla/leanSpec@0ab09dd`](anshalshukla/leanSpec@0ab09dd) - Follow-ups when canonical absorbs the refactor: - Swap the structural `verify_type_2` stub for the real `lean_multisig` primitive. - Revert `LEAN_SPEC_COMMIT_HASH` skip markers in `ssz_spectests.rs` and `signature_spectests.rs`. - Consider renaming `Table::BlockSignatures` → `Table::BlockProof`. ## ✅ Verification Checklist - [x] Ran `make fmt` — clean - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [x] Ran `cargo test --workspace --release` — all passing --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
# Conflicts: # crates/blockchain/src/store.rs # crates/common/test-fixtures/src/fork_choice.rs # crates/net/p2p/src/req_resp/handlers.rs # crates/net/rpc/src/lib.rs # crates/storage/src/store.rs
…370) Branched from #378 ## Summary - Bump `lean-multisig` / `leansig_wrapper` to devnet5 HEAD (`0242c909`) and rewrite `ethlambda-crypto` on the new Type-1 / Type-2 API. - Align the on-wire block proof with [leanSpec PR #717](leanEthereum/leanSpec#717) — `SignedBlock.proof` carries the SSZ-encoded `TypeTwoMultiSignature { proof: ByteList512KiB }` container, which collapses to `[4-byte LE offset = 4][type2_wire]` on the wire. - Port leanSpec PR #717's `SyncService._deconstruct_block_into_store` to the actor: imported blocks are SNARK-split per attestation, merged with local partial Type-1s, and (on aggregators) re-published on gossip. - Re-fixture against leanSpec `d9d2e67` (just past PR #717) and migrate the prod_scheme key JSON shape so signature and forkchoice spec tests cover the real cryptographic verifier end-to-end. ## Branch commit list | sha | what | |--|--| | `1cd80dd` | Crate-level integration: new Type-1 / Type-2 wrappers, real merge in `propose_block`, real `verify_type_2` in `verify_block_signatures`. | | `2c9dec0` | First pass at PR #717 envelope: `TypeOneInfo { participants, proof }`, `TypeTwoMultiSignature { info, proof }`, drop `bytecode_claim`. `split_type_2_signature(index)` → `split_type_2_by_message(message)`. | | `5361136` | Strip per-component Type-1 bytes when packing the Type-2 envelope — real Type-1s are ~225 KiB, N+1 copies blow the (old) 1 MiB `ByteListMiB` cap. | | `3199e7d` → `70c7cdb` | Experimental `--crypto-merge-t1-into-t2` flag, **reverted**. The merge runs synchronously on the actor today; moving it off-thread is a follow-up. | | `604ea4c` | Plan B: flatten `TypeOneMultiSignature` to `{ participants, proof }`, delete `TypeOneInfo` / `TypeOneInfos` / Rust `TypeTwoMultiSignature` wrappers, rename `ByteListMiB` → `ByteList512KiB`. | | `cc3df59` | Plan A: `reaggregate_from_block` module + actor hook. Caps `MAX_REAGGREGATIONS_PER_BLOCK = 4`, skips attestations behind the store's justified checkpoint, runs only when the chain is in sync. Aggregator-only republish on gossip. | | `4238a94` | Bump pinned `LEAN_SPEC_COMMIT_HASH` to `d9d2e67`, switch fixture generation to `--fork Lstar --scheme=test/prod`, port fixture parsers to the PR #717 schema (`signedBlock.proof.data` blob, `attestation.proof.proof.data` for gossip aggregates). | | `961aba4` | Restore the thin SSZ container header in front of the merged proof: `SignedBlock::wrap_merged_proof` / `merged_proof_bytes` helpers; lower `MAX_ATTESTATIONS_DATA` from 16 to 8 to match leanSpec PR #717. | ## Crypto crate API | function | wraps | notes | |--|--|--| | `aggregate_signatures(pks, sigs, msg, slot)` | `aggregate_type_1([], raw_xmss, …)` | Type-1 from raw XMSS only | | `aggregate_mixed(children, raw_pks, raw_sigs, msg, slot)` | `aggregate_type_1(children, raw_xmss, …)` | mixed Type-1 children + raw XMSS | | `aggregate_proofs(children, msg, slot)` | `aggregate_type_1(children, [], …)` | recursive Type-1 merge | | `verify_aggregated_signature(proof, pks, msg, slot)` | `verify_type_1` | Type-1 SNARK verify + explicit binding check | | `merge_type_1s_into_type_2(parts)` | `merge_many_type_1` | bundle N Type-1s into a Type-2 | | `verify_type_2_signature(proof_bytes, pks_per_component, expected_bindings)` | `verify_type_2` | Type-2 SNARK verify + per-component binding check; takes `&[u8]` after envelope strip | | `split_type_2_by_message(proof_bytes, pks_per_component, message)` | `split_type_2` (after locating index by message) | disaggregate to one Type-1; mirrors leanSpec `split_by_msg` | Type-1 / Type-2 proof bytes are `compress_without_pubkeys()` form throughout. `verify_type_2_signature` and `split_type_2_by_message` take `&[u8]` so callers feed the raw bytes (post-envelope-strip) directly. ## Wire format ``` TypeOneMultiSignature { participants: AggregationBits, proof: ByteList512KiB } SignedBlock.proof bytes: [4-byte LE offset = 4][raw lean-multisig Type-2 wire] ``` The 4-byte prefix is the SSZ Container-with-one-varlen-field offset header — the spec's `TypeTwoMultiSignature { proof: ByteList512KiB }` container. `SignedBlock::merged_proof_bytes()` / `SignedBlock::wrap_merged_proof()` keep the magic number off the call sites. No Rust struct for `TypeTwoMultiSignature` — per-component participants come from `block.body.attestations[i].aggregation_bits` and `block.proposer_index`, not the envelope. `MAX_ATTESTATIONS_DATA = 8` (down from 16, matching leanSpec PR #717). The merged Type-2 binds `MAX_ATTESTATIONS_DATA + 1 = 9` components, within lean-multisig's `MAX_RECURSIONS = 16`. ## Reaggregate-from-block New `crates/blockchain/src/reaggregate.rs`. After `process_block` succeeds and the chain is in sync, the actor: 1. Selects up to 4 attestations whose target outruns the store's justified checkpoint AND whose participants extend the local coverage. 2. `split_type_2_by_message`-splits each one out of the block's merged Type-2 proof. 3. Merges with locally-held partial Type-1s via `aggregate_proofs`. 4. Writes the combined proof into `latest_new_aggregated_payloads`. Aggregator-role nodes republish on gossip. 5 unit tests cover the candidate-selection rules without paying SNARK cost (target-slot gate, participant subset gate, hard cap, priority ordering). Each `split_type_2_by_message` runs a fresh SNARK; it currently executes synchronously on the actor thread. Moving to an off-thread worker mirroring `aggregation::run_aggregation_worker` is a natural follow-up if profiling shows it bleeding into the slot budget. ## Test coverage | Suite | Result | |---|---| | `signature_spectests` | 13 / 13 | | `forkchoice_spectests` | 84 / 84 | | `stf_spectests` | 35 / 35 | | `ssz_spectests` | 51 / 51 | | `test_driver_e2e` (Hive) | 8 / 8 | | `ethlambda-blockchain` unit | 24 / 24 (incl. 5 new reaggregate-from-block) | | Workspace total | 419 / 0 | Fixture regeneration: `LEAN_SPEC_COMMIT_HASH = d9d2e67`, generated with `make leanSpec/fixtures` (`uv run fill --fork Lstar --scheme=prod -o fixtures`). The prod_scheme key JSON shape upstream still has the pre-#725 flat layout (`attestation_public` / `attestation_secret` at top level) which the post-#725 `keys.py:395` no longer reads; this branch carries a one-shot local migration to the nested shape (`attestation_keypair.public_key` etc.) so the prod-scheme fixture filler runs. A small upstream PR converting the 12 `prod_scheme/*.json` files would let us drop the local step. ## Devnet validation Pending — re-run on a multi-node devnet now that `verify_type_2` actually executes on the import path. Expected to surface latency cliffs that the `--crypto-merge-t1-into-t2` flag previously hid.
Pinning to 825bec6 unintentionally pulled in leanSpec's devnet5 aggregated proof wire format (PR #717, an unavoidable ancestor of the download fix #745). That format requires the full crypto-stack migration tracked in the devnet5 PRs (#378/#370) and is not yet on main, so the forkchoice spec tests failed with AggregateVerificationFailed(DeserializationFailed). Repoint to f12000b (PR #725), the commit just before devnet5. It carries the key-schema rename that unbreaks fixture generation against the released `latest` test keys, while staying on the old proof format that the current leanMultisig@5eba3b1 still understands. The proofData -> proof field rename is therefore not needed and is reverted. f12000b predates the download fix (#745), whose download_keys reads the still-open download tempfile and intermittently aborts with EOFError. Work around it in both the Makefile and CI by fetching+extracting the prod keys with curl+tar before `fill`, so the buggy code path is skipped. Both blocks are flagged for removal once the pin moves past #745. Verified against freshly regenerated f12000b fixtures: forkchoice 84/0, signatures 11/0, full workspace 421/0.
…star (#391) ## Summary The pinned `LEAN_SPEC_COMMIT_HASH` (`18fe71f`, 2026-04-29) can no longer generate fixtures: the released `latest` test keys moved to the new key JSON schema (`attestation_keypair.public_key` etc.), so the old code fails with `KeyError: 'attestation_public'` during fixture generation. The fix bumps the pin to **`f12000b`** (leanSpec PR #725, 2026-05-17), which reads the new key schema. This is the commit **just before** leanSpec's devnet5 work, chosen deliberately: - An earlier draft of this PR pinned to `825bec6` (the #745 download fix). That commit unavoidably includes leanSpec PR #717 (`ac5f259`), which switched the aggregated-proof wire format to **devnet5**. The Rust client still runs `leanMultisig@5eba3b1` (pre-devnet5), so the forkchoice spec tests failed with `AggregateVerificationFailed(DeserializationFailed)`. The devnet5 proof format requires the full crypto-stack migration tracked in #378/#370, which is not yet on `main`. - `f12000b` predates #717, so fixtures keep the **old proof format** that the current crypto stack deserializes correctly. No Rust changes are needed. Also rename `--fork devnet` → `--fork Lstar` to match the upstream fork rename (already in effect at `f12000b`). ### Download workaround `f12000b` predates leanSpec PR #745, whose `download_keys` reads the still-open (unflushed) download tempfile, intermittently truncating the gzip tail and aborting with `EOFError` (surfaced as `Aborted!`). Both the Makefile and CI now fetch+extract the prod keys with `curl`+`tar` before `fill`, which fully writes the archive before reading it; `fill` then sees the keys present and skips its own buggy download. Both blocks are commented `Remove once the pin moves past PR #745.` ## Why not 825bec6 / devnet5? The devnet5 aggregated-proof wire format and the matching `ethlambda-crypto` rewrite live in #378 / #370 and are not yet merged to `main`. Bumping fixtures to a devnet5 leanSpec commit here would require duplicating that crypto work. This PR stays scoped to "unbreak fixture generation on `main`" and remains on the pre-devnet5 format until the devnet5 PRs land. ## Test plan - [x] `rm -rf leanSpec && make leanSpec/fixtures` — clean download (via curl+tar) + extract, 554 fixtures generated, no `Aborted!`. - [x] `cargo test -p ethlambda-blockchain --test forkchoice_spectests` — 84/0. - [x] `cargo test -p ethlambda-blockchain --test signature_spectests` — 11/0. - [x] `make test` — full workspace suite, 421/0. - [ ] CI runs green.
…Type-2 merge) (#395) ## Summary Fixes the devnet error `Failed to merge Type-1s into Type-2 ... child proof deserialization failed at index N`. **Root cause:** block import seeded the known payload pool with `TypeOneMultiSignature::empty(bits)` placeholders (participant bits, *empty proof bytes*) so block-borne votes carried fork-choice weight. Block building read the same pool and fed those empty bytes into the Type-2 merge, where lean-multisig decompression failed. **Fix:** stop registering block-attestation data in the pool at import altogether. A first pass mirrored leanSpec's `on_block` — registering each attestation's data key with an *empty proof set* instead of a proof-less placeholder. That removed the crash, but the empty entries turned out to be inert in our weight model and introduced an unbounded-growth path (see below), so the final version drops the registration entirely. ## Why removing it is safe ethlambda derives fork-choice weight from proof **participant bits**, not from data-key presence: - `extract_latest_attestations` iterates `entry.proofs`; an empty proof set yields zero voters. - the block builder scores candidates by voters-from-proofs, so an empty entry is never selected and never reaches `merge_type_1s_into_type_2`. So a registered-but-empty entry contributed **zero weight** and zero merge input. Its only observable effect was reserving an insertion-order slot for same-slot equivocation tie-breaking, which reaggregation re-establishes in block-attestation order anyway. Block-borne vote weight is still recovered: reaggregation SNARK-splits the block's merged Type-2 into per-attestation Type-1s (`split_type_2_by_message`) into the new pool, and gossip delivers them; both migrate to the known pool at the next acceptance tick. This matches the spec's documented one-slot deferral. ## Bonus: closes an unbounded-growth path The empty-proof entries bypassed the payload buffer's only capacity limit. `PayloadBuffer` evicts when `total_proofs > capacity`, but the bare-key insertion never incremented `total_proofs`, so a non-finalizing chain could grow `known_payloads` past its bound. Removing the registration eliminates this by construction (flagged by all four PR reviewers). ## Changes - `crates/blockchain/src/store.rs`: `on_block_core` no longer registers block-attestation data keys (keeps the per-validator attestation-valid metric). - `crates/storage/src/store.rs`: delete the now-dead `PayloadBuffer::register_data` and `Store::register_known_aggregated_data_batch`. - `crates/blockchain/tests/forkchoice_spectests.rs`: the harness deconstructs each imported block into per-attestation Type-1s (structurally, from `aggregation_bits`) into the known pool and recomputes the head — reproducing the proposer-view store the fixtures encode, the role leanSpec's harness fills via its proposer-build simulation. These entries go through `push`, so they are properly accounted and bounded. ## Notes / follow-ups - The spec routes block-reaggregated votes to the `new` pool (deferred), which a leanSpec reviewer flagged as leaving receiving nodes with zero block-vote weight (leanEthereum/leanSpec#717, discussion_r3259006576). The `new`-vs-`known` question remains open upstream. - Backfilling nodes don't run reaggregation (it is `synced`-gated in `lib.rs`), so block-borne votes during sync rely on gossip re-delivery. Accepted as an OK tradeoff for now; tracked as a follow-up. ## Test plan - [x] `cargo test --workspace --release` — full suite passes (forkchoice 84, stf 51, signature 13, storage 38, ssz 118, …; 0 failures) - [x] `cargo fmt --all -- --check` clean - [x] `cargo clippy --workspace --release -- -D warnings` clean
# Conflicts: # Cargo.lock # crates/common/crypto/Cargo.toml # crates/common/crypto/src/lib.rs
## 🗒️ Description / Motivation Recent leanVM changes introduced performance improvements. The upstream repo was also renamed from `leanMultisig` to `leanVM`. The pin matches the exact leanVM rev that leanSpec uses, so fixtures and client use the same prover/verifier. ## What Changed - Bump the multisig dependency to commit `8fcbd779`: the leanVM devnet5 rev that leanSpec main locks via `lean-multisig-py` v0.0.6. - Switch the git remote from `leanEthereum/leanMultisig` to the renamed `leanEthereum/leanVM`. - Pin transitive Plonky3 to `3f67d136` (the rev leanVM `8fcbd779` locks against). The floating Plonky3 HEAD otherwise resolved to a newer rev that requires the unstable `maybe_uninit_slice` feature and fails to build on stable. - Bump `LEAN_SPEC_COMMIT_HASH` in the Makefile to latest leanSpec main (`30ffb6ca`, 2026-06-03), which is what pulls in `lean-multisig-py` v0.0.6. --------- Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
…lope The leanSpec #833 sibling-fork tests merged from main still built SignedBlock with main's BlockSignatures envelope; devnet5 replaced it with a single merged-proof field.
## Motivation Track upstream leanVM at `e2592df` (the repo's `devnet5` branch), previously `8fcbd779` (#408). ## Description The aggregation crate renamed its public API since `8fcbd779`. `ethlambda-crypto` adapts to the new names; shapes, fields, and call signatures are unchanged, so the port is mechanical: | Old (`8fcbd779`) | New (`e2592df`) | |---|---| | `TypeOneMultiSignature` | `SingleMessageAggregateSignature` | | `TypeTwoMultiSignature` | `MultiMessageAggregateSignature` | | `aggregate_type_1` | `aggregate_single_message_signatures` | | `merge_many_type_1` | `merge_single_message_aggregates` | | `split_type_2` | `split_multi_message_aggregate` | | `verify_type_1` / `verify_type_2` | `verify_single_message_aggregate` / `verify_multi_message_aggregate` | **Plonky3 pin:** `p3-*` crates are pinned back to `3f67d136` (the rev devnet5 already used). Newer revs pulled in transitively by leanVM use the unstable `maybe_uninit_slice` feature, which doesn't build on Rust 1.92.0. ## Validation - ✅ `cargo build --workspace` - ✅ `cargo clippy -p ethlambda-crypto` clean - ✅ `cargo fmt --check` clean ## Notes - The heavy round-trip tests (`aggregate`/`verify`/`merge`/`split`) remain `#[ignore]` (12+ GiB/proof), so semantic correctness here is verified by compile + prover setup, not a live proof. - `test_setup_is_idempotent` stack-overflows in **debug** builds: the new bytecode compiler needs >2 MiB of stack. Release is unaffected and `make test` uses `--release`, so CI passes; flagging for awareness.
## Motivation The devnet5 branch now runs the `pq-devnet-5` spec, but the README still lists `pq-devnet-4` as the current devnet. ## Description - Move `pq-devnet-5` to current, with the `devnet5` Docker tag listed as available. - Move `pq-devnet-4` to the older devnets list (`devnet4` tag remains available). - Add a `pq-devnet-6` section noting it is in a planning phase with no features specified yet. - Drop the completed "Add support for pq-devnet-5" item from incoming features. --------- Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
🤖 Kimi Code ReviewThis PR implements the devnet5 Type-1/Type-2 multi-signature scheme (leanSpec PR #717), replacing per-attestation signature lists with a single merged SNARK proof per block. The changes are substantial but well-structured. Security & CorrectnessBlock Proof Envelope Validation ( Reaggregation DoS Mitigation ( Validator Index Bounds ( Slot Overflow Handling ( PerformanceMemory Pressure in CI ( Reaggregation Cloning ( Explicit Drop ( Code QualitySSZ Test Skipping ( Error Cleanup ( Fixture Field Renames ( Suggestions
SummaryThe PR correctly implements the consensus-critical Type-2 signature aggregation scheme with appropriate safety bounds. The CI fixes prevent cache poisoning and OOM kills. The temporary test skipping is documented and acceptable for a schema migration PR. Approve with minor logging suggestions. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code Review
The rest of the proof-model migration looked internally consistent, especially the end-to-end verifier path and the fork-choice/reaggregation plumbing. Residual risk is mostly test coverage: the SSZ conformance harness now skips the newly changed wire types, so compatibility regressions there are less likely to be caught automatically. Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewNow I have enough context to write the full review. PR Review: feat: add devnet 5 support (#378)OverviewThis PR implements pq-devnet-5 support by migrating from the legacy per-attestation The scope is large (1520 additions, 923 deletions across 29 files), but the mechanical rename from Correctness
SSZ spec tests disabled for all affected types Bitfield length in the merged-case of let max_vid = union_indices.iter().copied().max().unwrap_or(0);
let mut union_bits =
ethlambda_types::attestation::AggregationBits::with_length(max_vid as usize + 1)
...When merging a block proof with local partials, the resulting bitfield is sized to
for candidate in candidates {
// ...
let Ok(merged_bytes) = signed_block.merged_proof_bytes() else {
debug!("Reaggregation skipped: block proof envelope unusable");
return Vec::new(); // drops all prior progress
};
let split_bytes = match ethlambda_crypto::split_type_2_by_message(
merged_bytes,
pubkeys_per_component.clone(), // full clone every iteration
&data_root,
) {
Minor / Style IssuesDead code: Empty skip list constant adds noise const SKIP_TESTS: &[&str] = &[];The associated infrastructure (
leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" }The Cargo.lock pins to a specific SHA ( PR description template left blank CI / InfrastructureThe CI improvements are well-reasoned:
SummaryThe architectural migration from per-attestation
Automated review by Claude (Anthropic) · sonnet · custom prompt |
Greptile SummaryThis PR upgrades the client from pq-devnet-4 to pq-devnet-5 by replacing the per-attestation
Confidence Score: 3/5The core devnet-5 logic (Type-2 proof assembly, verification, and reaggregation) is well-structured and carefully commented, but the Hive test driver's block-step handler diverges from the offline spec-test runner in a way that will cause incorrect fork-choice results in the Hive environment. The offline spec-test runner inserts Type-1 proofs into the pool and re-runs crates/net/rpc/src/test_driver.rs — the "block" step handler needs the same post-import proof-injection and
|
| Filename | Overview |
|---|---|
| crates/net/rpc/src/test_driver.rs | Hive test driver apply_step "block" handler is missing the post-import proof injection and update_head call that the offline spec-test runner performs, creating a fork-choice divergence between the two test paths. |
| crates/blockchain/src/lib.rs | Major rework of block assembly (Type-1→Type-2 merge) and post-import reaggregation; process_block now returns bool (synced status) and reaggregation runs only when synced. |
| crates/blockchain/src/reaggregate.rs | New module: recovers per-attestation Type-1 proofs from a block's merged Type-2 via SNARK-split, then folds them into the aggregated payload pool; merged_proof_bytes() called per-iteration inside the candidate loop. |
| crates/blockchain/src/store.rs | Block verification rewritten for the Type-2 merged-proof model; on_block_core no longer stores per-attestation proofs during import; old proposer/participant mismatch error variants replaced. |
| crates/common/types/src/block.rs | New SignedBlock uses a single ByteList512KiB merged-proof field; TypeOneMultiSignature replaces AggregatedSignatureProof; MAX_ATTESTATIONS_DATA reduced from 16 to 8; envelope helpers are well-tested. |
| crates/blockchain/tests/forkchoice_spectests.rs | Correctly simulates post-import reaggregation by inserting Type-1 proofs into the known pool and calling update_head after each successful block import. |
| .github/workflows/ci.yml | Fixes cache-poisoning bug (no longer saves empty fixture cache on cancelled/OOM runs), simplifies key download to use the upstream helper, and limits prover parallelism to -n 1 to avoid OOM. |
| crates/blockchain/src/aggregation.rs | Mechanical rename from AggregatedSignatureProof / proof_data to TypeOneMultiSignature / proof; logic unchanged. |
Sequence Diagram
sequenceDiagram
participant V as Validator
participant BC as BlockChainServer
participant Crypto as ethlambda_crypto
participant Store as Store
V->>BC: propose_block(slot, validator_id, signature)
BC->>Store: produce_block_with_signatures()
Store-->>BC: (block, type_one_proofs[])
BC->>Crypto: aggregate_signatures([proposer_pubkey], [sig], block_root, slot)
Crypto-->>BC: proposer_proof_bytes (Type-1)
loop For each type_one_proof
BC->>Store: resolve attestation pubkeys
Store-->>BC: pubkeys
end
BC->>Crypto: merge_type_1s_into_type_2(merge_inputs)
Crypto-->>BC: merged_bytes (Type-2)
BC->>BC: SignedBlock::wrap_merged_proof(merged_bytes)
BC->>Store: on_block(signed_block)
Store-->>BC: Ok(synced)
alt "synced == true"
BC->>BC: run_reaggregate_from_block(signed_block)
BC->>Crypto: split_type_2_by_message(merged, pubkeys, data_root)
Crypto-->>BC: split_bytes (Type-1 per attestation)
BC->>Store: insert_new_aggregated_payloads_batch(entries)
end
Comments Outside Diff (2)
-
crates/net/rpc/src/test_driver.rs, line 353-364 (link)Hive test driver missing post-import reaggregation step
The offline spec-test runner (
forkchoice_spectests.rs) inserts Type-1 proofs into the known payload pool and re-runsupdate_headafter every successful block import. This compensates for the removal ofinsert_known_aggregated_payloads_batchfromon_block_core. The Hive test driver'sapply_step"block" branch callson_block_without_verificationbut performs neither step. As a result, any fixture whose expected head depends on block-borne attestation weights will produce a different head in the Hive runner than in the offline runner, causing those Hive fork-choice tests to fail or silently disagree.Prompt To Fix With AI
This is a comment left during a code review. Path: crates/net/rpc/src/test_driver.rs Line: 353-364 Comment: **Hive test driver missing post-import reaggregation step** The offline spec-test runner (`forkchoice_spectests.rs`) inserts Type-1 proofs into the known payload pool and re-runs `update_head` after every successful block import. This compensates for the removal of `insert_known_aggregated_payloads_batch` from `on_block_core`. The Hive test driver's `apply_step` "block" branch calls `on_block_without_verification` but performs neither step. As a result, any fixture whose expected head depends on block-borne attestation weights will produce a different head in the Hive runner than in the offline runner, causing those Hive fork-choice tests to fail or silently disagree. How can I resolve this? If you propose a fix, please make it concise.
-
crates/blockchain/src/reaggregate.rs, line 1265-1270 (link)merged_proof_bytes()called redundantly inside the candidate loopsigned_block.merged_proof_bytes()returns a shared slice intoself.proofand is free to call, but it is invoked once per candidate (up toMAX_REAGGREGATIONS_PER_BLOCKtimes) instead of once before the loop. If the envelope is invalid, thereturn Vec::new()on the error arm silently discards any aggregates collected in earlier loop iterations. Calling this before the loop would make the early-return semantics clearer and avoid the partial-progress discard.Prompt To Fix With AI
This is a comment left during a code review. Path: crates/blockchain/src/reaggregate.rs Line: 1265-1270 Comment: **`merged_proof_bytes()` called redundantly inside the candidate loop** `signed_block.merged_proof_bytes()` returns a shared slice into `self.proof` and is free to call, but it is invoked once per candidate (up to `MAX_REAGGREGATIONS_PER_BLOCK` times) instead of once before the loop. If the envelope is invalid, the `return Vec::new()` on the error arm silently discards any aggregates collected in earlier loop iterations. Calling this before the loop would make the early-return semantics clearer and avoid the partial-progress discard. How can I resolve this? If you propose a fix, please make it concise.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
crates/net/rpc/src/test_driver.rs:353-364
**Hive test driver missing post-import reaggregation step**
The offline spec-test runner (`forkchoice_spectests.rs`) inserts Type-1 proofs into the known payload pool and re-runs `update_head` after every successful block import. This compensates for the removal of `insert_known_aggregated_payloads_batch` from `on_block_core`. The Hive test driver's `apply_step` "block" branch calls `on_block_without_verification` but performs neither step. As a result, any fixture whose expected head depends on block-borne attestation weights will produce a different head in the Hive runner than in the offline runner, causing those Hive fork-choice tests to fail or silently disagree.
### Issue 2 of 2
crates/blockchain/src/reaggregate.rs:1265-1270
**`merged_proof_bytes()` called redundantly inside the candidate loop**
`signed_block.merged_proof_bytes()` returns a shared slice into `self.proof` and is free to call, but it is invoked once per candidate (up to `MAX_REAGGREGATIONS_PER_BLOCK` times) instead of once before the loop. If the envelope is invalid, the `return Vec::new()` on the error arm silently discards any aggregates collected in earlier loop iterations. Calling this before the loop would make the early-return semantics clearer and avoid the partial-progress discard.
Reviews (1): Last reviewed commit: "docs: update README devnet support for p..." | Re-trigger Greptile
## 🗒️ Description / Motivation - Adds the `MultiMessageAggregate` type required by the Devnet 5 specification. - Replaces the raw `ByteList512KiB` used by `SignedBlock.proof` with the correct SSZ container. - Removes manual SSZ offset wrapping and parsing. ## What Changed - Added `MultiMessageAggregate` containing a `ByteList512KiB` proof. - Updated `SignedBlock.proof` to use `MultiMessageAggregate`. - Added constructors and raw proof-byte accessors. - Updated block building, verification, reaggregation, storage, RPC, P2P, and test fixtures. - Added an SSZ encoding test for the new container. ## Correctness / Behavior Guarantees - The existing SSZ wire representation is preserved: the aggregate container encodes its variable-length proof with the required offset. - Raw lean-multisig proof bytes are passed unchanged to cryptographic operations. - Stored block proofs are now encoded and decoded as the specification-defined container. ## Tests Added / Run - Added `multi_message_aggregate_ssz_wraps_proof_bytes`. - Ran `cargo fmt --all`. - Ran `cargo check --workspace --all-targets` successfully. ## Related Issues / PRs - Closes #415 - Implements the aggregate container introduced in [leanEthereum/leanSpec#717](leanEthereum/leanSpec#717) ## ✅ Verification Checklist - [x] Ran formatting — clean - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [x] Ran `cargo test --workspace --release` — all passing Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
🗒️ Description / Motivation
Adds support for the
pq-devnet-5spec, whose headline feature is the new two-tier signature aggregation scheme from leanSpec PR #717: per-attestation Type-1 multi-signatures that get merged into a single block-level Type-2 proof. Instead of a block carrying one aggregated proof per attestation plus a proposer signature, aSignedBlocknow carries a single merged proof binding every signature it depends on.Closes #285.
What Changed
Wire format / types (
crates/common/types)SignedBlock.signature: BlockSignatures→SignedBlock.proof: ByteList512KiB, an SSZ-encodedTypeTwoMultiSignatureenvelope. Helpersmerged_proof_bytes()/wrap_merged_proof()handle the 4-byte SSZ offset header.AggregatedSignatureProofis replaced byTypeOneMultiSignature(flat container per leanSpec PR #717).BlockSignaturesandAttestationSignaturesare gone.Crypto (
crates/common/crypto)merge_type_1s_into_type_2(block building),verify_type_2_signature(block import), andsplit_type_2_by_message(recovering per-attestation Type-1 proofs from a merged proof).Blockchain
reaggregate.rsmodule ports leanSpec'sSyncService._deconstruct_block_into_store: after importing a block, it SNARK-splits the merged proof back into per-attestation Type-1 proofs and folds them into the local aggregated-payload pool, so block-borne votes can be republished on gossip. Splits are bounded (only when in sync, skip already-justified targets, skip participant subsets, maxMAX_REAGGREGATIONS_PER_BLOCKsplits per block) since each split runs a fresh SNARK.process_blocknow reports whether the block was newly imported, so reaggregation only runs once per block.Storage
BlockSignaturestable now stores the raw proof envelope; theAggregatedPayloadstable storesTypeOneMultiSignatures. Genesis blocks simply have no proof entry (the placeholderempty_block_signaturesis gone).Dependencies / CI
30ffb6c(2026-06-03), leanVM toe2592df,leansigpinned to itsdevnet4branch.-n 1: the devnet5 prover peaks at ~12 GiB per proof, so parallel provers OOM the 16 GiB runner. The fixtures cache is now only saved when generation actually succeeds, preventing a cancelled run from poisoning the cache with an empty fixture set.Correctness / Behavior Guarantees
Block, never theSignedBlockenvelope.Tests Added / Run
make fmt/make lintclean (CI Lint is green). The CI Test job regenerates devnet5 fixtures (~2.5 h single-prover) before running the suite.Related Issues / PRs
pq-devnet-5#285✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleancargo test --workspace --release— CI Test job in progress (devnet5 fixture generation takes ~2.5 h)