Skip to content

Commit 9a56f54

Browse files
conacheMegaRedHandpablodeymo
authored
feat(blockchain): advance XMSS preparation window preemptively (#332)
## 🗒️ Description / Motivation This PR closes #262. Every 65,536 slots, an XMSS signing key has to precompute its next bottom tree via leansig's [`advance_preparation`](https://github.com/leanEthereum/leanSig). The two most recently computed trees form a sliding "prepared window" of 131,072 slots - the range the key can sign for without doing more work. Once the wall-clock slot crosses out of that window, the precomputation has to run before the next signature. PR #261 made `advance_preparation()` run synchronously on the `BlockChainServer` actor's tick handler. When the window has to slide forward, the actor blocks on the hash work long enough to stall other executions. This PR moves that advance **off the signing path**, running it preemptively where blocking is cheap: - **At startup**, in `BlockChain::spawn` (before the actor starts ticking) - catches each loaded key's prepared window up to the current wall-clock slot. Handles long offline gaps. - **At the end of every tick**, after the interval's duties - advances each key's window to cover `slot + 1`, so the next tick's signing is always inside the prepared window. The lazy advance loop inside `sign_with_*` is kept as a safety net, with added `elapsed_ms` timing logs so we'd see it if it ever fires. ## What Changed **`crates/blockchain/src/key_manager.rs`** - New `KeyManager::advance_keys_to(slot)` - iterates registered validators and advances both attestation and proposal keys to cover `slot`. - New free helper `advance_key(...)` - synchronous advance loop with `Instant::now()` timing. Emits `info!` at start, `info!` with `elapsed_ms` at end, `warn!` on activation-interval exhaustion. - Added matching `Instant::now()` + `elapsed_ms` `info!` to the pre-existing advance loops in `sign_with_attestation_key` / `sign_with_proposal_key`. **`crates/blockchain/src/lib.rs`** - `BlockChain::spawn`: computes current wall-clock slot and calls `key_manager.advance_keys_to(current_slot)` before the actor starts ticking. - `on_tick`: at the very end, after metric updates, calls `self.key_manager.advance_keys_to((slot + 1) as u32)`. ## Correctness / Behavior Guarantees - Signing is never delayed by an advance - preempt runs at the idle tail of the interval (or before any tick fires, at startup). - Steady-state: the end-of-tick advance is a no-op in 65,535 out of every 65,536 slots. - After a long offline gap: startup catch-up walks the window forward before any tick fires. - Activation-interval exhaustion stays a hard error in the signing path; `advance_key` logs `warn!` and breaks so the next sign attempt surfaces the error. - `ValidatorKeyPair` shape and the `KeyManagerError` enum are unchanged from main. ## Tests Added / Run - [x] `make fmt` clean - [x] `make lint` clean - [x] `make test` passes - [ ] Boundary-crossing verification on devnet - only fires every 65,536 slots, deferred to operational verification. Log lines `Preparing XMSS key for slot in background` (start) and `XMSS key advance complete` (success) signal the path firing. ## Related Issues / PRs - Initially linked to #262 - Follow-up to #261 --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com> Co-authored-by: Pablo Deymonnaz <pdeymon@fi.uba.ar>
1 parent 22e56f1 commit 9a56f54

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

crates/blockchain/src/key_manager.rs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use std::collections::HashMap;
2+
use std::time::Instant;
23

34
use ethlambda_types::{
45
attestation::{AttestationData, XmssSignature},
56
primitives::{H256, HashTreeRoot as _},
67
signature::{ValidatorSecretKey, ValidatorSignature},
78
};
8-
use tracing::info;
9+
use tracing::{info, warn};
910

1011
use crate::metrics;
1112

@@ -48,6 +49,14 @@ impl KeyManager {
4849
self.keys.keys().copied().collect()
4950
}
5051

52+
/// Advances every validator's XMSS preparation windows to cover slot
53+
pub fn advance_keys_to(&mut self, slot: u32) {
54+
for (validator_id, key_pair) in self.keys.iter_mut() {
55+
advance_key(*validator_id, &mut key_pair.attestation_key, slot);
56+
advance_key(*validator_id, &mut key_pair.proposal_key, slot);
57+
}
58+
}
59+
5160
/// Signs an attestation using the validator's attestation key.
5261
pub fn sign_attestation(
5362
&mut self,
@@ -85,6 +94,7 @@ impl KeyManager {
8594
// Multiple advances may be needed if the node was offline for an extended period.
8695
if !key_pair.attestation_key.is_prepared_for(slot) {
8796
info!(validator_id, slot, "Advancing XMSS key preparation window");
97+
let start = Instant::now();
8898
while !key_pair.attestation_key.is_prepared_for(slot) {
8999
let before = key_pair.attestation_key.get_prepared_interval();
90100
key_pair.attestation_key.advance_preparation();
@@ -95,6 +105,12 @@ impl KeyManager {
95105
)));
96106
}
97107
}
108+
info!(
109+
validator_id,
110+
slot,
111+
elapsed_ms = start.elapsed().as_millis() as u64,
112+
"Advanced XMSS key preparation window"
113+
);
98114
}
99115

