66from lean_spec .subspecs .containers .slot import Slot
77from lean_spec .subspecs .ssz .hash import hash_tree_root
88from lean_spec .subspecs .xmss .containers import PublicKey , SecretKey
9- from lean_spec .subspecs .xmss .interface import DEFAULT_SIGNATURE_SCHEME
9+ from lean_spec .subspecs .xmss .interface import (
10+ TEST_SIGNATURE_SCHEME as DEFAULT_SIGNATURE_SCHEME ,
11+ )
1012from lean_spec .types import ValidatorIndex
1113
1214
1315class KeyPair (NamedTuple ):
14- """A validator’ s XMSS key pair."""
16+ """A validator' s XMSS key pair."""
1517
1618 public : PublicKey
17- """The validator’ s public key (used for verification)."""
19+ """The validator' s public key (used for verification)."""
1820
1921 secret : SecretKey
20- """The validator’s secret key (used for signing)."""
22+ """The validator's secret key (used for signing)."""
23+
24+
25+ _KEY_CACHE : dict [tuple [int , int ], KeyPair ] = {}
26+ """
27+ Cache keys across tests to avoid regenerating them for the same validator/lifetime combo.
28+
29+ Key: (validator_index, num_active_epochs) -> KeyPair
30+ """
2131
2232
2333class XmssKeyManager :
@@ -76,6 +86,13 @@ def __getitem__(self, validator_index: ValidatorIndex) -> KeyPair:
7686 # - We include slot 0 (genesis) in the count
7787 num_active_epochs = self .max_slot .as_int () + 1
7888
89+ # Check global cache first (keys are reused across tests)
90+ cache_key = (int (validator_index ), num_active_epochs )
91+ if cache_key in _KEY_CACHE :
92+ key_pair = _KEY_CACHE [cache_key ]
93+ self ._key_pairs [validator_index ] = key_pair
94+ return key_pair
95+
7996 # Generate the key pair using the default XMSS scheme.
8097 #
8198 # The seed is set to 0 for deterministic test keys.
@@ -85,12 +102,13 @@ def __getitem__(self, validator_index: ValidatorIndex) -> KeyPair:
85102
86103 # Store as a cohesive unit and return.
87104 key_pair = KeyPair (public = pk , secret = sk )
105+ _KEY_CACHE [cache_key ] = key_pair # Cache globally for reuse across tests
88106 self ._key_pairs [validator_index ] = key_pair
89107 return key_pair
90108
91109 def sign_attestation (self , attestation : Attestation ) -> Signature :
92110 """
93- Sign an attestation with the validator’ s XMSS key.
111+ Sign an attestation with the validator' s XMSS key.
94112
95113 Parameters
96114 ----------
@@ -113,22 +131,50 @@ def sign_attestation(self, attestation: Attestation) -> Signature:
113131
114132 # Lazy key retrieval: creates keys if first time seeing this validator.
115133 key_pair = self [validator_id ]
116-
117- # Compute the message digest from the attestation's SSZ tree root.
118- #
119- # This produces a cryptographic hash of the entire attestation structure.
120- message = bytes (hash_tree_root (attestation ))
134+ # Get the current secret key
135+ sk = key_pair .secret
121136
122137 # Map the attestation slot to an XMSS epoch.
123138 #
124139 # Each slot gets its own epoch to avoid key reuse.
125140 epoch = attestation .data .slot
126141
127- # Generate the XMSS signature using the validator's secret key.
128- xmss_sig = DEFAULT_SIGNATURE_SCHEME .sign (key_pair .secret , epoch , message )
142+ # Advance the key's prepared window until it covers the target epoch.
143+ #
144+ # We use the scheme that the key was generated with.
145+ scheme = DEFAULT_SIGNATURE_SCHEME
146+
147+ # Loop until the epoch is inside the prepared interval
148+ prepared_interval = scheme .get_prepared_interval (sk )
149+ while int (epoch ) not in prepared_interval :
150+ # Check if we're advancing past the key's total lifetime
151+ activation_interval = scheme .get_activation_interval (sk )
152+ if prepared_interval .stop >= activation_interval .stop :
153+ raise ValueError (
154+ f"Cannot sign for epoch { epoch } : "
155+ f"it is beyond the key's max lifetime { activation_interval .stop } "
156+ )
157+
158+ # Advance the key and get the new key object
159+ sk = scheme .advance_preparation (sk )
160+
161+ # Update the prepared interval for the next loop check
162+ prepared_interval = scheme .get_prepared_interval (sk )
163+
164+ # Update the cached key pair with the new, advanced secret key.
165+ # This ensures the *next* call to sign() uses the advanced state.
166+ self ._key_pairs [validator_id ] = KeyPair (public = key_pair .public , secret = sk )
167+
168+ # Compute the message digest from the attestation's SSZ tree root.
169+ #
170+ # This produces a cryptographic hash of the entire attestation structure.
171+ message = bytes (hash_tree_root (attestation ))
172+
173+ # Generate the XMSS signature using the validator's (now prepared) secret key.
174+ xmss_sig = scheme .sign (sk , epoch , message )
129175
130176 # Convert the signature to the wire format (byte array).
131- signature_bytes = xmss_sig .to_bytes (DEFAULT_SIGNATURE_SCHEME .config )
177+ signature_bytes = xmss_sig .to_bytes (scheme .config )
132178
133179 # Ensure the signature meets the consensus spec length (3100 bytes).
134180 #
0 commit comments