Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions packages/testing/src/consensus_testing/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
from lean_spec.spec.forks.lstar.containers import (
AggregatedAttestations,
AttestationData,
TypeOneMultiSignature,
SingleMessageAggregate,
)
from lean_spec.spec.ssz import Bytes32, Uint64

Expand Down Expand Up @@ -513,21 +513,24 @@ 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:
validator_ids: Validators to sign with.
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 = [
(
Expand All @@ -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),
Expand All @@ -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
Expand All @@ -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()
Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
AggregatedAttestation,
AggregatedAttestations,
AttestationData,
MultiMessageAggregate,
SignedBlock,
State,
)
Expand Down Expand Up @@ -68,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.
Expand Down Expand Up @@ -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 = MultiMessageAggregate(
proof=ByteList512KiB(data=b"\x00\x01\x02\x03"),
)
return signed_block

if operation == "append_phantom_attestation":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
AggregatedAttestations,
AttestationData,
Block,
SingleMessageAggregate,
State,
TypeOneMultiSignature,
)
from lean_spec.spec.ssz import ByteList512KiB, Bytes32

Expand Down Expand Up @@ -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.

Expand All @@ -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(
Expand Down
44 changes: 21 additions & 23 deletions packages/testing/src/consensus_testing/test_types/block_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -304,19 +305,16 @@ def _sign_block(
]
public_keys_per_part.append([proposer_pubkey])

merged = TypeTwoMultiSignature.aggregate(
[*attestation_proofs, proposer_type_1],
proof = MultiMessageAggregate.aggregate(
[*attestation_proofs, proposer_single_message_aggregate],
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 = MultiMessageAggregate(proof=ByteList512KiB(data=b""))

return SignedBlock(
block=final_block,
proof=ByteList512KiB(data=proof_bytes),
proof=proof,
)

def build_signed_block(
Expand Down Expand Up @@ -436,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
Expand Down Expand Up @@ -471,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)

Expand Down Expand Up @@ -512,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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
AttestationData,
Block,
SignedAggregatedAttestation,
SingleMessageAggregate,
State,
TypeOneMultiSignature,
)
from lean_spec.spec.ssz import ByteList512KiB, Bytes32

Expand Down Expand Up @@ -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,
)
Expand All @@ -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,
)
Expand Down
Loading
Loading