100116
let signature: ValidatorSignature = {
@@ -130,6 +146,7 @@ impl KeyManager {
130146
validator_id,
131147
slot, "Advancing XMSS proposal key preparation window"
132148
);
149+
let start = Instant::now();
133150
while !key_pair.proposal_key.is_prepared_for(slot) {
134151
let before = key_pair.proposal_key.get_prepared_interval();
135152
key_pair.proposal_key.advance_preparation();
@@ -140,6 +157,12 @@ impl KeyManager {
140157
)));
141158
}
142159
}
160+
info!(
161+
validator_id,
162+
slot,
163+
elapsed_ms = start.elapsed().as_millis() as u64,
164+
"Advanced XMSS proposal key preparation window"
165+
);
143166
}
144167

145168
let signature: ValidatorSignature = key_pair
@@ -153,6 +176,28 @@ impl KeyManager {
153176
}
154177
}
155178

179+
fn advance_key(validator_id: u64, key: &mut ValidatorSecretKey, slot: u32) {
180+
if key.is_prepared_for(slot) {
181+
return;
182+
}
183+
info!(validator_id, slot, "Advancing XMSS key preparation window");
184+
let start = Instant::now();
185+
while !key.is_prepared_for(slot) {
186+
let before = key.get_prepared_interval();
187+
key.advance_preparation();
188+
if key.get_prepared_interval() == before {
189+
warn!(validator_id, slot, "XMSS key activation interval exhausted");
190+
break;
191+
}
192+
}
193+
info!(
194+
validator_id,
195+
slot,
196+
elapsed_ms = start.elapsed().as_millis() as u64,
197+
"Advanced XMSS key preparation window"
198+
);
199+
}
200+
156201
#[cfg(test)]
157202
mod tests {
158203
use super::*;

crates/blockchain/src/lib.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,19 @@ impl BlockChain {
6464
metrics::set_is_aggregator(aggregator.is_enabled());
6565
metrics::set_node_sync_status(metrics::SyncStatus::Idle);
6666
let genesis_time = store.config().genesis_time;
67-
let key_manager = key_manager::KeyManager::new(validator_keys);
67+
let mut key_manager = key_manager::KeyManager::new(validator_keys);
68+
69+
// Catch XMSS keys up to the current slot before the first tick
70+
// store.time() doesn't work here: after an offline gap it lags wall-clock by
71+
// exactly the gap we need to catch up through
72+
let now_ms = SystemTime::UNIX_EPOCH
73+
.elapsed()
74+
.expect("already past the unix epoch")
75+
.as_millis() as u64;
76+
let current_slot =
77+
(now_ms.saturating_sub(genesis_time * 1000) / MILLISECONDS_PER_SLOT) as u32;
78+
key_manager.advance_keys_to(current_slot);
79+
6880
let handle = BlockChainServer {
6981
store,
7082
p2p: None,
@@ -195,6 +207,9 @@ impl BlockChainServer {
195207
metrics::update_safe_target_slot(self.store.safe_target_slot());
196208
// Update head slot metric (head may change when attestations are promoted at intervals 0/4)
197209
metrics::update_head_slot(self.store.head_slot());
210+
211+
// Advance XMSS keys for next slot so the signing paths don't have to
212+
self.key_manager.advance_keys_to((slot + 1) as u32);
198213
}
199214

200215
/// Kick off a committee-signature aggregation session:

0 commit comments

Comments
 (0)