Skip to content

Commit 8da1bc7

Browse files
committed
test(signatures): signed block with attestation
1 parent 22f2e44 commit 8da1bc7

4 files changed

Lines changed: 401 additions & 0 deletions

File tree

packages/testing/src/consensus_testing/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .test_fixtures import (
77
BaseConsensusFixture,
88
ForkChoiceTest,
9+
SignedBlockWithAttestationTest,
910
StateTransitionTest,
1011
)
1112
from .test_types import (
@@ -24,6 +25,7 @@
2425

2526
StateTransitionTestFiller = Type[StateTransitionTest]
2627
ForkChoiceTestFiller = Type[ForkChoiceTest]
28+
SignedBlockWithAttestationTestFiller = Type[SignedBlockWithAttestationTest]
2729

2830
__all__ = [
2931
# Public API
@@ -36,6 +38,7 @@
3638
"BaseConsensusFixture",
3739
"StateTransitionTest",
3840
"ForkChoiceTest",
41+
"SignedBlockWithAttestationTest",
3942
# Test types
4043
"BaseForkChoiceStep",
4144
"TickStep",
@@ -48,4 +51,5 @@
4851
# Type aliases for test function signatures
4952
"StateTransitionTestFiller",
5053
"ForkChoiceTestFiller",
54+
"SignedBlockWithAttestationTestFiller",
5155
]

packages/testing/src/consensus_testing/test_fixtures/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
from .base import BaseConsensusFixture
44
from .fork_choice import ForkChoiceTest
5+
from .signed_block_with_attestation import SignedBlockWithAttestationTest
56
from .state_transition import StateTransitionTest
67

78
__all__ = [
89
"BaseConsensusFixture",
910
"StateTransitionTest",
1011
"ForkChoiceTest",
12+
"SignedBlockWithAttestationTest",
1113
]
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
"""Signed block with attestation test fixture format."""
2+
3+
from __future__ import annotations
4+
5+
from functools import lru_cache
6+
from typing import Any, ClassVar, Dict
7+
8+
from pydantic import model_serializer, model_validator
9+
10+
from lean_spec.subspecs.containers.block.block import (
11+
BlockWithAttestation,
12+
SignedBlockWithAttestation,
13+
)
14+
from lean_spec.subspecs.containers.block.types import BlockSignatures
15+
from lean_spec.subspecs.containers.slot import Slot
16+
from lean_spec.subspecs.containers.state import Validators
17+
from lean_spec.subspecs.containers.state.state import State
18+
from lean_spec.subspecs.xmss.interface import TEST_SIGNATURE_SCHEME
19+
from lean_spec.types import ValidatorIndex
20+
21+
from ..keys import XmssKeyManager
22+
from .base import BaseConsensusFixture
23+
24+
25+
@lru_cache(maxsize=1)
26+
def _get_shared_key_manager() -> XmssKeyManager:
27+
"""
28+
Get or create the shared XMSS key manager for reusing keys across tests.
29+
30+
Uses functools.lru_cache to create a singleton instance that's shared
31+
across all test fixture generations within a session. This optimizes
32+
performance by reusing keys when possible.
33+
34+
Returns:
35+
Shared XmssKeyManager instance with max_slot=2.
36+
"""
37+
return XmssKeyManager(max_slot=Slot(2))
38+
39+
40+
class SignedBlockWithAttestationTest(BaseConsensusFixture):
41+
"""
42+
Test fixture for verifying signed block with attestation signature correctness.
43+
44+
This fixture tests the cryptographic correctness of a single SignedBlockWithAttestation:
45+
- Block + proposer attestation + aggregated signatures
46+
47+
The fixture verifies that:
48+
1. Signatures are correctly generated using XMSS keys
49+
2. Signatures can be verified using the validators' public keys
50+
3. Invalid signatures are properly rejected
51+
4. Signature aggregation is correct
52+
53+
Structure:
54+
anchor_state: Initial state with validator public keys
55+
container: Single signed block with attestation to test
56+
valid: Whether all signatures should verify (default: True)
57+
"""
58+
59+
format_name: ClassVar[str] = "signed_block_with_attestation_test"
60+
description: ClassVar[str] = "Tests signed block with attestation signature correctness"
61+
62+
anchor_state: State | None = None
63+
"""
64+
The initial consensus state with validator public keys.
65+
66+
If not provided, the framework will use the genesis fixture from pytest.
67+
This allows tests to omit genesis for simpler test code while still
68+
allowing customization when needed.
69+
"""
70+
71+
signed_block_with_attestation: SignedBlockWithAttestation
72+
"""
73+
Single signed block with attestation to verify.
74+
75+
The block will be verified for signature correctness against the
76+
validator public keys in anchor_state.
77+
"""
78+
79+
valid: bool = True
80+
"""
81+
Whether all signatures are expected to be valid.
82+
83+
If True, all signatures must verify successfully.
84+
If False, at least one signature must fail verification.
85+
"""
86+
87+
max_slot: Slot | None = None
88+
"""
89+
Maximum slot for which XMSS keys should be valid.
90+
91+
If not provided, will be auto-calculated from the container. This determines
92+
how many slots worth of XMSS signatures can be generated.
93+
"""
94+
95+
@model_serializer(mode="wrap", when_used="json")
96+
def _serialize_model(self, serializer: Any) -> Dict[str, Any]:
97+
"""
98+
Custom serializer for JSON output.
99+
100+
Outputs:
101+
- blockWithAttestation: The unsigned block with proposer attestation
102+
- attester_pubkeys: Array of public keys for attestations in block body (in order)
103+
- proposer_pubkey: The proposer's public key
104+
- maxSlot: Maximum slot value
105+
- signedBlockWithAttestation: The complete signed block with signatures
106+
"""
107+
# Get default serialization
108+
data = serializer(self)
109+
110+
if self.anchor_state is not None:
111+
# Collect attester pubkeys for all attestations in the block body
112+
attester_pubkeys = []
113+
for attestation in self.signed_block_with_attestation.message.block.body.attestations:
114+
validator_index = ValidatorIndex(int(attestation.validator_id))
115+
validator = self.anchor_state.validators[int(validator_index)]
116+
attester_pubkeys.append(
117+
validator.pubkey.hex()
118+
if isinstance(validator.pubkey, bytes)
119+
else validator.pubkey
120+
)
121+
122+
# Get proposer pubkey
123+
proposer_index = ValidatorIndex(int(self.signed_block_with_attestation.message.block.proposer_index))
124+
proposer = self.anchor_state.validators[int(proposer_index)]
125+
proposer_pubkey = (
126+
proposer.pubkey.hex()
127+
if isinstance(proposer.pubkey, bytes)
128+
else proposer.pubkey
129+
)
130+
131+
data["blockWithAttestation"] = self.signed_block_with_attestation.message.model_dump(mode="json")
132+
data["attester_pubkeys"] = attester_pubkeys
133+
data["proposer_pubkey"] = proposer_pubkey
134+
data["signedBlockWithAttestation"] = self.signed_block_with_attestation.model_dump(mode="json")
135+
data.pop("container", None)
136+
data.pop("anchorState", None)
137+
138+
return data
139+
140+
@model_validator(mode="after")
141+
def set_max_slot_default(self) -> SignedBlockWithAttestationTest:
142+
"""
143+
Auto-calculate max_slot from signed_block_with_attestation if not provided.
144+
145+
Uses the slot value from the block to ensure XMSS keys are
146+
generated with sufficient capacity.
147+
"""
148+
if self.max_slot is None:
149+
self.max_slot = Slot(int(self.signed_block_with_attestation.message.block.slot))
150+
151+
return self
152+
153+
def make_fixture(self) -> SignedBlockWithAttestationTest:
154+
"""
155+
Generate the fixture by building and verifying the signed block.
156+
157+
This validates the test by:
158+
1. Setting up XMSS key manager with validator keys
159+
2. Updating anchor_state with generated public keys
160+
3. Generating signatures using XMSS keys
161+
4. Verifying signatures against validator public keys
162+
5. Checking that valid/invalid expectation is met
163+
164+
Returns
165+
-------
166+
SignedBlockWithAttestationTest
167+
The validated fixture with properly signed block.
168+
169+
Raises
170+
------
171+
AssertionError
172+
If signature verification doesn't match expected validity.
173+
"""
174+
# Ensure anchor_state is set
175+
assert self.anchor_state is not None, "anchor_state must be set before make_fixture"
176+
assert self.max_slot is not None, "max_slot must be set before make_fixture"
177+
178+
# Use shared key manager if it has sufficient capacity, otherwise create a new one
179+
shared_key_manager = _get_shared_key_manager()
180+
key_manager = (
181+
shared_key_manager
182+
if self.max_slot <= shared_key_manager.max_slot
183+
else XmssKeyManager(max_slot=self.max_slot, scheme=TEST_SIGNATURE_SCHEME)
184+
)
185+
186+
# Update validator pubkeys to match key_manager's generated keys
187+
updated_validators = [
188+
validator.model_copy(
189+
update={
190+
"pubkey": key_manager[ValidatorIndex(i)].public.to_bytes(
191+
key_manager.scheme.config
192+
)
193+
}
194+
)
195+
for i, validator in enumerate(self.anchor_state.validators)
196+
]
197+
198+
self.anchor_state = self.anchor_state.model_copy(
199+
update={"validators": Validators(data=updated_validators)}
200+
)
201+
202+
# Build signed block with correct signatures
203+
signed_block = self._build_signed_block(self.signed_block_with_attestation.message, key_manager)
204+
self.signed_block_with_attestation = signed_block
205+
206+
# Verify all signatures in the block
207+
is_valid = signed_block.verify_signatures(self.anchor_state)
208+
209+
if self.valid and not is_valid:
210+
raise AssertionError(
211+
"SignedBlockWithAttestation: expected valid signatures but verification failed"
212+
)
213+
elif not self.valid and is_valid:
214+
raise AssertionError(
215+
"SignedBlockWithAttestation: expected invalid signatures but verification succeeded"
216+
)
217+
218+
return self
219+
220+
def _build_signed_block(
221+
self, block_with_attestation: BlockWithAttestation, key_manager: XmssKeyManager
222+
) -> SignedBlockWithAttestation:
223+
"""
224+
Build a SignedBlockWithAttestation with correct XMSS signatures.
225+
226+
This generates signatures for:
227+
1. All attestations in the block body
228+
2. The proposer's attestation
229+
230+
Parameters
231+
----------
232+
block_with_attestation : BlockWithAttestation
233+
The block and proposer attestation to sign.
234+
key_manager : XmssKeyManager
235+
Key manager for generating XMSS signatures.
236+
237+
Returns
238+
-------
239+
SignedBlockWithAttestation
240+
The block with valid XMSS signatures.
241+
"""
242+
block = block_with_attestation.block
243+
proposer_attestation = block_with_attestation.proposer_attestation
244+
245+
# Sign all attestations in the block body
246+
signature_list = []
247+
for attestation in block.body.attestations:
248+
signature_list.append(key_manager.sign_attestation(attestation))
249+
250+
# Sign the proposer attestation
251+
proposer_signature = key_manager.sign_attestation(proposer_attestation)
252+
signature_list.append(proposer_signature)
253+
254+
return SignedBlockWithAttestation(
255+
message=block_with_attestation, signature=BlockSignatures(data=signature_list)
256+
)

0 commit comments

Comments
 (0)