Skip to content
Draft
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
102 changes: 59 additions & 43 deletions packages/testing/src/consensus_testing/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,10 @@

from lean_spec.config import LEAN_ENV
from lean_spec.forks.lstar.containers import AttestationData
from lean_spec.forks.lstar.containers.block.types import (
AggregatedAttestations,
AttestationSignatures,
)
from lean_spec.forks.lstar.containers.block.types import AggregatedAttestations
from lean_spec.subspecs.koalabear import Fp
from lean_spec.subspecs.ssz.hash import hash_tree_root
from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof
from lean_spec.subspecs.xmss.aggregation import TypeOneInfo, TypeOneMultiSignature
from lean_spec.subspecs.xmss.constants import TARGET_CONFIG
from lean_spec.subspecs.xmss.containers import PublicKey, SecretKey, Signature, ValidatorKeyPair
from lean_spec.subspecs.xmss.interface import (
Expand All @@ -61,7 +58,15 @@
HashTreeOpening,
Randomness,
)
from lean_spec.types import Bytes32, Slot, Uint64, ValidatorIndex, ValidatorIndices
from lean_spec.types import (
AggregationBits,
ByteListMiB,
Bytes32,
Slot,
Uint64,
ValidatorIndex,
ValidatorIndices,
)

SecretField = Literal["attestation_secret", "proposal_secret"]
"""Discriminator for which secret key to load from a validator key pair."""
Expand Down Expand Up @@ -122,6 +127,21 @@ def create_dummy_signature() -> Signature:
)


def create_dummy_type_1(participants: AggregationBits) -> TypeOneMultiSignature:
"""Build a structurally valid Type-1 proof with empty proof bytes.

Skips the lean_multisig_py binding entirely so tests that only check
the proof's shape (participants) stay fast. Verifiers will reject the
empty proof bytes, so this must only be used in tests that do not
exercise cryptographic verification.
"""
placeholder = ByteListMiB(data=b"")
return TypeOneMultiSignature(
info=TypeOneInfo(participants=participants, proof=placeholder),
proof=placeholder,
)


DEFAULT_MAX_SLOT = Slot(10)
"""
Default max slot for the shared key manager.
Expand Down Expand Up @@ -508,18 +528,22 @@ def sign_and_aggregate(
self,
validator_ids: list[ValidatorIndex],
attestation_data: AttestationData,
) -> AggregatedSignatureProof:
) -> TypeOneMultiSignature:
"""
Sign attestation data with each validator and aggregate into a single proof.
Sign attestation_data with each validator and aggregate into a Type-1 proof.

Convenience method for the common sign-each-validator-then-aggregate pattern.
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 all participants to (data, slot).

Args:
validator_ids: Validators to sign with.
attestation_data: The attestation data to sign.
validator_ids: Validators that contribute signatures, in the
order they appear in the participant bitfield.
attestation_data: The attestation data the proof binds to.

Returns:
Aggregated signature proof combining all validators' signatures.
Cryptographically valid Type-1 proof covering validator_ids.
"""
raw_xmss = [
(
Expand All @@ -528,75 +552,67 @@ def sign_and_aggregate(
)
for vid in validator_ids
]

xmss_participants = ValidatorIndices(data=validator_ids).to_aggregation_bits()

return AggregatedSignatureProof.aggregate(
xmss_participants=xmss_participants,
return TypeOneMultiSignature.aggregate(
children=[],
raw_xmss=raw_xmss,
xmss_participants=ValidatorIndices(data=validator_ids).to_aggregation_bits(),
message=hash_tree_root(attestation_data),
slot=attestation_data.slot,
)

def build_attestation_signatures(
def build_attestation_proofs(
self,
aggregated_attestations: AggregatedAttestations,
signature_lookup: Mapping[AttestationData, Mapping[ValidatorIndex, Signature]]
| None = None,
) -> AttestationSignatures:
) -> list[TypeOneMultiSignature]:
"""
Produce aggregated signature proofs for a list of attestations.
Produce per-attestation Type-1 proofs aligned with the given attestations.

For each aggregated attestation:

1. Identify participating validators from the aggregation bitfield
2. Collect each participant's public key and individual signature
3. Combine them into a single aggregated proof for the leanVM verifier
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
multi-signature binding.

