refactor(types): port leanSpec Type-1 / Type-2 aggregation envelope#361
Conversation
…nature, and the BytecodeClaim placeholder to ethlambda-types, mirroring leanSpec commit anshalshukla/leanSpec@0ab09dd (dummy type 1 and type 2 aggregation with block proofs). Additive change with SSZ round-trip and capacity unit tests; the new types coexist with the legacy AggregatedSignatureProof / BlockSignatures wire shape until later phases migrate consumers.
…-signature envelope: SignedAggregatedAttestation.proof, AggregatedGroupOutput.proof, the storage PayloadBuffer, and the block-building helpers (compact_attestations, extend_proofs_greedily, build_block) now all carry TypeOneMultiSignature, reading participants from info.participants. SignedBlock still uses the legacy BlockSignatures shape on the wire; a temporary to_legacy/from_legacy boundary converts at block assembly and at block-body ingestion until Phase 3 swaps SignedBlock to a single Type-2 merged proof.
…stMiB carrying the SSZ-encoded TypeTwoMultiSignature that merges every per-attestation Type-1 plus a singleton proposer Type-1, completing the Type-1/Type-2 aggregation migration end-to-end. Block-level signature verification becomes a structural-only check (mirrors upstream's verify_type_2 stub) and gossip-time per-attestation verify remains the real safety net; block-body ingestion now decodes the merged proof, asserts info.len() == attestations.len() + 1, and feeds info-only Type-1 entries into the payload buffer for fork choice. Storage writes the proof blob into the existing BlockSignatures column family unchanged; legacy BlockSignatures / AttestationSignatures / AggregatedSignatureProof types are removed. SSZ-spec cases for the affected containers and one signature-spec case that relied on proposer-signature crypto are gated behind TODO(type1-type2) skips pending real verify_type_2 bindings. Attempted bump to anshalshukla/leanSpec@0ab09dd reverted (its testing harness still imports the removed AttestationSignatures so fixtures don't generate); pinned to canonical with a NOTE and regenerated fixtures.
…pe2-aggregation. PR #357 moved the per-test-binary fixture types (signature_types.rs, common.rs, build_signed_block) into the shared ethlambda-test-fixtures crate and renamed verify_signatures → verify_block_signatures (made pub) so the new RPC test-driver can call it. Conflicts resolved by: taking the rename and keeping our structural-only verify body; deleting the now-duplicated test/signature_types.rs and test/forkchoice_spectests.rs::build_signed_block; promoting our aggregate_type_2 / proposer_type_one helpers to TypeTwoMultiSignature::from_type_1s and TypeOneMultiSignature::for_proposer on ethlambda-types so the test-fixtures crate can build the merged Type-2 shape without depending on the blockchain layer; rewriting to_blank_signed_block (fork_choice) and From<TestSignedBlock> + try_into_signed_block_with_proofs (verify_signatures) in the test-fixtures crate to emit Type-2 proofs; updating test_driver's gossipAggregatedAttestation handler to build TypeOneMultiSignature instead of the removed AggregatedSignatureProof. cargo clippy --workspace --all-targets -- -D warnings and cargo test --workspace --release both green.
| /// `verify_type_2` primitive. | ||
| /// | ||
| /// TODO(type1-type2): re-enable once block-level crypto verification returns. | ||
| const SKIP_TESTS: &[&str] = &["test_invalid_proposer_signature"]; |
There was a problem hiding this comment.
We need to review why this fails
There was a problem hiding this comment.
Phase 3 replaced per-attestation verify_aggregated_signature + standalone proposer-signature crypto at block import with a structural check on the merged Type-2 envelope. The skipped test_invalid_proposer_signature flips proposer signature bytes and expects rejection on the crypto leg — that leg moved to gossip-time per-attestation verification, so the test passes against the structural stub and is misleading as-is. Re-enabled in the same follow-up that swaps the structural stub for the real verify_type_2 call (see thread on block.rs:178).
| /// Merge a list of Type-1 single-message proofs into a single Type-2 | ||
| /// multi-message proof. Mirrors upstream leanSpec's `aggregate_type_2` | ||
| /// stub: the metadata list (`TypeOneInfos`) is faithfully preserved so a | ||
| /// verifier can re-derive the per-message binding inputs, but the merged | ||
| /// `proof` bytes are left empty until the `lean_multisig_py` bindings ship | ||
| /// real cryptographic merging. Block-level signature verification stays | ||
| /// structural-only in the meantime, and per-attestation crypto verification | ||
| /// continues to run at gossip ingestion. |
There was a problem hiding this comment.
So we still lack the crypto primitives for this. Can we try using the devnet5 branch in leanMultisig?
There was a problem hiding this comment.
Yes — checked leanethereum/leanMultisig devnet5. It re-exports everything we need from rec_aggregation: TypeOneInfo, TypeOneMultiSignature, TypeTwoMultiSignature, aggregate_type_1, merge_many_type_1, split_type_2, verify_type_1, verify_type_2 (see src/lib.rs).
Two integration considerations for the follow-up PR:
- Their
TypeTwoMultiSignatureuses postcard + lz4 (not SSZ) and carriesExecutionProof+Evaluation<EF>, so we keep our outer SSZproof: ByteListMiBand stuff thecompress()bytes inside. setup_prover()/setup_verifier()must run once at boot.
Tracking this as a follow-up rather than expanding the scope of this PR — OK to keep the structural stub here in the meantime?
There was a problem hiding this comment.
Yes. Let's integrate in another PR
| // NOTE: After Phase 3 the legacy `BlockSignatures` / `AttestationSignatures` / | ||
| // `AggregatedSignatureProof` containers are removed from the domain, and | ||
| // `SignedBlock` now carries a single `proof: ByteListMiB` field. The pinned | ||
| // leanSpec fixtures still use the old shape, so SSZ-byte and root assertions | ||
| // for `SignedBlock`, `BlockSignatures`, `AggregatedSignatureProof`, and | ||
| // `SignedAggregatedAttestation` are intentionally skipped in | ||
| // `ssz_spectests.rs::run_ssz_test` until the fixture commit is bumped to the | ||
| // Type-1/Type-2 schema. |
There was a problem hiding this comment.
Can we not remove the types yet? It sounds like this is only temporary
There was a problem hiding this comment.
The temporariness is real, but I think the right cure is wiring up lean-multisig devnet5 (see thread on block.rs:178) rather than keeping the legacy containers around. Reasons:
- The legacy
BlockSignatures/AttestationSignatures/AggregatedSignatureProofshape is incompatible with how the merged Type-2 proof actually works on the wire — they assume per-attestation signatures travel inside the block, while Type-2 collapses them into a single merged blob, so a compat shim would be a no-op. - Keeping dead types invites accidental use of the wrong shape during the migration.
- The follow-up PR is small once the dep is in: mostly the
from_type_1sandverify_block_signaturesswaps.
Happy to revisit if you want a belt-and-braces approach.
🤖 Kimi Code ReviewHere is the review of the Type-1/Type-2 aggregation migration PR. Security & Correctness1. Bounds Check Ordering in
2. Inconsistent Error Types for Structural Mismatches
3. Hardcoded Limit in Test Fixtures
Code Quality & Maintainability4. Magic Number Usage
5. Clone Efficiency in
6. SSZ Stub Documentation
Memory Safety7. SSZ Decoding Limits
8. Bitfield Allocation in
Testing9. Test Skipping Strategy
10. Helper Function Duplication
Architectural ApprovalThe structural verification approach in
This is sound because the fork-choice rule only needs to account for votes (participants), not re-verify their cryptography at block import time. SummaryThe PR is correct and well-documented. The Type-1/Type-2 abstraction is cleanly implemented with appropriate SSZ bounds. Minor issues are limited to error variant consistency and magic number usage. The temporary removal of parallel crypto verification is appropriately flagged and safe given the gossip-layer verification remains intact. Recommended changes before merge:
Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Claude Code ReviewNow I have enough context to write a thorough review. Let me compile my findings. PR #361 —
|
🤖 Codex Code ReviewFindings
Notes I couldn’t run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Greptile SummaryThis PR ports the
Confidence Score: 3/5The gossip-layer crypto is intact, but The gossip-layer crypto is intact and the type migration across aggregation, storage, and block-production is clean. The unconditional proof decode in crates/blockchain/src/store.rs — both the unconditional proof decode in
|
| Filename | Overview |
|---|---|
| crates/common/types/src/block.rs | Introduces TypeOneMultiSignature, TypeTwoMultiSignature, TypeOneInfo, and BytecodeClaim; removes legacy BlockSignatures/AttestationSignatures/AggregatedSignatureProof. SSZ round-trips and capacity limits look correct; from_type_1s stub intentionally leaves proof bytes empty as documented. |
| crates/blockchain/src/store.rs | Rewrites verify_block_signatures as a structural-only check and adds inline proof decode in on_block_core; the merged proof is decoded twice on the verify=true path, and on_block_without_verification now enforces structural proof validity unconditionally — a behavioral change. Wrong error variants for slot/message mismatches. |
| crates/blockchain/src/aggregation.rs | Replaces AggregatedSignatureProof with TypeOneMultiSignature throughout; aggregate_job correctly populates TypeOneInfo with message, slot, and participants. resolve_child_pubkeys and select_proofs_greedily updated cleanly. |
| crates/blockchain/src/lib.rs | Assembles the merged Type-2 proof in propose_block by wrapping the proposer XMSS as a singleton Type-1 and folding all Type-1s via from_type_1s; logic is correct and matches verify_block_signatures expectations. |
| crates/common/test-fixtures/src/verify_signatures.rs | Fixture-to-SignedBlock converters updated to fold attestation Type-1s plus proposer Type-1 into a merged Type-2; both From and try_into_signed_block_with_proofs use zip which silently truncates if signature and attestation counts diverge, though verify_block_signatures would still catch the length mismatch. |
| crates/storage/src/store.rs | Switches PayloadEntry.proofs and all buffer APIs from AggregatedSignatureProof to TypeOneMultiSignature; write_signed_block and get_signed_block now store/load ByteListMiB blobs in the existing BlockSignatures column family (deferred rename). Subsumption logic correctly migrated to info.participants. |
Sequence Diagram
sequenceDiagram
participant Gossip
participant BlockChain as BlockChainServer
participant Store as blockchain::store
participant Storage as storage::store
Note over Gossip,Storage: Gossip path - per-attestation crypto still runs here
Gossip->>Store: on_gossip_aggregated_attestation(SignedAggregatedAttestation)
Store->>Store: verify_aggregated_signature(proof.proof, pubkeys, message, slot)
Store->>Storage: insert_new_aggregated_payload(TypeOneMultiSignature)
Note over BlockChain,Storage: Block production
BlockChain->>Store: produce_block_with_signatures(slot, validator_id)
Store->>Store: build_block() returns (Block, Vec TypeOneMultiSignature)
BlockChain->>BlockChain: TypeOneMultiSignature::for_proposer(proposer_index, xmss_sig, block_root, slot)
BlockChain->>BlockChain: TypeTwoMultiSignature::from_type_1s(all_t1s) - stub, proof bytes empty
BlockChain->>BlockChain: SSZ-encode to ByteListMiB to SignedBlock.proof
Note over BlockChain,Storage: Block ingestion
BlockChain->>Store: on_block(SignedBlock)
Store->>Store: verify_block_signatures(state, signed_block) - structural only
Store->>Store: TypeTwoMultiSignature::from_ssz_bytes(proof) - decode 1
Store->>Store: "check info.len() == attestations.len() + 1"
Store->>Store: check per-att message, slot, participants
Store->>Store: "check proposer entry - message==block_root, single bit"
Store->>Store: TypeTwoMultiSignature::from_ssz_bytes(proof) - decode 2, redundant
Store->>Store: insert info-only TypeOneMultiSignature entries into known_payloads
Store->>Storage: insert_signed_block(block_root, SignedBlock)
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
crates/blockchain/src/store.rs:526-534
**Redundant SSZ decode of `TypeTwoMultiSignature`**
When `verify = true`, `verify_block_signatures` already decodes and validates the merged proof (line 1216), performing the same `from_ssz_bytes` call and the same `info.len() == attestations.len() + 1` check. This block then repeats both operations unconditionally. On the hot block-ingestion path this means every block with signature verification pays for two SSZ decodes of potentially large proof blobs. Consider passing the already-decoded `TypeTwoMultiSignature` out of `verify_block_signatures` (or into `on_block_core`) rather than re-decoding from the raw bytes.
### Issue 2 of 3
crates/blockchain/src/store.rs:1236-1244
**Wrong error variants for non-count mismatches**
Two distinct failure modes are reported with misleading error types. When `info.slot != attestation.data.slot` the code returns `AttestationSignatureMismatch { signatures, attestations }` with the *same* counts — there is no count mismatch, so callers parsing the error numbers will be confused. When `info.message != attestation.data.hash_tree_root()` the code returns `ParticipantsMismatch`, but the participants may be identical — it is the signed message that diverges. These errors surface in logs and in the Hive test-driver's `verify_signatures/run` response, making triage harder when these checks start firing against real peers.
### Issue 3 of 3
crates/blockchain/src/store.rs:526-527
**`on_block_without_verification` now rejects structurally malformed proofs**
Previously, `on_block_without_verification` would happily zip over `attestation_signatures` regardless of their correctness. Now lines 526–534 run unconditionally (not guarded by `if verify`), so a call with a proof blob that fails SSZ decode *or* whose `info.len()` doesn't match attestation count will return an error even on the "no verification" path. This is a silent behavioral change for callers (tests, replay tools, benchmark harnesses) that construct `SignedBlock` with a minimal or placeholder proof. The intent of `on_block_without_verification` was to bypass signature checks entirely; it now enforces structural proof validity as a side effect of the fork-choice vote insertion logic.
Reviews (1): Last reviewed commit: "chore: remove comment" | Re-trigger Greptile
| let merged = TypeTwoMultiSignature::from_ssz_bytes(signed_block.proof.iter().as_slice()) | ||
| .map_err(|_| StoreError::ProposerSignatureDecodingFailed)?; | ||
| let expected_info_len = aggregated_attestations.len() + 1; | ||
| if merged.info.len() != expected_info_len { | ||
| return Err(StoreError::AttestationSignatureMismatch { | ||
| signatures: merged.info.len(), | ||
| attestations: aggregated_attestations.len(), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Redundant SSZ decode of
TypeTwoMultiSignature
When verify = true, verify_block_signatures already decodes and validates the merged proof (line 1216), performing the same from_ssz_bytes call and the same info.len() == attestations.len() + 1 check. This block then repeats both operations unconditionally. On the hot block-ingestion path this means every block with signature verification pays for two SSZ decodes of potentially large proof blobs. Consider passing the already-decoded TypeTwoMultiSignature out of verify_block_signatures (or into on_block_core) rather than re-decoding from the raw bytes.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/src/store.rs
Line: 526-534
Comment:
**Redundant SSZ decode of `TypeTwoMultiSignature`**
When `verify = true`, `verify_block_signatures` already decodes and validates the merged proof (line 1216), performing the same `from_ssz_bytes` call and the same `info.len() == attestations.len() + 1` check. This block then repeats both operations unconditionally. On the hot block-ingestion path this means every block with signature verification pays for two SSZ decodes of potentially large proof blobs. Consider passing the already-decoded `TypeTwoMultiSignature` out of `verify_block_signatures` (or into `on_block_core`) rather than re-decoding from the raw bytes.
How can I resolve this? If you propose a fix, please make it concise.| if info.slot != attestation.data.slot { | ||
| return Err(StoreError::AttestationSignatureMismatch { | ||
| signatures: merged.info.len(), | ||
| attestations: attestations.len(), | ||
| }); | ||
| } | ||
| if info.message != attestation.data.hash_tree_root() { | ||
| return Err(StoreError::ParticipantsMismatch); | ||
| } |
There was a problem hiding this comment.
Wrong error variants for non-count mismatches
Two distinct failure modes are reported with misleading error types. When info.slot != attestation.data.slot the code returns AttestationSignatureMismatch { signatures, attestations } with the same counts — there is no count mismatch, so callers parsing the error numbers will be confused. When info.message != attestation.data.hash_tree_root() the code returns ParticipantsMismatch, but the participants may be identical — it is the signed message that diverges. These errors surface in logs and in the Hive test-driver's verify_signatures/run response, making triage harder when these checks start firing against real peers.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/src/store.rs
Line: 1236-1244
Comment:
**Wrong error variants for non-count mismatches**
Two distinct failure modes are reported with misleading error types. When `info.slot != attestation.data.slot` the code returns `AttestationSignatureMismatch { signatures, attestations }` with the *same* counts — there is no count mismatch, so callers parsing the error numbers will be confused. When `info.message != attestation.data.hash_tree_root()` the code returns `ParticipantsMismatch`, but the participants may be identical — it is the signed message that diverges. These errors surface in logs and in the Hive test-driver's `verify_signatures/run` response, making triage harder when these checks start firing against real peers.
How can I resolve this? If you propose a fix, please make it concise.| let merged = TypeTwoMultiSignature::from_ssz_bytes(signed_block.proof.iter().as_slice()) | ||
| .map_err(|_| StoreError::ProposerSignatureDecodingFailed)?; |
There was a problem hiding this comment.
on_block_without_verification now rejects structurally malformed proofs
Previously, on_block_without_verification would happily zip over attestation_signatures regardless of their correctness. Now lines 526–534 run unconditionally (not guarded by if verify), so a call with a proof blob that fails SSZ decode or whose info.len() doesn't match attestation count will return an error even on the "no verification" path. This is a silent behavioral change for callers (tests, replay tools, benchmark harnesses) that construct SignedBlock with a minimal or placeholder proof. The intent of on_block_without_verification was to bypass signature checks entirely; it now enforces structural proof validity as a side effect of the fork-choice vote insertion logic.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/blockchain/src/store.rs
Line: 526-527
Comment:
**`on_block_without_verification` now rejects structurally malformed proofs**
Previously, `on_block_without_verification` would happily zip over `attestation_signatures` regardless of their correctness. Now lines 526–534 run unconditionally (not guarded by `if verify`), so a call with a proof blob that fails SSZ decode *or* whose `info.len()` doesn't match attestation count will return an error even on the "no verification" path. This is a silent behavioral change for callers (tests, replay tools, benchmark harnesses) that construct `SignedBlock` with a minimal or placeholder proof. The intent of `on_block_without_verification` was to bypass signature checks entirely; it now enforces structural proof validity as a side effect of the fork-choice vote insertion logic.
How can I resolve this? If you propose a fix, please make it concise.Move the per-block attestation-data cap to ethlambda_types::block as the canonical home, used by the wire types, the blockchain layer, and the test-fixtures crate. Replaces the duplicated literal in ethlambda-blockchain and the mirrored constant in ethlambda-test-fixtures, and derives the TypeOneInfos SSZ-list limit from MAX_ATTESTATIONS_DATA + 1 so the proposer-plus-attestations bound stays in one place. Addresses MegaRedHand review comment on PR #361.
🗒️ Description / Motivation
Ports the typed two-level multi-signature envelope introduced by contributor commit
anshalshukla/leanSpec@0ab09dd("dummy type 1 and type 2 aggregation with block proofs") to ethlambda:
TypeOneMultiSignature— single-message N-signer proof; replacesAggregatedSignatureProofon theSignedAggregatedAttestationgossip 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: ByteListMiBcarrying 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 reallean_multisigbindings land. Opening as draft until canonical catches up.What Changed
Landed as three commits, one per phase. Each phase compiled and passed
make testindependently.Phase 1 —
f2d0fb5— additive type plumbingcrates/common/types/src/block.rs— addedTypeOneInfo,TypeOneMultiSignature,TypeOneInfos(SSZ-list limitMAX_ATTESTATIONS_DATA + 1),TypeTwoMultiSignature, andBytecodeClaim(typed alias forH256, placeholder untillean_multisigdefines the trusted evaluation).Phase 2 —
18a60b5— gossip-layer pipelinecrates/common/types/src/attestation.rs—SignedAggregatedAttestation.proof→TypeOneMultiSignature.crates/blockchain/src/aggregation.rs—AggregatedGroupOutput.proof,aggregate_job,resolve_child_pubkeys,select_proofs_greedilyall carry/read Type-1.crates/storage/src/store.rs—PayloadEntry.proofs: Vec<TypeOneMultiSignature>; subsumption logic readsinfo.participants.compact_attestations,extend_proofs_greedily,build_block) operate on Type-1 throughout.to_legacy/from_legacyboundary at block assembly + block-body ingestion soSignedBlockwire stayed legacy through Phase 2.Phase 3 —
fc9ce1f— block wire + storageSignedBlock.signature: BlockSignatures→SignedBlock.proof: ByteListMiB. LegacyBlockSignatures/AttestationSignatures/AggregatedSignatureProofremoved.crates/blockchain/src/lib.rs::propose_blockwraps the proposer XMSS as a singleton Type-1, callsaggregate_type_2, SSZ-encodes the merged proof, and stashes it onSignedBlock.proof.crates/blockchain/src/store.rs::verify_signaturesrewritten as a structural-only check (mirrors upstreamverify_type_2stub): decode the merged proof, assertinfo.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_blocknow storeByteListMiBblobs in the existingBlockSignaturescolumn family (renaming deferred to avoid a CF migration).aggregate_type_2is a no-crypto stub today: it preserves the fullTypeOneInfosmetadata list but leavesproof: ByteListMiB::default(). Real merging arrives whenlean_multisigexposes a merged-proof primitive — the existingaggregate_proofsonly handles single-message merging.make leanSpec/fixtures). The regen also cleared three pre-existing forkchoice spec failures onmain(AttestationTooFarInFuture×2,AggregateVerificationFailed(InvalidProof)ontest_valid_gossip_aggregated_attestation) — they were stale-fixture artifacts.Correctness / Behavior Guarantees
Verified at gossip:
on_gossip_aggregated_attestationcontinues to run realethlambda_crypto::verify_aggregated_signatureon everySignedAggregatedAttestation. 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-multisigyet — the existingaggregate_proofsis single-message only).verify_signaturesenforces:info.len() == attestations.len() + 1,info[i]matches the correspondingblock.body.attestations[i]onparticipants,slot, andmessage,info[N]hasmessage == block_root,slot == block.slot, single-bitparticipantsset toblock.proposer_index,This is the conscious "mirror upstream stubs" trade-off agreed during planning. When
lean_multisigships a realverify_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_blockinserts info-only Type-1 entries (real(message, slot, participants), empty proof bytes) into the payload buffer.extract_latest_known_attestationsworks unchanged. Empty-bytes entries never get fed back intoaggregate_proofs(that path is only hit when multiple proofs share the sameAttestationData, 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 toTable::BlockProofis 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_HASHtoanshalshukla/leanSpec@0ab09ddto regenerate fixtures against the new schema. Reverted: the upstream testing harness in that commit (leanSpec/packages/testing/src/consensus_testing/keys.py) still importsAttestationSignatures, which the same commit removes —fillcrashes on module load. Documented in aNOTE(type1-type2)in the Makefile.Tests Added / Run
crates/common/types/src/block.rs.verify_signatures_rejects_participants_mismatch,build_block_caps_attestation_data_entries,on_block_rejects_duplicate_attestation_data, thecompact_attestationsandextend_proofs_greedilytests, allforkchoice_spectests.rsstep builders,signature_types.rsfixture converter, and therpc::test_get_latest_finalized_blocktest — all rebuilt to construct the new merged-proof shape.make fmt— cleancargo clippy --workspace --all-targets -- -D warnings— cleancargo test --workspace --release— green (84 forkchoice spec tests, 7 signature spec tests with 1 expected skip, all unit tests pass)Related Issues / PRs
anshalshukla/leanSpec@0ab09ddverify_type_2stub for the reallean_multisigprimitive.LEAN_SPEC_COMMIT_HASHskip markers inssz_spectests.rsandsignature_spectests.rs.Table::BlockSignatures→Table::BlockProof.✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleancargo test --workspace --release— all passing