From fdd0f947bf0b63b70278d38b74969fe542139804 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Fri, 29 May 2026 17:42:55 +0200 Subject: [PATCH 1/3] refactor(forks/lstar): type SignedBlock.proof as TypeTwoMultiSignature The field was typed as raw ByteList512KiB and producers manually called encode_bytes before storing the result; verify_signatures and the post-block split path then decode_bytes-ed it back. Promote the field to its real type so SSZ handles the (de)serialization once at the container boundary. - Removes the encode-then-wrap dance in the validator service and test builders. - Removes the try/decode_bytes/except blocks in verify_signatures and the sync service post-block handler. - Updates SSZ round-trip tests, fork choice tests, and reqresp client helpers to construct the typed envelope directly. - Deletes four tautological decode-smoke assertions in test_service.py now that the Pydantic-validated field guarantees the shape. The on-wire SSZ encoding of SignedBlock is unchanged; hash_tree_root will change because Container merkleization differs from List[byte] merkleization, so consensus fixtures that hardcode block roots need regeneration. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../test_fixtures/verify_signatures.py | 9 ++++++--- .../src/consensus_testing/test_types/block_spec.py | 9 +++------ src/lean_spec/node/sync/service.py | 11 +---------- src/lean_spec/node/validator/service.py | 4 ++-- src/lean_spec/spec/forks/lstar/containers.py | 8 ++++---- src/lean_spec/spec/forks/lstar/spec.py | 9 ++------- .../lstar/ssz/test_consensus_containers.py | 11 +++++++++-- tests/lean_spec/helpers/builders.py | 4 ++-- .../networking/client/test_reqresp_client_range.py | 3 ++- tests/lean_spec/node/validator/test_service.py | 14 -------------- .../lstar/forkchoice/test_attestation_target.py | 4 ++-- 11 files changed, 33 insertions(+), 53 deletions(-) diff --git a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py index a8bd8774..ab7a92a6 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -13,6 +13,7 @@ AttestationData, SignedBlock, State, + TypeTwoMultiSignature, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import Boolean, ByteList512KiB, Bytes32 @@ -177,9 +178,11 @@ def _apply_tamper(self, signed_block: SignedBlock) -> SignedBlock: return signed_block if operation == "corrupt_proof": - # Replace the merged proof with a short non-decodable blob. - # Decoding the Type-2 envelope must fail before verification. - signed_block.proof = ByteList512KiB(data=b"\x00\x01\x02\x03") + # Replace the merged proof with a short bogus payload. + # The verifier rejects the malformed proof bytes. + signed_block.proof = TypeTwoMultiSignature( + proof=ByteList512KiB(data=b"\x00\x01\x02\x03"), + ) return signed_block if operation == "append_phantom_attestation": diff --git a/packages/testing/src/consensus_testing/test_types/block_spec.py b/packages/testing/src/consensus_testing/test_types/block_spec.py index fbfa4b1b..db810a3d 100644 --- a/packages/testing/src/consensus_testing/test_types/block_spec.py +++ b/packages/testing/src/consensus_testing/test_types/block_spec.py @@ -304,19 +304,16 @@ def _sign_block( ] public_keys_per_part.append([proposer_pubkey]) - merged = TypeTwoMultiSignature.aggregate( + proof = TypeTwoMultiSignature.aggregate( [*attestation_proofs, proposer_type_1], public_keys_per_part=public_keys_per_part, ) - proof_bytes = merged.encode_bytes() else: - placeholder = ByteList512KiB(data=b"") - envelope = TypeTwoMultiSignature(proof=placeholder) - proof_bytes = envelope.encode_bytes() + proof = TypeTwoMultiSignature(proof=ByteList512KiB(data=b"")) return SignedBlock( block=final_block, - proof=ByteList512KiB(data=proof_bytes), + proof=proof, ) def build_signed_block( diff --git a/src/lean_spec/node/sync/service.py b/src/lean_spec/node/sync/service.py index 8131d730..62ed33c6 100644 --- a/src/lean_spec/node/sync/service.py +++ b/src/lean_spec/node/sync/service.py @@ -31,10 +31,8 @@ from lean_spec.spec.forks.lstar.containers import ( AggregationError, TypeOneMultiSignature, - TypeTwoMultiSignature, ) from lean_spec.spec.ssz import Bytes32 -from lean_spec.spec.ssz.exceptions import SSZError from .backfill_sync import BackfillSync, NetworkRequester from .block_cache import BlockCache @@ -550,14 +548,7 @@ def _deconstruct_block_into_store( return store, [] validators = parent_state.validators - # The wrapper must not raise on a malformed proof. - # The block already passed signature verification upstream, so this - # catches the realistic SSZ deserialization failure modes only. - try: - type_two = TypeTwoMultiSignature.decode_bytes(block.proof.data) - except (SSZError, ValueError, IndexError) as exc: - logger.debug("Post-block Type-2 decode failed: %s", exc) - return store, [] + type_two = block.proof # Build the per-message pubkey layout once. # The layout is invariant per block: one entry per body attestation diff --git a/src/lean_spec/node/validator/service.py b/src/lean_spec/node/validator/service.py index d53b4722..2d3556fe 100644 --- a/src/lean_spec/node/validator/service.py +++ b/src/lean_spec/node/validator/service.py @@ -52,7 +52,7 @@ ValidatorIndex, ) from lean_spec.spec.forks.lstar.containers import TypeOneMultiSignature, TypeTwoMultiSignature -from lean_spec.spec.ssz import ByteList512KiB, Bytes32, Uint64 +from lean_spec.spec.ssz import Bytes32, Uint64 from .constants import HYSTERESIS_BAND, NETWORK_STALL_THRESHOLD, SYNC_LAG_THRESHOLD from .registry import ValidatorEntry, ValidatorRegistry @@ -485,7 +485,7 @@ def _sign_block( return SignedBlock( block=block, - proof=ByteList512KiB(data=merged.encode_bytes()), + proof=merged, ) def _sign_attestation( diff --git a/src/lean_spec/spec/forks/lstar/containers.py b/src/lean_spec/spec/forks/lstar/containers.py index 6416f8d8..1c581245 100644 --- a/src/lean_spec/spec/forks/lstar/containers.py +++ b/src/lean_spec/spec/forks/lstar/containers.py @@ -628,15 +628,15 @@ class Block(Container): class SignedBlock(Container): """Envelope carrying a block with a single aggregated proof for all signatures. - The proof is the SSZ-encoded form of a Type-2 multi-message proof that - binds every attestation in the body plus the proposer's signature over - the block root. + The proof is a Type-2 multi-message proof. + It binds every attestation in the body plus the proposer's signature + over the block root. """ block: Block """The block being signed.""" - proof: ByteList512KiB + proof: TypeTwoMultiSignature """Single full-block proof covering attestations and the proposer signature.""" diff --git a/src/lean_spec/spec/forks/lstar/spec.py b/src/lean_spec/spec/forks/lstar/spec.py index b6b84f12..5d9f6b7d 100644 --- a/src/lean_spec/spec/forks/lstar/spec.py +++ b/src/lean_spec/spec/forks/lstar/spec.py @@ -42,7 +42,6 @@ Slot, State, TypeOneMultiSignature, - TypeTwoMultiSignature, ValidatorIndex, Validators, ) @@ -836,7 +835,7 @@ def verify_signatures( """ Verify the merged Type-2 proof carried by a signed block. - The block envelope holds one SSZ-encoded Type-2 proof binding + The block envelope holds one Type-2 proof binding every body attestation plus the proposer's signature over the block root. @@ -852,11 +851,7 @@ def verify_signatures( """ block = signed_block.block aggregated_attestations = block.body.attestations - - try: - type_two = TypeTwoMultiSignature.decode_bytes(signed_block.proof.data) - except Exception as exc: - raise AssertionError(f"Block proof decoding failed: {exc}") from exc + type_two = signed_block.proof num_validators = Uint64(len(validators)) public_keys_per_message: list[list[PublicKey]] = [] diff --git a/tests/consensus/lstar/ssz/test_consensus_containers.py b/tests/consensus/lstar/ssz/test_consensus_containers.py index 0c8d22aa..923e30ed 100644 --- a/tests/consensus/lstar/ssz/test_consensus_containers.py +++ b/tests/consensus/lstar/ssz/test_consensus_containers.py @@ -23,6 +23,7 @@ SignedAttestation, SignedBlock, TypeOneMultiSignature, + TypeTwoMultiSignature, Validator, Validators, ) @@ -256,7 +257,10 @@ def test_signed_block_minimal(ssz: SSZTestFiller) -> None: ) ssz( type_name="SignedBlock", - value=SignedBlock(block=block, proof=ByteList512KiB(data=b"")), + value=SignedBlock( + block=block, + proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b"")), + ), ) @@ -271,7 +275,10 @@ def test_signed_block_with_proof_bytes(ssz: SSZTestFiller) -> None: ) ssz( type_name="SignedBlock", - value=SignedBlock(block=block, proof=ByteList512KiB(data=b"\xde\xad\xbe\xef")), + value=SignedBlock( + block=block, + proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b"\xde\xad\xbe\xef")), + ), ) diff --git a/tests/lean_spec/helpers/builders.py b/tests/lean_spec/helpers/builders.py index 00e9af0d..4146e5d2 100644 --- a/tests/lean_spec/helpers/builders.py +++ b/tests/lean_spec/helpers/builders.py @@ -210,7 +210,7 @@ def make_signed_block( body=BlockBody(attestations=AggregatedAttestations(data=[])), ) - return SignedBlock(block=block, proof=ByteList512KiB(data=b"")) + return SignedBlock(block=block, proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b""))) def make_aggregated_attestation( @@ -477,7 +477,7 @@ def make_signed_block_from_store( signed_block = SignedBlock( block=block, - proof=ByteList512KiB(data=merged.encode_bytes()), + proof=merged, ) target_interval = Interval.from_slot(block.slot) diff --git a/tests/lean_spec/node/networking/client/test_reqresp_client_range.py b/tests/lean_spec/node/networking/client/test_reqresp_client_range.py index f80995d2..4ab20e8c 100644 --- a/tests/lean_spec/node/networking/client/test_reqresp_client_range.py +++ b/tests/lean_spec/node/networking/client/test_reqresp_client_range.py @@ -24,6 +24,7 @@ Block, BlockBody, SignedBlock, + TypeTwoMultiSignature, ) from lean_spec.spec.ssz import ByteList512KiB, Bytes32, Uint64 @@ -115,7 +116,7 @@ def empty_signed_block(slot: Slot, parent_root: Bytes32, state_seed: int) -> Sig state_root=Bytes32(bytes([state_seed]) * 32), body=BlockBody(attestations=AggregatedAttestations(data=[])), ) - return SignedBlock(block=block, proof=ByteList512KiB(data=b"")) + return SignedBlock(block=block, proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b""))) def build_chain(start_slot: int, count: int, root_seed: int = 0xAA) -> list[SignedBlock]: diff --git a/tests/lean_spec/node/validator/test_service.py b/tests/lean_spec/node/validator/test_service.py index b5090e2f..1d3acac6 100644 --- a/tests/lean_spec/node/validator/test_service.py +++ b/tests/lean_spec/node/validator/test_service.py @@ -25,7 +25,6 @@ SignedAttestation, SignedBlock, TypeOneMultiSignature, - TypeTwoMultiSignature, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import Bytes32, Uint64 @@ -139,7 +138,6 @@ def test_attestation_proofs_merge_into_envelope( result = service._sign_block(block, ValidatorIndex(0), [agg_proof]) - TypeTwoMultiSignature.decode_bytes(result.proof.data) assert result.block.proposer_index == ValidatorIndex(0) def test_missing_validator_raises_value_error( @@ -939,9 +937,6 @@ async def capture_block(block: SignedBlock) -> None: assert signed_block.block.slot == Slot(1) assert signed_block.block.proposer_index == ValidatorIndex(1) - # The merged proof must decode and the block carries the proposer index. - TypeTwoMultiSignature.decode_bytes(signed_block.proof.data) - async def test_produce_real_attestation_with_valid_signature( self, key_manager: XmssKeyManager, @@ -1041,11 +1036,6 @@ async def capture_block(block: SignedBlock) -> None: await service._maybe_produce_block(Slot(2)) assert len(blocks_produced) == 1 - signed_block = blocks_produced[0] - - # The merged proof decodes cleanly; the proposer identity now lives - # on the block, not inside the proof envelope. - TypeTwoMultiSignature.decode_bytes(signed_block.proof.data) async def test_block_includes_pending_attestations( self, @@ -1104,10 +1094,6 @@ async def capture_block(block: SignedBlock) -> None: body_attestations = signed_block.block.body.attestations assert len(body_attestations) > 0 - # The merged proof decodes; its component count is rederived from - # the block body (one Type-1 per attestation plus the proposer). - TypeTwoMultiSignature.decode_bytes(signed_block.proof.data) - async def test_multiple_slots_produce_different_attestations( self, real_sync_service: SyncService, diff --git a/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py b/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py index 68b9cd02..7ae0428b 100644 --- a/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py +++ b/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py @@ -19,7 +19,7 @@ TypeTwoMultiSignature, ) from lean_spec.spec.forks.lstar.spec import LstarSpec -from lean_spec.spec.ssz import ByteList512KiB, Bytes32 +from lean_spec.spec.ssz import Bytes32 from tests.lean_spec.helpers import make_store @@ -597,7 +597,7 @@ def test_attestation_target_after_on_block( ) signed_block = SignedBlock( block=block, - proof=ByteList512KiB(data=merged.encode_bytes()), + proof=merged, ) # Process block via on_block on a fresh consumer store From 76ac8481088f34774cf8c6432e10393850893d8b Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Fri, 29 May 2026 17:46:12 +0200 Subject: [PATCH 2/3] refactor(forks/lstar): inline single-use type_two alias Both call sites used the local alias exactly once. Drop it. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lean_spec/node/sync/service.py | 4 +--- src/lean_spec/spec/forks/lstar/spec.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/lean_spec/node/sync/service.py b/src/lean_spec/node/sync/service.py index 62ed33c6..6a899643 100644 --- a/src/lean_spec/node/sync/service.py +++ b/src/lean_spec/node/sync/service.py @@ -548,8 +548,6 @@ def _deconstruct_block_into_store( return store, [] validators = parent_state.validators - type_two = block.proof - # Build the per-message pubkey layout once. # The layout is invariant per block: one entry per body attestation # in order, then the proposer entry. Hoisted out of the per-att loop @@ -607,7 +605,7 @@ def _deconstruct_block_into_store( try: # The split takes the bits from the block attestation this # component binds, since the Rust binding does not return them. - block_t1 = type_two.split_by_msg( + block_t1 = block.proof.split_by_msg( message=data_root, public_keys_per_message=public_keys_per_message, participants=att.aggregation_bits, diff --git a/src/lean_spec/spec/forks/lstar/spec.py b/src/lean_spec/spec/forks/lstar/spec.py index 5d9f6b7d..6b3ce6fc 100644 --- a/src/lean_spec/spec/forks/lstar/spec.py +++ b/src/lean_spec/spec/forks/lstar/spec.py @@ -851,7 +851,6 @@ def verify_signatures( """ block = signed_block.block aggregated_attestations = block.body.attestations - type_two = signed_block.proof num_validators = Uint64(len(validators)) public_keys_per_message: list[list[PublicKey]] = [] @@ -895,7 +894,7 @@ def verify_signatures( message_bindings.append((hash_tree_root(block), block.slot)) try: - type_two.verify( + signed_block.proof.verify( public_keys_per_message=public_keys_per_message, messages=message_bindings, ) From 7facb6d258b021acb5d36000657b598fd8310d62 Mon Sep 17 00:00:00 2001 From: Thomas Coratger <60488569+tcoratger@users.noreply.github.com> Date: Fri, 29 May 2026 18:00:51 +0200 Subject: [PATCH 3/3] refactor(forks/lstar): rename multi-signature types to SingleMessageAggregate / MultiMessageAggregate TypeOneMultiSignature and TypeTwoMultiSignature were named after the underlying Rust prover convention, not what they semantically are. Rename them to express the actual data: an aggregate over a single message versus an aggregate over many distinct messages. - Classes: TypeOneMultiSignature -> SingleMessageAggregate, TypeTwoMultiSignature -> MultiMessageAggregate - Variables and locals follow the snake_case form: type_1 -> single_message_aggregate, type_2 -> multi_message_aggregate (plus the compound forms type1_wire, type2_wire, block_t1, proposer_type_1, etc.) - Test function names match: test_type_one_* -> test_single_message_*, test_type_two_* -> test_multi_message_* - Prose mentions of "Type-1" / "Type-2" in docstrings and comments become "single-message aggregate" / "multi-message aggregate" External Rust binding names (aggregate_type_1, verify_type_1, merge_many_type_1, split_type_2_by_msg, verify_type_2_with_messages) come from the leanMultisig-py package and are left untouched. The "Type 1" / "Type 2" mentions in node/snappy/encoding.py refer to Snappy's Copy Type 1 / Copy Type 2 wire encodings and are unrelated. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../testing/src/consensus_testing/keys.py | 27 +++--- .../test_fixtures/state_transition.py | 8 +- .../test_fixtures/verify_signatures.py | 6 +- .../test_types/aggregated_attestation_spec.py | 12 +-- .../test_types/block_spec.py | 39 ++++----- .../gossip_aggregated_attestation_spec.py | 6 +- src/lean_spec/node/sync/service.py | 29 +++---- src/lean_spec/node/validator/service.py | 27 +++--- .../spec/forks/lstar/aggregation_select.py | 14 ++-- src/lean_spec/spec/forks/lstar/containers.py | 82 +++++++++++-------- src/lean_spec/spec/forks/lstar/spec.py | 37 +++++---- src/lean_spec/spec/forks/lstar/store.py | 10 +-- .../lstar/fc/test_block_production.py | 2 +- .../lstar/ssz/test_consensus_containers.py | 12 +-- .../lstar/ssz/test_xmss_containers.py | 30 +++---- .../test_structural_rejections.py | 2 +- tests/lean_spec/helpers/builders.py | 20 ++--- .../client/test_reqresp_client_range.py | 4 +- .../node/sync/test_reaggregate_from_block.py | 8 +- .../lean_spec/node/validator/test_service.py | 4 +- .../spec/crypto/xmss/test_aggregation.py | 77 +++++++++-------- .../forkchoice/test_attestation_target.py | 15 ++-- .../forkchoice/test_compute_block_weights.py | 8 +- .../forkchoice/test_store_attestations.py | 14 ++-- .../lstar/forkchoice/test_store_pruning.py | 4 +- .../forks/lstar/forkchoice/test_validator.py | 4 +- 26 files changed, 265 insertions(+), 236 deletions(-) diff --git a/packages/testing/src/consensus_testing/keys.py b/packages/testing/src/consensus_testing/keys.py index 8caa8c75..d71327e9 100755 --- a/packages/testing/src/consensus_testing/keys.py +++ b/packages/testing/src/consensus_testing/keys.py @@ -65,7 +65,7 @@ from lean_spec.spec.forks.lstar.containers import ( AggregatedAttestations, AttestationData, - TypeOneMultiSignature, + SingleMessageAggregate, ) from lean_spec.spec.ssz import Bytes32, Uint64 @@ -513,13 +513,16 @@ def sign_and_aggregate( self, validator_ids: list[ValidatorIndex], attestation_data: AttestationData, - ) -> TypeOneMultiSignature: + ) -> SingleMessageAggregate: """ - Sign attestation data with each validator and aggregate into a Type-1 proof. + Sign attestation data with each validator and aggregate the result. + + Returns a single-message aggregate proof binding all participants + to the (data, slot) pair. Each validator's XMSS attestation key signs the attestation data root. The signatures are then handed to the multi-signature - binding to produce a single cryptographically valid Type-1 proof + binding to produce a single cryptographically valid single-message aggregate proof binding all participants to (data, slot). Args: @@ -527,7 +530,7 @@ def sign_and_aggregate( attestation_data: The attestation data to sign. Returns: - Cryptographically valid Type-1 proof covering validator_ids. + Cryptographically valid single-message aggregate proof covering validator_ids. """ raw_xmss = [ ( @@ -537,7 +540,7 @@ def sign_and_aggregate( ) for vid in validator_ids ] - return TypeOneMultiSignature.aggregate( + return SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss, message=hash_tree_root(attestation_data), @@ -549,15 +552,15 @@ def build_attestation_proofs( aggregated_attestations: AggregatedAttestations, signature_lookup: Mapping[AttestationData, Mapping[ValidatorIndex, Signature]] | None = None, - ) -> list[TypeOneMultiSignature]: + ) -> list[SingleMessageAggregate]: """ - Produce Type-1 proofs aligned with the given attestations. + Produce single-message aggregate proofs aligned with the given attestations. For each aggregated attestation: 1. Identify participating validators from the aggregation bitfield. 2. Collect each participant's attestation public key and signature. - 3. Combine them into a single Type-1 single-message proof via the + 3. Combine them into a single single-message aggregate single-message proof via the multi-signature binding. Pre-computed signatures can be supplied via the lookup to avoid @@ -569,11 +572,11 @@ def build_attestation_proofs( attestation data then validator index. Returns: - One Type-1 single-message proof per attestation, parallel to the input. + One single-message aggregate proof per attestation, parallel to the input. """ lookup = signature_lookup or {} - proofs: list[TypeOneMultiSignature] = [] + proofs: list[SingleMessageAggregate] = [] for agg in aggregated_attestations: # Decode which validators participated from the bitfield. validator_ids = agg.aggregation_bits.to_validator_indices() @@ -594,7 +597,7 @@ def build_attestation_proofs( # Produce a single aggregated proof that the leanVM can verify # in one pass over all participants. proofs.append( - TypeOneMultiSignature.aggregate( + SingleMessageAggregate.aggregate( children=[], raw_xmss=list(zip(validator_ids, public_keys, signatures, strict=True)), message=hash_tree_root(agg.data), diff --git a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py index fbac6655..9666b82b 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/state_transition.py +++ b/packages/testing/src/consensus_testing/test_fixtures/state_transition.py @@ -12,8 +12,8 @@ AttestationData, Block, BlockBody, + SingleMessageAggregate, State, - TypeOneMultiSignature, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import Bytes32 @@ -255,7 +255,7 @@ def _build_block_from_spec( # Path 3: normal block construction via the spec's builder. else: - aggregated_payloads: dict[AttestationData, set[TypeOneMultiSignature]] = {} + aggregated_payloads: dict[AttestationData, set[SingleMessageAggregate]] = {} if spec.attestations: aggregated_payloads = StateTransitionTest._build_aggregated_payloads_from_spec( spec.attestations, state, block_registry @@ -301,7 +301,7 @@ def _build_aggregated_payloads_from_spec( attestation_specs: list[AggregatedAttestationSpec], state: State, block_registry: dict[str, Block], - ) -> dict[AttestationData, set[TypeOneMultiSignature]]: + ) -> dict[AttestationData, set[SingleMessageAggregate]]: """ Build aggregated signature payloads from attestation specifications. @@ -317,7 +317,7 @@ def _build_aggregated_payloads_from_spec( # XMSS keys require precomputation up to the highest slot used. max_slot = max(spec.slot for spec in attestation_specs) key_manager = XmssKeyManager.shared(max_slot=max_slot) - payloads: dict[AttestationData, set[TypeOneMultiSignature]] = {} + payloads: dict[AttestationData, set[SingleMessageAggregate]] = {} for spec in attestation_specs: if not spec.valid_signature: diff --git a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py index ab7a92a6..77fd9e86 100644 --- a/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py +++ b/packages/testing/src/consensus_testing/test_fixtures/verify_signatures.py @@ -11,9 +11,9 @@ AggregatedAttestation, AggregatedAttestations, AttestationData, + MultiMessageAggregate, SignedBlock, State, - TypeTwoMultiSignature, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import Boolean, ByteList512KiB, Bytes32 @@ -69,7 +69,7 @@ class VerifySignaturesTest(BaseConsensusFixture): set bit. Exercises the empty-participants check inside signature verification. - `{"operation": "corrupt_proof"}`: Replace the merged proof with - a short non-decodable blob. Exercises the Type-2 decode check. + a short non-decodable blob. Exercises the multi-message aggregate decode check. - `{"operation": "append_phantom_attestation"}`: Add a body attestation with no matching proof component. Exercises the component count check between the body and the merged proof. @@ -180,7 +180,7 @@ def _apply_tamper(self, signed_block: SignedBlock) -> SignedBlock: if operation == "corrupt_proof": # Replace the merged proof with a short bogus payload. # The verifier rejects the malformed proof bytes. - signed_block.proof = TypeTwoMultiSignature( + signed_block.proof = MultiMessageAggregate( proof=ByteList512KiB(data=b"\x00\x01\x02\x03"), ) return signed_block diff --git a/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py index c6206360..ff9731c1 100644 --- a/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py +++ b/packages/testing/src/consensus_testing/test_types/aggregated_attestation_spec.py @@ -9,8 +9,8 @@ AggregatedAttestations, AttestationData, Block, + SingleMessageAggregate, State, - TypeOneMultiSignature, ) from lean_spec.spec.ssz import ByteList512KiB, Bytes32 @@ -161,7 +161,7 @@ def build_invalid_proof( state: State, key_manager: XmssKeyManager, block: Block, - ) -> tuple[Block, TypeOneMultiSignature]: + ) -> tuple[Block, SingleMessageAggregate]: """ Build an invalid attestation proof and append it to the block body. @@ -187,20 +187,20 @@ def build_invalid_proof( data=attestation_data, ) - # Empty proof bytes flag "no real Type-1 here" — the caller treats + # Empty proof bytes flag "no real single-message aggregate here" — the caller treats # any such entry as a placeholder and bypasses real binding merges. placeholder = ByteList512KiB(data=b"") if not self.valid_signature: - invalid_proof = TypeOneMultiSignature(participants=aggregation_bits, proof=placeholder) + invalid_proof = SingleMessageAggregate(participants=aggregation_bits, proof=placeholder) elif self.signer_ids is not None: # Valid proof from wrong validators (participant mismatch). valid_proof = key_manager.sign_and_aggregate(self.signer_ids, attestation_data) - invalid_proof = TypeOneMultiSignature( + invalid_proof = SingleMessageAggregate( participants=aggregation_bits, proof=valid_proof.proof ) else: - invalid_proof = TypeOneMultiSignature(participants=aggregation_bits, proof=placeholder) + invalid_proof = SingleMessageAggregate(participants=aggregation_bits, proof=placeholder) # Append invalid attestation to the block body. block.body.attestations = AggregatedAttestations( diff --git a/packages/testing/src/consensus_testing/test_types/block_spec.py b/packages/testing/src/consensus_testing/test_types/block_spec.py index db810a3d..269694e4 100644 --- a/packages/testing/src/consensus_testing/test_types/block_spec.py +++ b/packages/testing/src/consensus_testing/test_types/block_spec.py @@ -17,11 +17,11 @@ AttestationData, Block, BlockBody, + MultiMessageAggregate, SignedAttestation, SignedBlock, + SingleMessageAggregate, State, - TypeOneMultiSignature, - TypeTwoMultiSignature, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.forks.lstar.store import Store @@ -239,29 +239,30 @@ def build_attestations( def _sign_block( self, final_block: Block, - attestation_proofs: list[TypeOneMultiSignature], + attestation_proofs: list[SingleMessageAggregate], proposer_index: ValidatorIndex, key_manager: XmssKeyManager, state: State, ) -> SignedBlock: """Sign a block and assemble the final SignedBlock with the merged proof. - Builds a Type-1 wrapping the proposer's XMSS signature, then merges - that with the per-attestation Type-1 proofs into a single Type-2 proof - and SSZ-encodes it onto the envelope. Consumers of this filler feed - the block through spec.on_block / verify_signatures, which decodes - the proof and verifies it, so an honest merged proof is required. + Builds a single-message aggregate wrapping the proposer's XMSS + signature, then merges that with the per-attestation single-message + aggregate proofs into a single multi-message aggregate proof and + stores it on the envelope. Consumers of this filler feed the block + through spec.on_block / verify_signatures, which decodes the proof + and verifies it, so an honest merged proof is required. When valid_signature is False, the proposer signature is a dummy XMSS one and the binding-driven aggregation would reject it before - verify_signatures ever runs. The Type-2 envelope is then assembled + verify_signatures ever runs. The multi-message aggregate envelope is then assembled directly from the info entries with empty proof bytes — that decodes structurally and lets verify_signatures reach (and reject at) the verify_type_2 call, which is the contract the test exercises. Args: final_block: The unsigned block. - attestation_proofs: Per-attestation Type-1 proofs (parallel to + attestation_proofs: Per-attestation single-message aggregate proofs (parallel to final_block.body.attestations). proposer_index: Which validator proposes this block. key_manager: XMSS key manager for signing. @@ -277,7 +278,7 @@ def _sign_block( # The binding rejects placeholder bytes; if anything in the merged # input is a dummy (invalid proposer sig or a build_invalid_proof # attestation), bypass aggregate_type_2 entirely and assemble the - # Type-2 envelope by hand. The result still SSZ-decodes so + # multi-message aggregate envelope by hand. The result still SSZ-decodes so # verify_signatures reaches verify_type_2 for the rejection. any_placeholder_attestation = any(not proof.proof.data for proof in attestation_proofs) use_placeholder = not self.valid_signature or any_placeholder_attestation @@ -288,7 +289,7 @@ def _sign_block( self.slot, block_root, ) - proposer_type_1 = TypeOneMultiSignature.aggregate( + proposer_single_message_aggregate = SingleMessageAggregate.aggregate( children=[], raw_xmss=[(proposer_index, proposer_pubkey, proposer_signature)], message=block_root, @@ -304,12 +305,12 @@ def _sign_block( ] public_keys_per_part.append([proposer_pubkey]) - proof = TypeTwoMultiSignature.aggregate( - [*attestation_proofs, proposer_type_1], + proof = MultiMessageAggregate.aggregate( + [*attestation_proofs, proposer_single_message_aggregate], public_keys_per_part=public_keys_per_part, ) else: - proof = TypeTwoMultiSignature(proof=ByteList512KiB(data=b"")) + proof = MultiMessageAggregate(proof=ByteList512KiB(data=b"")) return SignedBlock( block=final_block, @@ -433,11 +434,11 @@ def build_signed_block_with_store( Simulates what a real node does when proposing a block. Replays the gossip, aggregation, and proposal pipeline through the Store. - Returns a Store enriched with the aggregated Type-1 payloads built + Returns a Store enriched with the aggregated single-message aggregate payloads built during the simulated pipeline. The caller can persist these so future block builds can re-aggregate the same attestations rather than reconstructing them from on-chain block bodies (which would require - splitting the block-level Type-2 proof — a heavy and, in the test + splitting the block-level multi-message aggregate proof — a heavy and, in the test recursive-aggregation mode, unreliable operation). Other fields of the original Store (gossip signatures, time, head, etc.) are preserved so the simulated build does not consume state the caller @@ -468,7 +469,7 @@ def build_signed_block_with_store( # Preserve the caller's Store so unrelated fields (gossip signatures, # head, finalization checkpoints, time) survive the simulated pipeline. - # Only the freshly aggregated Type-1 payloads merge back at the end. + # Only the freshly aggregated single-message aggregate payloads merge back at the end. caller_store = store store = copy.deepcopy(store) @@ -509,7 +510,7 @@ def build_signed_block_with_store( # Trigger Store aggregation to merge gossip signatures into known payloads. # Aggregation runs on a local clone: gossip pools mutate here, but the # caller's gossip-signature view must not be consumed by this simulated - # build. Only the freshly aggregated Type-1 payloads propagate back. + # build. Only the freshly aggregated single-message aggregate payloads propagate back. aggregation_store, _ = spec.aggregate(store) merged_store = spec.accept_new_attestations(aggregation_store) diff --git a/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py b/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py index 3547cd1e..5046c4c8 100644 --- a/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py +++ b/packages/testing/src/consensus_testing/test_types/gossip_aggregated_attestation_spec.py @@ -8,8 +8,8 @@ AttestationData, Block, SignedAggregatedAttestation, + SingleMessageAggregate, State, - TypeOneMultiSignature, ) from lean_spec.spec.ssz import ByteList512KiB, Bytes32 @@ -185,7 +185,7 @@ def build_signed( # Exercises signature verification rejection. if not self.valid_signature: placeholder = ByteList512KiB(data=b"\x00" * 32) - proof = TypeOneMultiSignature( + proof = SingleMessageAggregate( participants=ValidatorIndices(data=validator_ids).to_aggregation_bits(), proof=placeholder, ) @@ -202,7 +202,7 @@ def build_signed( # but the claimed participants no longer match. # The store must detect and reject this inconsistency. if self.signer_ids and self.signer_ids != self.validator_ids: - proof = TypeOneMultiSignature( + proof = SingleMessageAggregate( participants=ValidatorIndices(data=validator_ids).to_aggregation_bits(), proof=proof.proof, ) diff --git a/src/lean_spec/node/sync/service.py b/src/lean_spec/node/sync/service.py index 6a899643..5c9058b2 100644 --- a/src/lean_spec/node/sync/service.py +++ b/src/lean_spec/node/sync/service.py @@ -30,7 +30,7 @@ ) from lean_spec.spec.forks.lstar.containers import ( AggregationError, - TypeOneMultiSignature, + SingleMessageAggregate, ) from lean_spec.spec.ssz import Bytes32 @@ -515,17 +515,18 @@ def _deconstruct_block_into_store( On block import we already trust the block-attestation participant bitfields via spec on_block signature verification. The block carries - one merged Type-2 proof binding every attestation in its body. + one merged multi-message aggregate proof binding every attestation in its body. For each block attestation that covers validators not already held in the given store: - 1. Extract that data's Type-1 proof out of the block's Type-2 proof. - 2. Merge it with all local partial Type-1 proofs for the same data - into one Type-1 proof whose participant bits are the union. + 1. Extract that data's single-message aggregate proof + out of the block's multi-message aggregate proof. + 2. Merge it with all local partial single-message aggregate proofs for the same data + into one single-message aggregate proof whose participant bits are the union. 3. Write the combined proof into the pending pool. - If the data was never seen locally, the extracted Type-1 is used + If the data was never seen locally, the extracted single-message aggregate is used as-is. Runs for every node, including non-validators, so the per-attestation @@ -541,7 +542,7 @@ def _deconstruct_block_into_store( if not block_attestations: return store, [] - # The Type-2 proof was built against the parent state's validator set. + # The multi-message aggregate proof was built against the parent state's validator set. # Without it we cannot resolve the pubkey layout the proof was bound to. parent_state = store.states.get(block.block.parent_root) if parent_state is None: @@ -564,10 +565,10 @@ def _deconstruct_block_into_store( [validators[block.block.proposer_index].get_proposal_pubkey()] ) - # Index local partial Type-1 proofs by AttestationData root. Equivalent + # Index local partial single-message aggregate proofs by AttestationData root. Equivalent # AttestationData instances from different code paths may not share a # dict key, so match on the hash tree root instead. - local_proofs_by_root: dict[Bytes32, list[TypeOneMultiSignature]] = {} + local_proofs_by_root: dict[Bytes32, list[SingleMessageAggregate]] = {} for data, proofs in store.latest_new_aggregated_payloads.items(): local_proofs_by_root.setdefault(hash_tree_root(data), []).extend(proofs) @@ -575,7 +576,7 @@ def _deconstruct_block_into_store( # The combined proof is retained locally so the block-sourced # aggregate survives without depending on gossip loopback. Shallow # copy the dict and its inner sets to preserve store immutability. - new_payloads: dict[AttestationData, set[TypeOneMultiSignature]] = { + new_payloads: dict[AttestationData, set[SingleMessageAggregate]] = { k: set(v) for k, v in store.latest_new_aggregated_payloads.items() } aggregates: list[SignedAggregatedAttestation] = [] @@ -605,14 +606,14 @@ def _deconstruct_block_into_store( try: # The split takes the bits from the block attestation this # component binds, since the Rust binding does not return them. - block_t1 = block.proof.split_by_msg( + block_single_message_aggregate = block.proof.split_by_msg( message=data_root, public_keys_per_message=public_keys_per_message, participants=att.aggregation_bits, ) if local_proofs: - combined = TypeOneMultiSignature.aggregate( + combined = SingleMessageAggregate.aggregate( children=[ ( child, @@ -621,7 +622,7 @@ def _deconstruct_block_into_store( for vid in child.participants.to_validator_indices() ], ) - for child in (block_t1, *local_proofs) + for child in (block_single_message_aggregate, *local_proofs) ], raw_xmss=[], message=data_root, @@ -629,7 +630,7 @@ def _deconstruct_block_into_store( ) else: # Data unseen locally: nothing to merge, use as-is. - combined = block_t1 + combined = block_single_message_aggregate except (AggregationError, AssertionError, KeyError, ValueError) as exc: logger.debug("Post-block re-aggregation failed for %s: %s", data_root, exc) continue diff --git a/src/lean_spec/node/validator/service.py b/src/lean_spec/node/validator/service.py index 2d3556fe..a2c26015 100644 --- a/src/lean_spec/node/validator/service.py +++ b/src/lean_spec/node/validator/service.py @@ -51,7 +51,7 @@ Slot, ValidatorIndex, ) -from lean_spec.spec.forks.lstar.containers import TypeOneMultiSignature, TypeTwoMultiSignature +from lean_spec.spec.forks.lstar.containers import MultiMessageAggregate, SingleMessageAggregate from lean_spec.spec.ssz import Bytes32, Uint64 from .constants import HYSTERESIS_BAND, NETWORK_STALL_THRESHOLD, SYNC_LAG_THRESHOLD @@ -402,20 +402,21 @@ def _sign_block( self, block: Block, validator_index: ValidatorIndex, - attestation_proofs: list[TypeOneMultiSignature], + attestation_proofs: list[SingleMessageAggregate], ) -> SignedBlock: """ Sign a block and wrap it for publishing. - Signs the block root with the proposer's proposal key, wraps the - signature into a singleton Type-1 proof, and merges that with the - per-attestation Type-1 proofs into a single Type-2 proof. The - merged proof is SSZ-encoded and stored on SignedBlock.proof. + Signs the block root with the proposer's proposal key. + Wraps the signature into a singleton single-message aggregate proof. + Merges that with the per-attestation single-message aggregate proofs + into a single multi-message aggregate proof. + The merged proof is stored on the block envelope. Args: block: The block to sign. validator_index: Index of the proposing validator. - attestation_proofs: Per-AttestationData Type-1 proofs included in + attestation_proofs: Per-AttestationData single-message aggregate proofs included in the block body, parallel to block.body.attestations. Returns: @@ -448,17 +449,17 @@ def _sign_block( raise ValueError(f"Validator {validator_index} not found in state validators") proposer_pubkey = validators[validator_index].get_proposal_pubkey() - # Wrap the proposer's raw XMSS signature into a singleton Type-1. + # Wrap the proposer's raw XMSS signature into a singleton single-message aggregate. # The single fresh entry carries the proposer index alongside its key and signature. - proposer_type_1 = TypeOneMultiSignature.aggregate( + proposer_single_message_aggregate = SingleMessageAggregate.aggregate( children=[], raw_xmss=[(validator_index, proposer_pubkey, proposer_signature)], message=block_root, slot=block.slot, ) - # Merge the per-attestation proofs and the proposer Type-1 into one - # Type-2 proof. Order matters: verify_signatures expects the proposer + # Merge the per-attestation proofs and the proposer single-message aggregate into one + # multi-message aggregate proof. Order matters: verify_signatures expects the proposer # entry to be last, parallel to block.body.attestations + 1. # The pubkey lookup below indexes the active validator set, so each # participant must fall within it. @@ -478,8 +479,8 @@ def _sign_block( public_keys_per_part.append(part_pubkeys) public_keys_per_part.append([proposer_pubkey]) - merged = TypeTwoMultiSignature.aggregate( - [*attestation_proofs, proposer_type_1], + merged = MultiMessageAggregate.aggregate( + [*attestation_proofs, proposer_single_message_aggregate], public_keys_per_part=public_keys_per_part, ) diff --git a/src/lean_spec/spec/forks/lstar/aggregation_select.py b/src/lean_spec/spec/forks/lstar/aggregation_select.py index 6c96ba63..9f50a563 100644 --- a/src/lean_spec/spec/forks/lstar/aggregation_select.py +++ b/src/lean_spec/spec/forks/lstar/aggregation_select.py @@ -1,13 +1,13 @@ """Greedy proof selection for lstar block production.""" -from lean_spec.spec.forks.lstar.containers import TypeOneMultiSignature, ValidatorIndex +from lean_spec.spec.forks.lstar.containers import SingleMessageAggregate, ValidatorIndex def select_greedily( - *proof_sets: set[TypeOneMultiSignature] | None, -) -> tuple[list[TypeOneMultiSignature], set[ValidatorIndex]]: + *proof_sets: set[SingleMessageAggregate] | None, +) -> tuple[list[SingleMessageAggregate], set[ValidatorIndex]]: """ - Greedy set-cover over Type-1 proofs maximizing validator coverage. + Greedy set-cover over single-message aggregate proofs maximizing validator coverage. Iterates the proof sets in order, repeatedly picking the proof with the most uncovered validators until no further coverage is possible. @@ -18,13 +18,13 @@ def select_greedily( inner max key, so the loop runs in O(P * V) instead of O(P^2 * V). Args: - *proof_sets: One or more sets of Type-1 proofs, ordered by priority. + *proof_sets: One or more sets of single-message aggregate proofs, ordered by priority. None entries are skipped. Returns: The chosen proofs and the union of validator indices they cover. """ - selected: list[TypeOneMultiSignature] = [] + selected: list[SingleMessageAggregate] = [] covered: set[ValidatorIndex] = set() for proofs in proof_sets: @@ -33,7 +33,7 @@ def select_greedily( # Materialize each proof's validator index set once. # The greedy loop below would otherwise recompute it on every comparison. - coverage_of: dict[TypeOneMultiSignature, set[ValidatorIndex]] = { + coverage_of: dict[SingleMessageAggregate, set[ValidatorIndex]] = { p: set(p.participants.to_validator_indices()) for p in proofs } remaining = list(proofs) diff --git a/src/lean_spec/spec/forks/lstar/containers.py b/src/lean_spec/spec/forks/lstar/containers.py index 1c581245..3854a65f 100644 --- a/src/lean_spec/spec/forks/lstar/containers.py +++ b/src/lean_spec/spec/forks/lstar/containers.py @@ -135,7 +135,7 @@ class AggregationError(Exception): """Raised when aggregation, merging, splitting, or verification fails.""" -class TypeOneMultiSignature(Container): +class SingleMessageAggregate(Container): """Single-message proof aggregating signatures from many validators. Every validator signs the same message for the same slot. @@ -155,11 +155,11 @@ class TypeOneMultiSignature(Container): @classmethod def aggregate( cls, - children: list[tuple["TypeOneMultiSignature", list[PublicKey]]], + children: list[tuple["SingleMessageAggregate", list[PublicKey]]], raw_xmss: list[tuple[ValidatorIndex, PublicKey, Signature]], message: Bytes32, slot: Slot, - ) -> "TypeOneMultiSignature": + ) -> "SingleMessageAggregate": """Fold fresh signatures and child proofs into one single-message proof. # Overview @@ -210,7 +210,7 @@ def aggregate( # Phase 3: hand off to the Rust prover. # The mode argument routes the call to the matching backend bytecode. try: - _, type1_wire = aggregate_type_1( + _, single_message_aggregate_wire = aggregate_type_1( raw_pubkeys_ssz, raw_signatures_ssz, bytes(message), @@ -222,7 +222,10 @@ def aggregate( except Exception as exc: raise AggregationError(str(exc)) from exc - return cls(participants=participants, proof=ByteList512KiB(data=type1_wire)) + return cls( + participants=participants, + proof=ByteList512KiB(data=single_message_aggregate_wire), + ) def verify( self, @@ -230,7 +233,7 @@ def verify( message: Bytes32, slot: Slot, ) -> None: - """Verify this single-message Type-1 proof against a pubkey set. + """Verify this single-message single-message aggregate proof against a pubkey set. Args: public_keys: Pubkeys for the validators named by participants. @@ -247,7 +250,7 @@ def verify( expected = len(self.participants.to_validator_indices()) if len(public_keys) != expected: raise AggregationError( - f"Type-1 verify expected {expected} pubkeys for participants, " + f"single-message aggregate verify expected {expected} pubkeys for participants, " f"got {len(public_keys)}" ) @@ -262,33 +265,31 @@ def verify( mode=LEAN_ENV, ) except Exception as exc: - raise AggregationError(f"Type-1 verification failed: {exc}") from exc + raise AggregationError(f"single-message aggregate verification failed: {exc}") from exc def __hash__(self) -> int: """Content-deterministic hash via SSZ encoding.""" return hash(self.encode_bytes()) -class TypeTwoMultiSignature(Container): +class MultiMessageAggregate(Container): """Merged proof covering many distinct messages. Each component is a single-message proof over its own message. Merging binds the components into one proof the block can carry whole. - - A signed block stores this proof as a single serialized blob. """ model_config = Container.model_config | {"frozen": True} proof: ByteList512KiB - """Compact no-pubkeys serialized Type-2 proof bytes.""" + """Compact no-pubkeys serialized multi-message aggregate proof bytes.""" @classmethod def aggregate( cls, - parts: list[TypeOneMultiSignature], + parts: list[SingleMessageAggregate], public_keys_per_part: list[list[PublicKey]], - ) -> "TypeTwoMultiSignature": + ) -> "MultiMessageAggregate": """Merge several single-message proofs over distinct messages into one. # Why the public keys are passed in @@ -309,37 +310,49 @@ def aggregate( with its participant count, or the prover rejects the inputs. """ if not parts: - raise AggregationError("Type-2 aggregate requires at least one Type-1 input") + raise AggregationError( + "multi-message aggregate requires at least one single-message aggregate input" + ) # Each component carries the public keys named by its bitfield, in the same order. # # A miscount would otherwise fail deep in the prover with an opaque error. - type1_entries: list[tuple[list[bytes], bytes]] = [] + single_message_aggregate_entries: list[tuple[list[bytes], bytes]] = [] for idx, (part, pubkeys) in enumerate(zip(parts, public_keys_per_part, strict=True)): expected = len(part.participants.to_validator_indices()) if len(pubkeys) != expected: raise AggregationError( - f"Type-2 aggregate entry {idx} expected {expected} pubkeys, got {len(pubkeys)}" + f"multi-message aggregate entry {idx} " + f"expected {expected} pubkeys, got {len(pubkeys)}" ) - type1_entries.append(([pk.encode_bytes() for pk in pubkeys], bytes(part.proof.data))) + single_message_aggregate_entries.append( + ([pk.encode_bytes() for pk in pubkeys], bytes(part.proof.data)) + ) # Hand the per-component keys and proof bytes to the Rust prover. # # The mode argument selects the matching backend bytecode. try: - _, type2_wire = merge_many_type_1(type1_entries, LOG_INV_RATE, mode=LEAN_ENV) + _, multi_message_aggregate_wire = merge_many_type_1( + single_message_aggregate_entries, + LOG_INV_RATE, + mode=LEAN_ENV, + ) except Exception as exc: raise AggregationError(str(exc)) from exc - return cls(proof=ByteList512KiB(data=type2_wire)) + return cls(proof=ByteList512KiB(data=multi_message_aggregate_wire)) def split_by_msg( self, message: Bytes32, public_keys_per_message: list[list[PublicKey]], participants: AggregationBits, - ) -> TypeOneMultiSignature: - """Recover the Type-1 proof bound to one message from this Type-2 merge. + ) -> SingleMessageAggregate: + """Recover the single-message aggregate proof bound to one message. + + Splits this multi-message aggregate to extract the component + bound to the given message. # Why the layout and participants are passed in @@ -348,12 +361,12 @@ def split_by_msg( - The caller supplies both, drawn from the block attestation this component binds. Args: - message: Message that selects the Type-1 component. - public_keys_per_message: Pubkey layout this Type-2 was built with. + message: Message that selects the single-message aggregate component. + public_keys_per_message: Pubkey layout this multi-message aggregate was built with. participants: Bitfield naming the validators of the recovered component. Returns: - The Type-1 proof bound to the message. + The single-message aggregate proof bound to the message. Raises: AggregationError: When the Rust binding rejects the split. @@ -367,7 +380,7 @@ def split_by_msg( # # The mode argument selects the matching backend bytecode. try: - _, type1_wire = split_type_2_by_msg( + _, single_message_aggregate_wire = split_type_2_by_msg( pub_keys_per_component_ssz, bytes(self.proof.data), bytes(message), @@ -375,11 +388,11 @@ def split_by_msg( mode=LEAN_ENV, ) except Exception as exc: - raise AggregationError(f"Type-2 split failed: {exc}") from exc + raise AggregationError(f"multi-message aggregate split failed: {exc}") from exc - return TypeOneMultiSignature( + return SingleMessageAggregate( participants=participants, - proof=ByteList512KiB(data=type1_wire), + proof=ByteList512KiB(data=single_message_aggregate_wire), ) def verify( @@ -407,7 +420,8 @@ def verify( # A length mismatch would leave components unbound or misaligned. if len(messages) != len(public_keys_per_message): raise AggregationError( - f"Type-2 verify expected {len(public_keys_per_message)} message bindings, " + f"multi-message aggregate verify expected " + f"{len(public_keys_per_message)} message bindings, " f"got {len(messages)}" ) @@ -428,7 +442,7 @@ def verify( mode=LEAN_ENV, ) except Exception as exc: - raise AggregationError(f"Type-2 verification failed: {exc}") from exc + raise AggregationError(f"multi-message aggregate verification failed: {exc}") from exc def __hash__(self) -> int: """Content-deterministic hash via SSZ encoding.""" @@ -565,7 +579,7 @@ class SignedAggregatedAttestation(Container): data: AttestationData """Combined attestation data similar to the beacon chain format.""" - proof: TypeOneMultiSignature + proof: SingleMessageAggregate """Aggregated single-message proof covering all participating validators.""" @@ -628,7 +642,7 @@ class Block(Container): class SignedBlock(Container): """Envelope carrying a block with a single aggregated proof for all signatures. - The proof is a Type-2 multi-message proof. + The proof is a multi-message aggregate multi-message proof. It binds every attestation in the body plus the proposer's signature over the block root. """ @@ -636,7 +650,7 @@ class SignedBlock(Container): block: Block """The block being signed.""" - proof: TypeTwoMultiSignature + proof: MultiMessageAggregate """Single full-block proof covering attestations and the proposer signature.""" diff --git a/src/lean_spec/spec/forks/lstar/spec.py b/src/lean_spec/spec/forks/lstar/spec.py index 6b3ce6fc..9631efe3 100644 --- a/src/lean_spec/spec/forks/lstar/spec.py +++ b/src/lean_spec/spec/forks/lstar/spec.py @@ -39,9 +39,9 @@ SignedAggregatedAttestation, SignedAttestation, SignedBlock, + SingleMessageAggregate, Slot, State, - TypeOneMultiSignature, ValidatorIndex, Validators, ) @@ -621,8 +621,8 @@ def build_block( proposer_index: ValidatorIndex, parent_root: Bytes32, known_block_roots: AbstractSet[Bytes32], - aggregated_payloads: dict[AttestationData, set[TypeOneMultiSignature]] | None = None, - ) -> tuple[Block, State, list[AggregatedAttestation], list[TypeOneMultiSignature]]: + aggregated_payloads: dict[AttestationData, set[SingleMessageAggregate]] | None = None, + ) -> tuple[Block, State, list[AggregatedAttestation], list[SingleMessageAggregate]]: """ Build a valid block on top of the given pre-state. @@ -634,7 +634,7 @@ def build_block( repeats with the new checkpoint. """ aggregated_attestations: list[AggregatedAttestation] = [] - aggregated_signatures: list[TypeOneMultiSignature] = [] + aggregated_signatures: list[SingleMessageAggregate] = [] if aggregated_payloads: # Fixed-point loop: find attestation_data entries matching the current @@ -774,7 +774,7 @@ def build_block( # During the fixed-point loop above, multiple proofs may have been # selected for the same AttestationData across iterations. Group them # and merge each group into a single recursive proof. - proof_groups: dict[AttestationData, list[TypeOneMultiSignature]] = {} + proof_groups: dict[AttestationData, list[SingleMessageAggregate]] = {} for att, sig in zip(aggregated_attestations, aggregated_signatures, strict=True): proof_groups.setdefault(att.data, []).append(sig) @@ -797,7 +797,7 @@ def build_block( ) for proof in proofs ] - sig = TypeOneMultiSignature.aggregate( + sig = SingleMessageAggregate.aggregate( children=children, raw_xmss=[], message=hash_tree_root(att_data), @@ -833,9 +833,9 @@ def verify_signatures( validators: Validators, ) -> bool: """ - Verify the merged Type-2 proof carried by a signed block. + Verify the merged multi-message aggregate proof carried by a signed block. - The block envelope holds one Type-2 proof binding + The block envelope holds one multi-message aggregate proof binding every body attestation plus the proposer's signature over the block root. @@ -1158,7 +1158,7 @@ def on_gossip_aggregated_attestation( # Prepare public keys for verification public_keys = [validators[vid].get_attestation_pubkey() for vid in validator_ids] - # Verify the Type-1 single-message aggregated proof. + # Verify the single-message aggregate single-message aggregated proof. try: proof.verify( public_keys=public_keys, @@ -1258,7 +1258,7 @@ def on_block( # # Consequence: a block's own attestations contribute zero weight # to the head computation triggered by this import. - # Recovered Type-1 proofs land in the new pool and migrate to + # Recovered single-message aggregate proofs land in the new pool and migrate to # the known pool at the next acceptance tick. # Head weight from block-imported votes is therefore deferred # by up to one slot. @@ -1279,7 +1279,7 @@ def on_block( def extract_attestations_from_aggregated_payloads( self, store: LstarStore, - aggregated_payloads: dict[AttestationData, set[TypeOneMultiSignature]], + aggregated_payloads: dict[AttestationData, set[SingleMessageAggregate]], ) -> dict[ValidatorIndex, AttestationData]: """Extract attestations from aggregated payloads. @@ -1618,7 +1618,7 @@ def aggregate(self, store: LstarStore) -> tuple[LstarStore, list[SignedAggregate # Hand everything to the XMSS subspec. # Each fresh entry already carries its validator index alongside its key and signature. # Out comes a single proof covering all selected validators. - proof = TypeOneMultiSignature.aggregate( + proof = SingleMessageAggregate.aggregate( children=children, raw_xmss=raw_entries, message=hash_tree_root(data), @@ -1781,8 +1781,11 @@ def produce_block_with_signatures( store: LstarStore, slot: Slot, validator_index: ValidatorIndex, - ) -> tuple[LstarStore, Block, list[TypeOneMultiSignature]]: - """Produce a block and its per-attestation Type-1 proofs for the target slot. + ) -> tuple[LstarStore, Block, list[SingleMessageAggregate]]: + """Produce a block for the target slot. + + Returns the block alongside its per-attestation single-message + aggregate proofs. Block production proceeds in four stages: 1. Retrieve the current chain head as the parent block @@ -1793,10 +1796,10 @@ def produce_block_with_signatures( The block builder uses a fixed-point algorithm to collect attestations. Each iteration may update the justified checkpoint. - Returns the per-attestation Type-1 proofs unmerged. The validator + Returns the per-attestation single-message aggregate proofs unmerged. The validator service signs the block root with the proposal key, wraps that into - a singleton Type-1, and merges all of them into the block-level - Type-2 proof carried by SignedBlock.proof. + a singleton single-message aggregate, and merges all of them into the block-level + multi-message aggregate proof carried by SignedBlock.proof. Raises: AssertionError: If validator is not the proposer for this slot, diff --git a/src/lean_spec/spec/forks/lstar/store.py b/src/lean_spec/spec/forks/lstar/store.py index e244a54e..45778d1f 100644 --- a/src/lean_spec/spec/forks/lstar/store.py +++ b/src/lean_spec/spec/forks/lstar/store.py @@ -17,7 +17,7 @@ AttestationData, Checkpoint, Config, - TypeOneMultiSignature, + SingleMessageAggregate, ValidatorIndex, ) from lean_spec.spec.ssz import Bytes32 @@ -124,7 +124,7 @@ class Store[StateT: Container, BlockT: Container](StrictBaseModel): Keyed by AttestationData. """ - latest_new_aggregated_payloads: dict[AttestationData, set[TypeOneMultiSignature]] = Field( + latest_new_aggregated_payloads: dict[AttestationData, set[SingleMessageAggregate]] = Field( default_factory=dict ) """ @@ -134,12 +134,12 @@ class Store[StateT: Container, BlockT: Container](StrictBaseModel): They migrate to known payloads via interval ticks. Populated from gossip aggregated attestations. Block import does not feed individual proofs into this map directly. - The block-level proof is a merged Type-2 blob verified as a whole. - On gossip-block import, any validator deconstructs that Type-2 into + The block-level proof is a merged multi-message aggregate blob verified as a whole. + On gossip-block import, any validator deconstructs that multi-message aggregate into per-message proofs, writes them back here, and gossips the aggregate. """ - latest_known_aggregated_payloads: dict[AttestationData, set[TypeOneMultiSignature]] = Field( + latest_known_aggregated_payloads: dict[AttestationData, set[SingleMessageAggregate]] = Field( default_factory=dict ) """ diff --git a/tests/consensus/lstar/fc/test_block_production.py b/tests/consensus/lstar/fc/test_block_production.py index bf5721c2..ef2310ec 100644 --- a/tests/consensus/lstar/fc/test_block_production.py +++ b/tests/consensus/lstar/fc/test_block_production.py @@ -273,7 +273,7 @@ def test_produce_block_enforces_max_attestations_data_limit( The builder sorts entries by target.slot and processes them in order. After selecting MAX_ATTESTATIONS_DATA entries it breaks, excluding the entries with the highest target slots. The proposer signature occupies - the remaining slot in the Type-2 proof envelope. + the remaining slot in the multi-message aggregate proof envelope. Expected post-state ------------------- diff --git a/tests/consensus/lstar/ssz/test_consensus_containers.py b/tests/consensus/lstar/ssz/test_consensus_containers.py index 923e30ed..d7c5700f 100644 --- a/tests/consensus/lstar/ssz/test_consensus_containers.py +++ b/tests/consensus/lstar/ssz/test_consensus_containers.py @@ -19,11 +19,11 @@ JustificationRoots, JustificationValidators, JustifiedSlots, + MultiMessageAggregate, SignedAggregatedAttestation, SignedAttestation, SignedBlock, - TypeOneMultiSignature, - TypeTwoMultiSignature, + SingleMessageAggregate, Validator, Validators, ) @@ -259,7 +259,7 @@ def test_signed_block_minimal(ssz: SSZTestFiller) -> None: type_name="SignedBlock", value=SignedBlock( block=block, - proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b"")), + proof=MultiMessageAggregate(proof=ByteList512KiB(data=b"")), ), ) @@ -277,7 +277,7 @@ def test_signed_block_with_proof_bytes(ssz: SSZTestFiller) -> None: type_name="SignedBlock", value=SignedBlock( block=block, - proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b"\xde\xad\xbe\xef")), + proof=MultiMessageAggregate(proof=ByteList512KiB(data=b"\xde\xad\xbe\xef")), ), ) @@ -422,7 +422,7 @@ def test_signed_aggregated_attestation_minimal(ssz: SSZTestFiller) -> None: type_name="SignedAggregatedAttestation", value=SignedAggregatedAttestation( data=data, - proof=TypeOneMultiSignature( + proof=SingleMessageAggregate( participants=AggregationBits(data=[Boolean(True)]), proof=ByteList512KiB(data=b""), ), @@ -438,7 +438,7 @@ def test_signed_aggregated_attestation_typical(ssz: SSZTestFiller) -> None: type_name="SignedAggregatedAttestation", value=SignedAggregatedAttestation( data=data, - proof=TypeOneMultiSignature( + proof=SingleMessageAggregate( participants=AggregationBits( data=[Boolean(True), Boolean(False), Boolean(True), Boolean(True)] ), diff --git a/tests/consensus/lstar/ssz/test_xmss_containers.py b/tests/consensus/lstar/ssz/test_xmss_containers.py index 5e9427e4..22d0107f 100644 --- a/tests/consensus/lstar/ssz/test_xmss_containers.py +++ b/tests/consensus/lstar/ssz/test_xmss_containers.py @@ -16,8 +16,8 @@ ) from lean_spec.spec.forks import AggregationBits, Slot, ValidatorIndex from lean_spec.spec.forks.lstar.containers import ( - TypeOneMultiSignature, - TypeTwoMultiSignature, + MultiMessageAggregate, + SingleMessageAggregate, ) from lean_spec.spec.ssz import Boolean, ByteList512KiB, Bytes32, Uint64 @@ -65,7 +65,7 @@ def test_signature_actual(ssz: SSZTestFiller) -> None: ssz(type_name="Signature", value=signature) -# --- TypeOneMultiSignature / TypeTwoMultiSignature --- +# --- SingleMessageAggregate / MultiMessageAggregate --- def _bits(participants: list[bool]) -> AggregationBits: @@ -73,35 +73,35 @@ def _bits(participants: list[bool]) -> AggregationBits: return AggregationBits(data=[Boolean(b) for b in participants]) -def test_type_one_multi_signature_empty(ssz: SSZTestFiller) -> None: - """SSZ roundtrip for a Type-1 proof with empty proof bytes.""" +def test_single_message_aggregate_empty(ssz: SSZTestFiller) -> None: + """SSZ roundtrip for a single-message aggregate proof with empty proof bytes.""" ssz( - type_name="TypeOneMultiSignature", - value=TypeOneMultiSignature( + type_name="SingleMessageAggregate", + value=SingleMessageAggregate( participants=_bits([True]), proof=ByteList512KiB(data=b""), ), ) -def test_type_one_multi_signature_with_proof(ssz: SSZTestFiller) -> None: - """SSZ roundtrip for a Type-1 proof with non-empty proof bytes.""" +def test_single_message_aggregate_with_proof(ssz: SSZTestFiller) -> None: + """SSZ roundtrip for a single-message aggregate proof with non-empty proof bytes.""" wire = b"\xde\xad\xbe\xef" ssz( - type_name="TypeOneMultiSignature", - value=TypeOneMultiSignature( + type_name="SingleMessageAggregate", + value=SingleMessageAggregate( participants=_bits([True, False, True]), proof=ByteList512KiB(data=wire), ), ) -def test_type_two_multi_signature_roundtrip(ssz: SSZTestFiller) -> None: - """SSZ roundtrip for a Type-2 proof envelope.""" +def test_multi_message_aggregate_roundtrip(ssz: SSZTestFiller) -> None: + """SSZ roundtrip for a multi-message aggregate proof envelope.""" wire = b"\x01\x02\x03" ssz( - type_name="TypeTwoMultiSignature", - value=TypeTwoMultiSignature(proof=ByteList512KiB(data=wire)), + type_name="MultiMessageAggregate", + value=MultiMessageAggregate(proof=ByteList512KiB(data=wire)), ) diff --git a/tests/consensus/lstar/verify_signatures/test_structural_rejections.py b/tests/consensus/lstar/verify_signatures/test_structural_rejections.py index f303a817..76a80efc 100644 --- a/tests/consensus/lstar/verify_signatures/test_structural_rejections.py +++ b/tests/consensus/lstar/verify_signatures/test_structural_rejections.py @@ -33,7 +33,7 @@ def test_corrupt_proof_rejected( Expected Behavior ----------------- - Verification fails with AssertionError because the Type-2 envelope + Verification fails with AssertionError because the multi-message aggregate envelope cannot be decoded. Why This Matters diff --git a/tests/lean_spec/helpers/builders.py b/tests/lean_spec/helpers/builders.py index 4146e5d2..d99d1614 100644 --- a/tests/lean_spec/helpers/builders.py +++ b/tests/lean_spec/helpers/builders.py @@ -38,11 +38,11 @@ AttestationData, Block, BlockBody, + MultiMessageAggregate, SignedAggregatedAttestation, SignedAttestation, SignedBlock, - TypeOneMultiSignature, - TypeTwoMultiSignature, + SingleMessageAggregate, Validator, Validators, ) @@ -210,7 +210,7 @@ def make_signed_block( body=BlockBody(attestations=AggregatedAttestations(data=[])), ) - return SignedBlock(block=block, proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b""))) + return SignedBlock(block=block, proof=MultiMessageAggregate(proof=ByteList512KiB(data=b""))) def make_aggregated_attestation( @@ -393,8 +393,8 @@ def make_aggregated_proof( key_manager: XmssKeyManager, participants: list[ValidatorIndex], attestation_data: AttestationData, -) -> TypeOneMultiSignature: - """Create a valid Type-1 aggregated proof for the given participants.""" +) -> SingleMessageAggregate: + """Create a valid single-message aggregate aggregated proof for the given participants.""" data_root = hash_tree_root(attestation_data) raw_xmss = [ ( @@ -404,7 +404,7 @@ def make_aggregated_proof( ) for vid in participants ] - return TypeOneMultiSignature.aggregate( + return SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss, message=data_root, @@ -442,7 +442,7 @@ def make_signed_block_from_store( """Produce a signed block and advance the consumer store to accept it. Returns the updated store (with time advanced) and the signed block. - The merged Type-2 proof is built honestly because callers usually + The merged multi-message aggregate proof is built honestly because callers usually feed the result through spec.on_block, which decodes and verifies the proof. """ @@ -463,15 +463,15 @@ def make_signed_block_from_store( public_keys_per_part.append([proposer_pubkey]) proposer_signature = key_manager.sign_block_root(proposer_index, slot, block_root) - proposer_type_1 = TypeOneMultiSignature.aggregate( + proposer_single_message_aggregate = SingleMessageAggregate.aggregate( children=[], raw_xmss=[(proposer_index, proposer_pubkey, proposer_signature)], message=block_root, slot=slot, ) - merged = TypeTwoMultiSignature.aggregate( - [*attestation_proofs, proposer_type_1], + merged = MultiMessageAggregate.aggregate( + [*attestation_proofs, proposer_single_message_aggregate], public_keys_per_part=public_keys_per_part, ) diff --git a/tests/lean_spec/node/networking/client/test_reqresp_client_range.py b/tests/lean_spec/node/networking/client/test_reqresp_client_range.py index 4ab20e8c..0b2f856a 100644 --- a/tests/lean_spec/node/networking/client/test_reqresp_client_range.py +++ b/tests/lean_spec/node/networking/client/test_reqresp_client_range.py @@ -23,8 +23,8 @@ AggregatedAttestations, Block, BlockBody, + MultiMessageAggregate, SignedBlock, - TypeTwoMultiSignature, ) from lean_spec.spec.ssz import ByteList512KiB, Bytes32, Uint64 @@ -116,7 +116,7 @@ def empty_signed_block(slot: Slot, parent_root: Bytes32, state_seed: int) -> Sig state_root=Bytes32(bytes([state_seed]) * 32), body=BlockBody(attestations=AggregatedAttestations(data=[])), ) - return SignedBlock(block=block, proof=TypeTwoMultiSignature(proof=ByteList512KiB(data=b""))) + return SignedBlock(block=block, proof=MultiMessageAggregate(proof=ByteList512KiB(data=b""))) def build_chain(start_slot: int, count: int, root_seed: int = 0xAA) -> list[SignedBlock]: diff --git a/tests/lean_spec/node/sync/test_reaggregate_from_block.py b/tests/lean_spec/node/sync/test_reaggregate_from_block.py index ed92d538..e1edd3a3 100644 --- a/tests/lean_spec/node/sync/test_reaggregate_from_block.py +++ b/tests/lean_spec/node/sync/test_reaggregate_from_block.py @@ -1,8 +1,8 @@ -"""Tests for post-block Type-1 deconstruction in SyncService. +"""Tests for post-block single-message aggregate deconstruction in SyncService. Exercises `SyncService._deconstruct_block_into_store`: for every processed -block (gossip, head-sync, or backfilled), the merged Type-2 proof is split -into per-attestation Type-1 proofs, merged with locally held partials, and +block (gossip, head-sync, or backfilled), the merged multi-message aggregate proof is split +into per-attestation single-message aggregate proofs, merged with locally held partials, and written into the pending pool, replacing the partials it subsumes. Deconstruction only runs for an attestation when: @@ -54,7 +54,7 @@ def _setup( The chain block sits at slot 1. The returned signed block sits at slot 2 and carries one attestation whose target is the slot-1 block, ahead of the still-genesis justified checkpoint. The returned store holds the - slot-1 block and its state (the parent state the Type-2 pubkey layout + slot-1 block and its state (the parent state the multi-message aggregate pubkey layout is resolved against) with the justified checkpoint still at genesis. """ spec = LstarSpec() diff --git a/tests/lean_spec/node/validator/test_service.py b/tests/lean_spec/node/validator/test_service.py index 1d3acac6..c7581b67 100644 --- a/tests/lean_spec/node/validator/test_service.py +++ b/tests/lean_spec/node/validator/test_service.py @@ -24,7 +24,7 @@ Block, SignedAttestation, SignedBlock, - TypeOneMultiSignature, + SingleMessageAggregate, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import Bytes32, Uint64 @@ -1063,7 +1063,7 @@ async def test_block_includes_pending_attestations( signatures.append(sig) public_keys.append(key_manager[vid].attestation_keypair.public_key) - proof = TypeOneMultiSignature.aggregate( + proof = SingleMessageAggregate.aggregate( children=[], raw_xmss=list(zip(participants, public_keys, signatures, strict=True)), message=data_root, diff --git a/tests/lean_spec/spec/crypto/xmss/test_aggregation.py b/tests/lean_spec/spec/crypto/xmss/test_aggregation.py index e86f65ac..acac1e6d 100644 --- a/tests/lean_spec/spec/crypto/xmss/test_aggregation.py +++ b/tests/lean_spec/spec/crypto/xmss/test_aggregation.py @@ -9,8 +9,8 @@ from lean_spec.spec.forks import Checkpoint, Slot, ValidatorIndex from lean_spec.spec.forks.lstar.containers import ( AggregationError, - TypeOneMultiSignature, - TypeTwoMultiSignature, + MultiMessageAggregate, + SingleMessageAggregate, ) from lean_spec.spec.ssz import ByteList512KiB from tests.lean_spec.helpers import make_attestation_data_simple, make_bytes32 @@ -20,7 +20,7 @@ def _sign_and_aggregate( key_manager: XmssKeyManager, validator_ids: list[ValidatorIndex], att_data_args: tuple[Slot, int, int, Checkpoint], -) -> TypeOneMultiSignature: +) -> SingleMessageAggregate: """Sign attestation data with the given validators and aggregate.""" slot, head, target, source = att_data_args att_data = make_attestation_data_simple(slot, make_bytes32(head), make_bytes32(target), source) @@ -34,7 +34,7 @@ def _sign_and_aggregate( ) for vid in validator_ids ] - return TypeOneMultiSignature.aggregate( + return SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss, message=data_root, @@ -43,7 +43,7 @@ def _sign_and_aggregate( def test_aggregate_multiple_signatures(key_manager: XmssKeyManager) -> None: - """Multiple validators' signatures can be aggregated into a single Type-1 proof.""" + """Multiple validators' signatures aggregate into one single-message aggregate proof.""" source = Checkpoint(root=make_bytes32(10), slot=Slot(0)) att_data = make_attestation_data_simple(Slot(2), make_bytes32(11), make_bytes32(12), source) vids = [ValidatorIndex(i) for i in range(4)] @@ -57,7 +57,7 @@ def test_aggregate_multiple_signatures(key_manager: XmssKeyManager) -> None: for vid in vids ] - proof = TypeOneMultiSignature.aggregate( + proof = SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss, message=hash_tree_root(att_data), @@ -92,7 +92,7 @@ def test_aggregate_children_with_raw_signatures(key_manager: XmssKeyManager) -> for vid in extra_vids ] - parent = TypeOneMultiSignature.aggregate( + parent = SingleMessageAggregate.aggregate( children=[ ( child, @@ -128,7 +128,7 @@ def test_aggregate_three_children(key_manager: XmssKeyManager) -> None: child_b_pks = [key_manager[ValidatorIndex(1)].attestation_keypair.public_key] child_c_pks = [key_manager[ValidatorIndex(2)].attestation_keypair.public_key] - parent = TypeOneMultiSignature.aggregate( + parent = SingleMessageAggregate.aggregate( children=[(child_a, child_a_pks), (child_b, child_b_pks), (child_c, child_c_pks)], raw_xmss=[], message=hash_tree_root(att_data), @@ -163,13 +163,13 @@ def test_aggregate_children_of_children(key_manager: XmssKeyManager) -> None: leaf_d_pks = [key_manager[ValidatorIndex(3)].attestation_keypair.public_key] # Level 1: two intermediate proofs. - mid_ab = TypeOneMultiSignature.aggregate( + mid_ab = SingleMessageAggregate.aggregate( children=[(leaf_a, leaf_a_pks), (leaf_b, leaf_b_pks)], raw_xmss=[], message=msg, slot=att_data.slot, ) - mid_cd = TypeOneMultiSignature.aggregate( + mid_cd = SingleMessageAggregate.aggregate( children=[(leaf_c, leaf_c_pks), (leaf_d, leaf_d_pks)], raw_xmss=[], message=msg, @@ -177,7 +177,7 @@ def test_aggregate_children_of_children(key_manager: XmssKeyManager) -> None: ) # Level 2: final root proof. - root = TypeOneMultiSignature.aggregate( + root = SingleMessageAggregate.aggregate( children=[(mid_ab, leaf_a_pks + leaf_b_pks), (mid_cd, leaf_c_pks + leaf_d_pks)], raw_xmss=[], message=msg, @@ -221,7 +221,7 @@ def test_aggregate_mixed_children_and_raw_multiple(key_manager: XmssKeyManager) for vid in extra_vids ] - proof = TypeOneMultiSignature.aggregate( + proof = SingleMessageAggregate.aggregate( children=[(child_a, child_a_pks), (child_b, child_b_pks)], raw_xmss=raw_xmss, message=msg, @@ -238,8 +238,10 @@ def test_aggregate_mixed_children_and_raw_multiple(key_manager: XmssKeyManager) ) -def test_type_one_verify_rejects_pubkey_count_mismatch(key_manager: XmssKeyManager) -> None: - """Type-1 verification refuses a pubkey set that does not match the bitfield.""" +def test_single_message_aggregate_verify_rejects_pubkey_count_mismatch( + key_manager: XmssKeyManager, +) -> None: + """Verification refuses a pubkey set whose size does not match the bitfield.""" source = Checkpoint(root=make_bytes32(160), slot=Slot(0)) att_args = (Slot(2), 161, 162, source) vids = [ValidatorIndex(0), ValidatorIndex(1)] @@ -249,12 +251,15 @@ def test_type_one_verify_rejects_pubkey_count_mismatch(key_manager: XmssKeyManag only_one = [key_manager[ValidatorIndex(0)].attestation_keypair.public_key] with pytest.raises( - AggregationError, match="Type-1 verify expected 2 pubkeys for participants, got 1" + AggregationError, + match="single-message aggregate verify expected 2 pubkeys for participants, got 1", ): proof.verify(public_keys=only_one, message=make_bytes32(161), slot=att_args[0]) -def test_type_two_split_by_msg_rejected_under_test_prover(key_manager: XmssKeyManager) -> None: +def test_multi_message_aggregate_split_by_msg_rejected_under_test_prover( + key_manager: XmssKeyManager, +) -> None: """Splitting a merged proof aborts under the reduced test-config prover. The split branch is functional only under the production prover. @@ -276,12 +281,12 @@ def test_type_two_split_by_msg_rejected_under_test_prover(key_manager: XmssKeyMa pubkeys_a = [key_manager[vid].attestation_keypair.public_key for vid in vids_a] pubkeys_b = [key_manager[vid].attestation_keypair.public_key for vid in vids_b] - merged = TypeTwoMultiSignature.aggregate( + merged = MultiMessageAggregate.aggregate( parts=[part_a, part_b], public_keys_per_part=[pubkeys_a, pubkeys_b], ) - with pytest.raises(AggregationError, match="Type-2 split failed"): + with pytest.raises(AggregationError, match="multi-message aggregate split failed"): merged.split_by_msg( message=hash_tree_root(att_data_a), public_keys_per_message=[pubkeys_a, pubkeys_b], @@ -332,7 +337,7 @@ def test_aggregate_corrupted_proof_fails_verification(key_manager: XmssKeyManage corrupted_bytes = bytearray(proof.proof.data) corrupted_bytes[10] ^= 0xFF corrupted_bytes[20] ^= 0xFF - proof = TypeOneMultiSignature( + proof = SingleMessageAggregate( participants=proof.participants, proof=ByteList512KiB(data=bytes(corrupted_bytes)), ) @@ -362,7 +367,7 @@ def test_aggregate_child_signed_different_message_fails(key_manager: XmssKeyMana # The binding rejects mismatching messages during recursive aggregation. with pytest.raises(AggregationError): - TypeOneMultiSignature.aggregate( + SingleMessageAggregate.aggregate( children=[(child_a, child_a_pks), (child_b, child_b_pks)], raw_xmss=[], message=hash_tree_root(att_data_b), @@ -370,13 +375,13 @@ def test_aggregate_child_signed_different_message_fails(key_manager: XmssKeyMana ) -def test_type_two_aggregate_rejects_empty_parts() -> None: - """Type-2 aggregation requires at least one Type-1 input.""" - with pytest.raises(AggregationError, match="at least one Type-1 input"): - TypeTwoMultiSignature.aggregate(parts=[], public_keys_per_part=[]) +def test_multi_message_aggregate_rejects_empty_parts() -> None: + """multi-message aggregate aggregation requires at least one single-message aggregate input.""" + with pytest.raises(AggregationError, match="at least one single-message aggregate input"): + MultiMessageAggregate.aggregate(parts=[], public_keys_per_part=[]) -def test_type_two_aggregate_rejects_mismatched_pubkey_layout( +def test_multi_message_aggregate_rejects_mismatched_pubkey_layout( key_manager: XmssKeyManager, ) -> None: """The per-part pubkey layout must match the participant count of each part.""" @@ -392,13 +397,13 @@ def test_type_two_aggregate_rejects_mismatched_pubkey_layout( wrong_layout = [[key_manager[ValidatorIndex(0)].attestation_keypair.public_key]] with pytest.raises(AggregationError, match="expected 2 pubkeys, got 1"): - TypeTwoMultiSignature.aggregate( + MultiMessageAggregate.aggregate( parts=[part], public_keys_per_part=wrong_layout, ) -def test_type_two_aggregate_propagates_prover_error(key_manager: XmssKeyManager) -> None: +def test_multi_message_aggregate_propagates_prover_error(key_manager: XmssKeyManager) -> None: """A corrupted component proof makes the merge prover reject the inputs.""" source = Checkpoint(root=make_bytes32(210), slot=Slot(0)) att_args = (Slot(8), 211, 212, source) @@ -410,17 +415,17 @@ def test_type_two_aggregate_propagates_prover_error(key_manager: XmssKeyManager) corrupted_bytes = bytearray(part.proof.data) corrupted_bytes[10] ^= 0xFF corrupted_bytes[20] ^= 0xFF - part = TypeOneMultiSignature( + part = SingleMessageAggregate( participants=part.participants, proof=ByteList512KiB(data=bytes(corrupted_bytes)), ) with pytest.raises(AggregationError, match="merge_many_type_1 failed"): - TypeTwoMultiSignature.aggregate(parts=[part], public_keys_per_part=[pubkeys]) + MultiMessageAggregate.aggregate(parts=[part], public_keys_per_part=[pubkeys]) -def test_type_two_verify_round_trip(key_manager: XmssKeyManager) -> None: - """A Type-2 merge of two distinct-message Type-1 proofs round-trips through verify.""" +def test_multi_message_aggregate_verify_round_trip(key_manager: XmssKeyManager) -> None: + """Merging two distinct-message single-message aggregates round-trips through verify.""" source = Checkpoint(root=make_bytes32(300), slot=Slot(0)) # Two distinct messages signed by disjoint validator sets. @@ -447,7 +452,7 @@ def test_type_two_verify_round_trip(key_manager: XmssKeyManager) -> None: pubkeys_a = [key_manager[vid].attestation_keypair.public_key for vid in vids_a] pubkeys_b = [key_manager[vid].attestation_keypair.public_key for vid in vids_b] - merged = TypeTwoMultiSignature.aggregate( + merged = MultiMessageAggregate.aggregate( parts=[part_a, part_b], public_keys_per_part=[pubkeys_a, pubkeys_b], ) @@ -461,7 +466,7 @@ def test_type_two_verify_round_trip(key_manager: XmssKeyManager) -> None: ) -def test_type_two_verify_rejects_message_swap(key_manager: XmssKeyManager) -> None: +def test_multi_message_aggregate_verify_rejects_message_swap(key_manager: XmssKeyManager) -> None: """Swapping the parallel message bindings causes verification to fail. Without per-component message binding a proposer could pair honest @@ -492,7 +497,7 @@ def test_type_two_verify_rejects_message_swap(key_manager: XmssKeyManager) -> No pubkeys_a = [key_manager[vid].attestation_keypair.public_key for vid in vids_a] pubkeys_b = [key_manager[vid].attestation_keypair.public_key for vid in vids_b] - merged = TypeTwoMultiSignature.aggregate( + merged = MultiMessageAggregate.aggregate( parts=[part_a, part_b], public_keys_per_part=[pubkeys_a, pubkeys_b], ) @@ -509,7 +514,7 @@ def test_type_two_verify_rejects_message_swap(key_manager: XmssKeyManager) -> No ) -def test_type_two_verify_rejects_mismatched_messages_length( +def test_multi_message_aggregate_verify_rejects_mismatched_messages_length( key_manager: XmssKeyManager, ) -> None: """messages must have the same length as public_keys_per_message.""" @@ -520,7 +525,7 @@ def test_type_two_verify_rejects_mismatched_messages_length( part = _sign_and_aggregate(key_manager, vids, att_args) pubkeys = [key_manager[vid].attestation_keypair.public_key for vid in vids] - merged = TypeTwoMultiSignature.aggregate( + merged = MultiMessageAggregate.aggregate( parts=[part], public_keys_per_part=[pubkeys], ) diff --git a/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py b/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py index 7ae0428b..38f382fa 100644 --- a/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py +++ b/tests/lean_spec/spec/forks/lstar/forkchoice/test_attestation_target.py @@ -13,10 +13,10 @@ from lean_spec.spec.forks.lstar.containers import ( Attestation, AttestationData, + MultiMessageAggregate, SignedAttestation, SignedBlock, - TypeOneMultiSignature, - TypeTwoMultiSignature, + SingleMessageAggregate, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import Bytes32 @@ -570,11 +570,12 @@ def test_attestation_target_after_on_block( store, block, signatures = spec.produce_block_with_signatures(store, slot_1, proposer_1) block_root = hash_tree_root(block) - # Wrap the proposer's signature into a singleton Type-1, then merge - # with the per-attestation Type-1s into the block-level Type-2. + # Wrap the proposer's signature into a singleton single-message aggregate. + # Merge it with the per-attestation single-message aggregates + # into the block-level multi-message aggregate. proposer_signature = key_manager.sign_block_root(proposer_1, slot_1, block_root) proposer_pubkey = key_manager.get_public_keys(proposer_1)[1] - proposer_type_1 = TypeOneMultiSignature.aggregate( + proposer_single_message_aggregate = SingleMessageAggregate.aggregate( children=[], raw_xmss=[(proposer_1, proposer_pubkey, proposer_signature)], message=block_root, @@ -591,8 +592,8 @@ def test_attestation_target_after_on_block( ] public_keys_per_part.append([proposer_pubkey]) - merged = TypeTwoMultiSignature.aggregate( - [*signatures, proposer_type_1], + merged = MultiMessageAggregate.aggregate( + [*signatures, proposer_single_message_aggregate], public_keys_per_part=public_keys_per_part, ) signed_block = SignedBlock( diff --git a/tests/lean_spec/spec/forks/lstar/forkchoice/test_compute_block_weights.py b/tests/lean_spec/spec/forks/lstar/forkchoice/test_compute_block_weights.py index 2d0b6415..9cb9bf2a 100644 --- a/tests/lean_spec/spec/forks/lstar/forkchoice/test_compute_block_weights.py +++ b/tests/lean_spec/spec/forks/lstar/forkchoice/test_compute_block_weights.py @@ -7,16 +7,16 @@ from lean_spec.spec.crypto.merkleization import hash_tree_root from lean_spec.spec.forks import Checkpoint, Slot, ValidatorIndex, ValidatorIndices from lean_spec.spec.forks.lstar import Store -from lean_spec.spec.forks.lstar.containers import AttestationData, TypeOneMultiSignature +from lean_spec.spec.forks.lstar.containers import AttestationData, SingleMessageAggregate from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz.byte_arrays import ByteList512KiB, Bytes32 from tests.lean_spec.helpers import make_bytes32, make_signed_block -def _make_empty_proof(participants: list[ValidatorIndex]) -> TypeOneMultiSignature: - """Create a placeholder Type-1 proof carrying a participant bitfield.""" +def _make_empty_proof(participants: list[ValidatorIndex]) -> SingleMessageAggregate: + """Create a placeholder single-message aggregate proof carrying a participant bitfield.""" placeholder = ByteList512KiB(data=b"") - return TypeOneMultiSignature( + return SingleMessageAggregate( participants=ValidatorIndices(data=participants).to_aggregation_bits(), proof=placeholder, ) diff --git a/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_attestations.py b/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_attestations.py index 79479df2..686e872b 100644 --- a/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_attestations.py +++ b/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_attestations.py @@ -14,7 +14,7 @@ AttestationData, SignedAggregatedAttestation, SignedAttestation, - TypeOneMultiSignature, + SingleMessageAggregate, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import ByteList512KiB, Bytes32 @@ -286,7 +286,7 @@ def test_valid_proof_stored_correctly( ) for vid in participants ] - proof = TypeOneMultiSignature.aggregate( + proof = SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss, message=data_root, @@ -332,7 +332,7 @@ def test_attestation_data_used_as_key( ) for vid in participants ] - proof = TypeOneMultiSignature.aggregate( + proof = SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss, message=data_root, @@ -371,7 +371,7 @@ def test_invalid_proof_rejected(self, key_manager: XmssKeyManager, spec: LstarSp ) for vid in signers ] - proof = TypeOneMultiSignature.aggregate( + proof = SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss, message=data_root, @@ -382,7 +382,7 @@ def test_invalid_proof_rejected(self, key_manager: XmssKeyManager, spec: LstarSp corrupted_data = bytearray(proof.proof.data) corrupted_data[10] ^= 0xFF corrupted_data[20] ^= 0xFF - corrupted_proof = TypeOneMultiSignature( + corrupted_proof = SingleMessageAggregate( participants=proof.participants, proof=ByteList512KiB(data=bytes(corrupted_data)), ) @@ -418,7 +418,7 @@ def test_multiple_proofs_accumulate(self, key_manager: XmssKeyManager, spec: Lst ) for vid in participants_1 ] - proof_1 = TypeOneMultiSignature.aggregate( + proof_1 = SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss_1, message=data_root, @@ -435,7 +435,7 @@ def test_multiple_proofs_accumulate(self, key_manager: XmssKeyManager, spec: Lst ) for vid in participants_2 ] - proof_2 = TypeOneMultiSignature.aggregate( + proof_2 = SingleMessageAggregate.aggregate( children=[], raw_xmss=raw_xmss_2, message=data_root, diff --git a/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_pruning.py b/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_pruning.py index 652e7422..740582d1 100644 --- a/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_pruning.py +++ b/tests/lean_spec/spec/forks/lstar/forkchoice/test_store_pruning.py @@ -2,7 +2,7 @@ from lean_spec.spec.forks import Slot, ValidatorIndex, ValidatorIndices from lean_spec.spec.forks.lstar import AttestationSignatureEntry, Store -from lean_spec.spec.forks.lstar.containers import TypeOneMultiSignature +from lean_spec.spec.forks.lstar.containers import SingleMessageAggregate from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import ByteList512KiB, Bytes32 from tests.lean_spec.helpers import ( @@ -121,7 +121,7 @@ def test_prunes_related_structures_together(spec: LstarSpec, pruning_store: Stor # Create mock aggregated proof (empty proof data for testing) placeholder = ByteList512KiB(data=b"") - mock_proof = TypeOneMultiSignature( + mock_proof = SingleMessageAggregate( participants=ValidatorIndices(data=[ValidatorIndex(1)]).to_aggregation_bits(), proof=placeholder, ) diff --git a/tests/lean_spec/spec/forks/lstar/forkchoice/test_validator.py b/tests/lean_spec/spec/forks/lstar/forkchoice/test_validator.py index 4072dda0..05313e08 100644 --- a/tests/lean_spec/spec/forks/lstar/forkchoice/test_validator.py +++ b/tests/lean_spec/spec/forks/lstar/forkchoice/test_validator.py @@ -14,7 +14,7 @@ BlockBody, Config, SignedAttestation, - TypeOneMultiSignature, + SingleMessageAggregate, ) from lean_spec.spec.forks.lstar.spec import LstarSpec from lean_spec.spec.ssz import Bytes32, Uint64 @@ -90,7 +90,7 @@ def test_produce_block_with_attestations( # Build payloads keyed by attestation data. # If data_5 == data_6 (same slot/head/target/source), they share a key. - known_payloads: dict[AttestationData, set[TypeOneMultiSignature]] = {} + known_payloads: dict[AttestationData, set[SingleMessageAggregate]] = {} known_payloads.setdefault(signed_5.data, set()).add(proof_5) known_payloads.setdefault(signed_6.data, set()).add(proof_6)