Pre-computed signatures can be supplied via the lookup to avoid
redundant signing. Missing signatures are computed on the fly.
redundant signing; missing entries are signed on the fly. The
resulting proofs feed into block production and signature
verification, both of which require real cryptographic content.

Args:
aggregated_attestations: Attestations with aggregation bitfields set.
signature_lookup: Optional pre-computed signatures keyed by
attestation data then validator index.

Returns:
One aggregated signature proof per attestation.
One Type-1 single-message proof per attestation, parallel to the input.
"""
lookup = signature_lookup or {}

proofs: list[AggregatedSignatureProof] = []
proofs: list[TypeOneMultiSignature] = []
for agg in aggregated_attestations:
# Decode which validators participated from the bitfield.
validator_ids = agg.aggregation_bits.to_validator_indices()

# Try the lookup first for pre-computed signatures.
# Fall back to signing on the fly for any missing entries.
sigs_for_data = lookup.get(agg.data, {})

# Collect the attestation public key for each participant.
public_keys = [self.get_public_keys(vid)[0] for vid in validator_ids]

# Gather individual signatures, computing any that are missing.
signatures = [
sigs_for_data.get(vid) or self.sign_attestation_data(vid, agg.data)
for vid in validator_ids
]

# Produce a single aggregated proof that the leanVM can verify
# in one pass over all participants.
proof = AggregatedSignatureProof.aggregate(
xmss_participants=agg.aggregation_bits,
children=[],
raw_xmss=list(zip(public_keys, signatures, strict=True)),
message=hash_tree_root(agg.data),
slot=agg.data.slot,
proofs.append(
TypeOneMultiSignature.aggregate(
children=[],
raw_xmss=list(zip(public_keys, signatures, strict=True)),
xmss_participants=agg.aggregation_bits,
message=hash_tree_root(agg.data),
slot=agg.data.slot,
)
)
proofs.append(proof)

return AttestationSignatures(data=proofs)
return proofs


def _generate_single_keypair(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ def make_fixture(self) -> Self:
case BlockStep():
# Build a complete signed block from the lightweight spec.
# The spec contains minimal fields; we fill the rest.
signed_block = step.block.build_signed_block_with_store(
signed_block, store = step.block.build_signed_block_with_store(
store, self._block_registry, key_manager, self.lean_env
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from lean_spec.forks.lstar.containers.state import State
from lean_spec.forks.lstar.spec import LstarSpec
from lean_spec.subspecs.ssz.hash import hash_tree_root
from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof
from lean_spec.subspecs.xmss.aggregation import TypeOneMultiSignature
from lean_spec.types import Bytes32, ValidatorIndices

from ..keys import XmssKeyManager
Expand Down Expand Up @@ -250,7 +250,7 @@ def _build_block_from_spec(

# Path 3: normal block construction via the spec's builder.
else:
aggregated_payloads: dict[AttestationData, set[AggregatedSignatureProof]] = {}
aggregated_payloads: dict[AttestationData, set[TypeOneMultiSignature]] = {}
if spec.attestations:
aggregated_payloads = StateTransitionTest._build_aggregated_payloads_from_spec(
spec.attestations, state, block_registry
Expand Down Expand Up @@ -304,7 +304,7 @@ def _build_aggregated_payloads_from_spec(
attestation_specs: list[AggregatedAttestationSpec],
state: State,
block_registry: dict[str, Block],
) -> dict[AttestationData, set[AggregatedSignatureProof]]:
) -> dict[AttestationData, set[TypeOneMultiSignature]]:
"""
Build aggregated signature payloads from attestation specifications.

Expand All @@ -320,7 +320,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[AggregatedSignatureProof]] = {}
payloads: dict[AttestationData, set[TypeOneMultiSignature]] = {}

for spec in attestation_specs:
if not spec.valid_signature:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@
from pydantic import Field

from lean_spec.forks.lstar.containers.attestation import AggregatedAttestation
from lean_spec.forks.lstar.containers.block import (
SignedBlock,
)
from lean_spec.forks.lstar.containers.block.types import (
AggregatedAttestations,
AttestationSignatures,
)
from lean_spec.forks.lstar.containers.block import SignedBlock
from lean_spec.forks.lstar.containers.block.types import AggregatedAttestations
from lean_spec.forks.lstar.containers.state import State
from lean_spec.forks.lstar.spec import LstarSpec
from lean_spec.types import AggregationBits, Boolean, ValidatorIndex
from lean_spec.subspecs.xmss.aggregation import TypeOneInfos, TypeTwoMultiSignature
from lean_spec.types import AggregationBits, Boolean, ByteListMiB, ValidatorIndex

