Skip to content

feat: add devnet 5 support#378

Merged
pablodeymo merged 21 commits into
mainfrom
devnet5
Jun 10, 2026
Merged

feat: add devnet 5 support#378
pablodeymo merged 21 commits into
mainfrom
devnet5

Conversation

@MegaRedHand

@MegaRedHand MegaRedHand commented May 15, 2026

Copy link
Copy Markdown
Collaborator

🗒️ Description / Motivation

Adds support for the pq-devnet-5 spec, 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, a SignedBlock now carries a single merged proof binding every signature it depends on.

Closes #285.

What Changed

Wire format / types (crates/common/types)

  • SignedBlock.signature: BlockSignaturesSignedBlock.proof: ByteList512KiB, an SSZ-encoded TypeTwoMultiSignature envelope. Helpers merged_proof_bytes() / wrap_merged_proof() handle the 4-byte SSZ offset header.
  • AggregatedSignatureProof is replaced by TypeOneMultiSignature (flat container per leanSpec PR #717). BlockSignatures and AttestationSignatures are gone.

Crypto (crates/common/crypto)

  • New leanVM operations: merge_type_1s_into_type_2 (block building), verify_type_2_signature (block import), and split_type_2_by_message (recovering per-attestation Type-1 proofs from a merged proof).

Blockchain

  • Block builder produces the merged Type-2 proof from the pooled Type-1 proofs; block import verifies it.
  • New reaggregate.rs module ports leanSpec's SyncService._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, max MAX_REAGGREGATIONS_PER_BLOCK splits per block) since each split runs a fresh SNARK.
  • process_block now reports whether the block was newly imported, so reaggregation only runs once per block.

Storage

  • BlockSignatures table now stores the raw proof envelope; the AggregatedPayloads table stores TypeOneMultiSignatures. Genesis blocks simply have no proof entry (the placeholder empty_block_signatures is gone).

Dependencies / CI

  • leanSpec pin bumped to 30ffb6c (2026-06-03), leanVM to e2592df, leansig pinned to its devnet4 branch.
  • CI fixture generation runs with -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.
  • Removed the curl+tar key-download workaround (the new pin is past leanSpec PR #745).

Correctness / Behavior Guarantees

  • The on-chain block root is unchanged by proof serialization: consumers always hash the inner Block, never the SignedBlock envelope.
  • Block import rejects blocks whose merged Type-2 proof does not verify against the block's attestations and proposer key.
  • Reaggregation never floods gossip: it is skipped while syncing, and the per-block SNARK-split budget is capped.

Tests Added / Run

  • Fork choice, signature, STF, and SSZ spec tests adapted to the devnet5 fixture format (fixtures regenerated from the new leanSpec pin).
  • New unit tests for the proof envelope helpers and reaggregation bounds.
  • make fmt / make lint clean (CI Lint is green). The CI Test job regenerates devnet5 fixtures (~2.5 h single-prover) before running the suite.

Related Issues / PRs

✅ Verification Checklist

  • Ran make fmt — clean
  • Ran make lint (clippy with -D warnings) — clean
  • Ran cargo test --workspace --release — CI Test job in progress (devnet5 fixture generation takes ~2.5 h)

pablodeymo and others added 6 commits May 12, 2026 16:25
…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.
MegaRedHand added a commit that referenced this pull request May 27, 2026
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.
MegaRedHand added a commit that referenced this pull request May 27, 2026
…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.
MegaRedHand and others added 13 commits May 27, 2026 16:32
…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>
@MegaRedHand MegaRedHand marked this pull request as ready for review June 10, 2026 15:41
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

This 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 & Correctness

Block Proof Envelope Validation (crates/common/types/src/block.rs:47-64)
The merged_proof_bytes() helper correctly validates the 4-byte SSZ offset header before slicing. This prevents out-of-bounds panics on malformed input, but consider explicitly logging the UnexpectedOffset case at the call site in verify_block_signatures to aid debugging invalid blocks from the network.

Reaggregation DoS Mitigation (crates/blockchain/src/reaggregate.rs:36)
The MAX_REAGGREGATIONS_PER_BLOCK = 4 cap is appropriate given the expensive SNARK splitting operation. Item 1 in the safety checklist is satisfied.

Validator Index Bounds (crates/blockchain/src/reaggregate.rs:98-108)
Good: Participant indices are checked against num_validators before array access in the reaggregation path. The early return on out-of-range prevents panics.

Slot Overflow Handling (crates/blockchain/src/reaggregate.rs:143-147)
The silent continue on slot.try_into() overflow is safe for consensus (the attestation is simply skipped), but consider logging this at debug! level to catch fixture/generation issues.

Performance

Memory Pressure in CI (.github/workflows/ci.yml:120-128)
The change from -n auto to -n 1 for fixture generation is correct given the 12 GiB peak per prover. The accompanying comment clearly documents the OOM root cause.

Reaggregation Cloning (crates/blockchain/src/reaggregate.rs:137)
pubkeys_per_component.clone() inside the candidate loop is O(N×M) where N is attestations and M is candidates (max 4). Given the small constants, this is acceptable, but note that split_type_2_by_message could potentially take the pubkeys by reference to avoid the clone if the lean-multisig API allowed it.

Explicit Drop (crates/blockchain/src/lib.rs:534)
drop(type_one_proofs) is semantically correct but redundant; the compiler would insert this automatically after the final use. No change required.

Code Quality

SSZ Test Skipping (crates/common/types/tests/ssz_spectests.rs:66-78)
The temporary skipping of SignedBlock, BlockSignatures, etc., is acceptable given the explicit TODO and the schema migration context. Ensure an issue is filed to track re-enabling these once the leanSpec pin moves past the Type-2 schema.

Error Cleanup (crates/blockchain/src/store.rs:801-812)
Good cleanup removing AttestationSignatureMismatch and ParticipantsMismatch which are obsolete in the merged-proof world. The new SlotOutOfRange error is more precise.

Fixture Field Renames (crates/common/test-fixtures/src/common.rs:101-108)
The serde renames from attestationPubkey to attestationPublicKey match the leanSpec PR #799 schema updates. Consistent with the comment in verify_signatures.rs.

Suggestions

  1. Log Level for Split Failures (crates/blockchain/src/reaggregate.rs:160-165)
    Consider upgrading the debug! log for split_type_2_by_message failures to warn! in production builds. A split failure indicates either a malformed block or a crypto library issue, both worth surfacing prominently.

  2. Comment Typo (crates/blockchain/src/reaggregate.rs:2)
    "deconstruct_block_into_store" should be "deconstruct_block_into_pool" or similar to match the actual function semantics (it writes to the store's payload buffer, not the block store).

  3. Skip List (crates/blockchain/tests/signature_spectests.rs:16-21)
    The SKIP_TESTS array is empty with a comment indicating Phase-3 crypto stubs. Either populate this with the tests that need real crypto, or remove the empty array to avoid confusion.

Summary

The 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

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. crates/storage/src/store.rs synthesizes genesis as ByteList512KiB::default(), but SignedBlock.proof is now defined as an SSZ-wrapped TypeTwoMultiSignature, and merged_proof_bytes() rejects any envelope shorter than 4 bytes. That means the node will serve a structurally invalid genesis/finalized block over BlocksByRoot and /lean/v0/blocks/finalized, which is likely to break cross-client decoding or later verification. This should synthesize SignedBlock::wrap_merged_proof(&[]) instead of an empty byte list.

  2. crates/common/crypto/src/lib.rs applies the 512 KiB cap to the raw compress_without_pubkeys() Type-2 bytes, but crates/blockchain/src/lib.rs then prepends the 4-byte SSZ wrapper before putting it into SignedBlock.proof. A proof at the advertised limit will therefore pass aggregation and then fail late during wrapping, turning a valid-size merge into a proposer-side block-build failure. The size check needs to reserve 4 bytes for the envelope, or the merge helper should return raw bytes and let the caller enforce the final wrapped limit.

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

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have enough context to write the full review.


PR Review: feat: add devnet 5 support (#378)

Overview

This PR implements pq-devnet-5 support by migrating from the legacy per-attestation BlockSignatures structure (devnet-4) to a single merged Type-2 SNARK proof on SignedBlock (leanSpec PR #717). It also adds a new reaggregate.rs module that recovers per-attestation Type-1 proofs from imported blocks so block-borne votes gain fork-choice weight.

The scope is large (1520 additions, 923 deletions across 29 files), but the mechanical rename from AggregatedSignatureProofTypeOneMultiSignature and ByteListMiBByteList512KiB accounts for most of the churn. The new crypto layer (merge_type_1s_into_type_2, verify_type_2_signature, split_type_2_by_message) is the core of the change.


Correctness

MAX_ATTESTATIONS_DATA silently halved (16 → 8)
crates/common/types/src/block.rs, line 3228. This is a protocol-level change (down from leanSpec PR #536 to PR #717's new cap). The old constant in lib.rs had an explicit citation; the new one has none. This must be confirmed against the spec and coordinated with all other clients.

SSZ spec tests disabled for all affected types
crates/common/types/tests/ssz_spectests.rs. SignedBlock, BlockSignatures, AggregatedSignatureProof, and SignedAggregatedAttestation are skipped with a TODO. The new wire format therefore has no SSZ compatibility test coverage until the leanSpec fixture pin is bumped. This is acknowledged, but it's worth tracking as a hard blocker before claiming devnet-5 readiness.

Bitfield length in the merged-case of reaggregate_from_block
crates/blockchain/src/reaggregate.rs, around line 1344:

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 max_vid + 1, not the original committee length. If the committee has 100 validators but only validators {5, 10, 15} participated, the merged bitfield would have length 16 rather than 100. Other clients may reject the gossip message if they enforce canonical committee-size bitfields. The block_t1 path (no local partials) correctly clones att.aggregation_bits and preserves the original length; the merge path should do the same. One safe fix: start from att.aggregation_bits.clone() (zeroed out) and OR in the union indices, rather than computing a minimum-length bitfield.

merged_proof_bytes() called inside the candidate loop
crates/blockchain/src/reaggregate.rs, line 1267:

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
    };

merged_proof_bytes() only parses a 4-byte header and returns a slice—it's infallible given a valid block (and validity was already checked during import). Calling it in the loop and discarding completed candidates on failure is unnecessary. Extract it once before the loop; if it fails, return early without processing any candidates.

pubkeys_per_component.clone() inside the loop
crates/blockchain/src/reaggregate.rs, line 1273:

let split_bytes = match ethlambda_crypto::split_type_2_by_message(
    merged_bytes,
    pubkeys_per_component.clone(),  // full clone every iteration
    &data_root,
) {

pubkeys_per_component is a Vec<Vec<ValidatorPublicKey>> where each key is 52 bytes. Cloning it on every candidate (up to 4 per MAX_REAGGREGATIONS_PER_BLOCK) is expensive. split_type_2_by_message could take &[Vec<ValidatorPublicKey>] or convert internally. Even if the API requires ownership, one option is to clone once per call only if candidates.len() > 1 and avoid cloning on the last iteration.


Minor / Style Issues

Dead code: TypeOneMultiSignature::for_proposer
crates/common/types/src/block.rs, around line 3297. The helper is defined but not used in this PR. The block-building path in lib.rs builds the proposer component manually. Either use the helper there or remove it.

Empty skip list constant adds noise
crates/blockchain/tests/signature_spectests.rs, line 3063:

const SKIP_TESTS: &[&str] = &[];

The associated infrastructure (if SKIP_TESTS.iter().any(...)) runs on every test but always takes the false branch. Fine to keep as a documented extension point, but the comment "every fixture is exercised against the real primitive" is the key claim—it should ideally be backed by at least one passing fixture so CI actually exercises verify_type_2_signature.

leansig pinned to a branch, not a rev
Cargo.toml, line 411:

leansig = { git = "https://github.com/leanEthereum/leanSig", branch = "devnet4" }

The Cargo.lock pins to a specific SHA (15cbdd43...), so this is safe for reproducibility. But the branch reference in Cargo.toml will cause cargo update to silently change the locked hash when the branch tip advances. All other leanVM crates use rev = "e2592df". Pinning leansig the same way would be more consistent and explicit.

PR description template left blank
The description still contains the placeholder text ("What does this PR change?", "List the files or areas touched", etc.). Given the scope of the change this makes it harder to review the intent and behavioral guarantees.


CI / Infrastructure

The CI improvements are well-reasoned:

  • Switching back to uv run python -m consensus_testing.keys --download --scheme prod (removing the manual curl+tar workaround) is correct now that the leanSpec pin has advanced past PR #745.
  • -n 1 for fixture generation with a memory-budget comment is exactly the right documentation for a subtle constraint.
  • The cache-poisoning fix (steps.generate-fixtures.outcome == 'success') is a real correctness improvement over the previous always() guard.

Summary

The architectural migration from per-attestation BlockSignatures to a single merged Type-2 proof is solid. The main issues to address before merge:

  • The bitfield-length bug in the merge path of reaggregate_from_block (could cause gossip rejection by other clients).
  • merged_proof_bytes() called in a loop where it should be hoisted out.
  • SSZ spec coverage gap—plan to re-enable skipped tests once the fixture pin advances.
  • MAX_ATTESTATIONS_DATA change from 16 → 8 needs an explicit spec citation.

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

@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR upgrades the client from pq-devnet-4 to pq-devnet-5 by replacing the per-attestation AggregatedSignatureProof model with a single merged Type-2 SNARK that binds all attestation and proposer signatures in one block-level proof (leanSpec PR #717). A new reaggregate.rs module recovers per-attestation Type-1 proofs post-import via SNARK-split, feeding them back into the aggregation pool so block-borne votes carry fork-choice weight.

  • Block wire format: SignedBlock now carries a single ByteList512KiB merged-proof field; TypeOneMultiSignature (renamed from AggregatedSignatureProof) is used for gossip-level and in-memory aggregation; MAX_ATTESTATIONS_DATA drops from 16 → 8.
  • Block verification: verify_block_signatures is rewritten to call the new verify_type_2_signature primitive after resolving per-component pubkeys from the block body; the previous parallel per-attestation verify path is removed.
  • CI hardening: the fixture-generation step is capped at -n 1 to prevent OOM kills on the 16 GiB runner, and the fixture cache is now only saved when generation actually succeeds (eliminating the empty-cache poisoning bug).

Confidence Score: 3/5

The 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 update_head after every successful block import, compensating for the removal of insert_known_aggregated_payloads_batch from on_block_core. The Hive test driver's apply_step "block" branch omits both of those steps. Any Hive fork-choice fixture whose expected head depends on block-borne attestation weights will disagree with the spec, making this a live correctness gap in the test-driver path that ships with this PR.

crates/net/rpc/src/test_driver.rs — the "block" step handler needs the same post-import proof-injection and update_head logic that forkchoice_spectests.rs performs.

Important Files Changed

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
Loading

Comments Outside Diff (2)

  1. crates/net/rpc/src/test_driver.rs, line 353-364 (link)

    P1 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.

    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.
  2. crates/blockchain/src/reaggregate.rs, line 1265-1270 (link)

    P2 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.

    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>
@pablodeymo pablodeymo merged commit 8ee2c79 into main Jun 10, 2026
4 checks passed
@pablodeymo pablodeymo deleted the devnet5 branch June 10, 2026 16:01
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.

Add support for pq-devnet-5

3 participants