from ..keys import XmssKeyManager
from ..test_types import BlockSpec
Expand Down Expand Up @@ -60,10 +56,9 @@ class VerifySignaturesTest(BaseConsensusFixture):

Supported operations:

- `{"operation": "drop_last_signature"}`: Remove the last entry
from the block's attestation_signatures list. Produces a signed
block whose signature-group count is one less than its
attestation count.
- `{"operation": "drop_last_signature"}`: Drop the last info entry from
the merged Type-2 proof while leaving the body unchanged. Produces a
signed block whose proof binds to fewer messages than expected.
- `{"operation": "set_proposer_index", "value": int}`: Rewrite
the block's proposer_index field. Use this to exercise the
validator-bounds check that the builder skips because its round-
Expand Down Expand Up @@ -154,14 +149,16 @@ def _apply_tamper(self, signed_block: SignedBlock) -> SignedBlock:
operation = self.tamper.get("operation")

if operation == "drop_last_signature":
original = signed_block.signature.attestation_signatures.data
if not original:
raise ValueError("drop_last_signature requires at least one attestation signature")
truncated = AttestationSignatures(data=list(original[:-1]))
tampered_signatures = signed_block.signature.model_copy(
update={"attestation_signatures": truncated}
decoded = TypeTwoMultiSignature.decode_bytes(signed_block.proof.data)
if len(decoded.info) <= 1:
raise ValueError(
"drop_last_signature requires the proof to bind at least two messages"
)
truncated_info = TypeOneInfos(data=list(decoded.info)[:-1])
tampered = decoded.model_copy(update={"info": truncated_info})
return signed_block.model_copy(
update={"proof": ByteListMiB(data=tampered.encode_bytes())}
)
return signed_block.model_copy(update={"signature": tampered_signatures})

if operation == "set_proposer_index":
value = self.tamper.get("value")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from lean_spec.forks.lstar.containers.block.block import Block
from lean_spec.forks.lstar.containers.block.types import AggregatedAttestations
from lean_spec.forks.lstar.containers.state import State
from lean_spec.subspecs.xmss.aggregation import AggregatedSignatureProof
from lean_spec.subspecs.xmss.aggregation import TypeOneInfo, TypeOneMultiSignature
from lean_spec.types import (
ByteListMiB,
Bytes32,
Expand Down Expand Up @@ -164,7 +164,7 @@ def build_invalid_proof(
state: State,
key_manager: XmssKeyManager,
block: Block,
) -> tuple[Block, AggregatedSignatureProof]:
) -> tuple[Block, TypeOneMultiSignature]:
"""
Build an invalid attestation proof and append it to the block body.

Expand All @@ -190,23 +190,26 @@ def build_invalid_proof(
data=attestation_data,
)

# Empty proof bytes flag "no real Type-1 here" β€” the caller treats
# any such entry as a placeholder and bypasses real binding merges.
placeholder = ByteListMiB(data=b"")

if not self.valid_signature:
# Cryptographically invalid proof (zeroed-out bytes).
invalid_proof = AggregatedSignatureProof(
participants=ValidatorIndices(data=self.validator_ids).to_aggregation_bits(),
proof_data=ByteListMiB(data=b"\x00" * 32),
invalid_proof = TypeOneMultiSignature(
info=TypeOneInfo(participants=aggregation_bits, proof=placeholder),
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 = AggregatedSignatureProof(
participants=aggregation_bits,
proof_data=valid_proof.proof_data,
invalid_proof = TypeOneMultiSignature(
info=TypeOneInfo(participants=aggregation_bits, proof=valid_proof.proof),
proof=valid_proof.proof,
)
else:
invalid_proof = AggregatedSignatureProof(
participants=ValidatorIndices(data=self.validator_ids).to_aggregation_bits(),
proof_data=ByteListMiB(data=b"\x00" * 32),
invalid_proof = TypeOneMultiSignature(
info=TypeOneInfo(participants=aggregation_bits, proof=placeholder),
proof=placeholder,
)

# Append invalid attestation to the block body.
Expand Down
Loading
Loading