From cccf6232224046e29e589ba50c5d751987f9bfa6 Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 28 Jul 2026 11:52:47 +0530 Subject: [PATCH 1/3] refactor: move signature logic to crypto crate --- crates/common/crypto/src/signature.rs | 203 +++++++++++++++++++++++++ crates/common/types/src/attestation.rs | 8 +- 2 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 crates/common/crypto/src/signature.rs diff --git a/crates/common/crypto/src/signature.rs b/crates/common/crypto/src/signature.rs new file mode 100644 index 00000000..7475928f --- /dev/null +++ b/crates/common/crypto/src/signature.rs @@ -0,0 +1,203 @@ +//! Validator XMSS signatures, public/secret keys, and the leansig-backed +//! primitives behind them. + +use std::ops::Range; + +use ethlambda_types::{primitives::H256, state::Validator}; +use leansig::{ + serialization::Serializable, + signature::{SignatureScheme, SignatureSchemeSecretKey as _, SigningError}, +}; + +/// The XMSS signature scheme used for validator signatures. +/// +/// This is a post-quantum secure signature scheme based on hash functions. +/// Uses Poseidon1 hashing with an aborting hypercube message hash, +/// 32-bit lifetime (2^32 signatures per key), dimension 46, and base 8. +pub type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_aborting::lifetime_2_to_the_32::SIGAbortingTargetSumLifetime32Dim46Base8; + +/// The public key type from the leansig library. +pub type LeanSigPublicKey = ::PublicKey; + +/// The signature type from the leansig library. +pub type LeanSigSignature = ::Signature; + +/// The secret key type from the leansig library. +pub type LeanSigSecretKey = ::SecretKey; + +pub type Signature = LeanSigSignature; + +/// Error returned when parsing signature or key bytes fails. +#[derive(Debug, Clone, thiserror::Error)] +#[error("signature parse error: {0}")] +pub struct SignatureParseError(pub String); + +#[derive(Clone)] +pub struct ValidatorSignature { + inner: LeanSigSignature, +} + +impl ValidatorSignature { + pub fn from_bytes(bytes: &[u8]) -> Result { + let sig = LeanSigSignature::from_bytes(bytes) + .map_err(|e| SignatureParseError(format!("{e:?}")))?; + Ok(Self { inner: sig }) + } + + pub fn to_bytes(&self) -> Vec { + self.inner.to_bytes() + } + + pub fn is_valid(&self, pubkey: &ValidatorPublicKey, slot: u32, message: &H256) -> bool { + LeanSignatureScheme::verify(&pubkey.inner, slot, &message.0, &self.inner) + } + + pub fn into_inner(self) -> LeanSigSignature { + self.inner + } +} + +#[derive(Clone)] +pub struct ValidatorPublicKey { + inner: LeanSigPublicKey, +} + +impl ValidatorPublicKey { + pub fn from_bytes(bytes: &[u8]) -> Result { + let pk = LeanSigPublicKey::from_bytes(bytes) + .map_err(|e| SignatureParseError(format!("{e:?}")))?; + Ok(Self { inner: pk }) + } + + pub fn to_bytes(&self) -> Vec { + self.inner.to_bytes() + } + + pub fn into_inner(self) -> LeanSigPublicKey { + self.inner + } +} + +/// Validator private key for signing attestations and blocks. +pub struct ValidatorSecretKey { + inner: LeanSigSecretKey, +} + +impl ValidatorSecretKey { + pub fn from_bytes(bytes: &[u8]) -> Result { + let sk = LeanSigSecretKey::from_bytes(bytes) + .map_err(|e| SignatureParseError(format!("{e:?}")))?; + Ok(Self { inner: sk }) + } + + /// Sign a message with this private key. + /// + /// The slot is used as part of the XMSS signature scheme to track + /// one-time signature usage. + pub fn sign(&self, slot: u32, message: &H256) -> Result { + let sig = LeanSignatureScheme::sign(&self.inner, slot, &message.0)?; + Ok(ValidatorSignature { inner: sig }) + } + + /// Returns true if the key is prepared to sign at the given slot. + /// + /// XMSS keys maintain a sliding window of two bottom trees. Only slots + /// within this window can be signed without advancing the preparation. + pub fn is_prepared_for(&self, slot: u32) -> bool { + self.inner.get_prepared_interval().contains(&(slot as u64)) + } + + /// Returns the slot range currently covered by the prepared window. + pub fn get_prepared_interval(&self) -> Range { + self.inner.get_prepared_interval() + } + + /// Advance the prepared window forward by one bottom tree. + /// + /// Each call slides the window by sqrt(LIFETIME) = 65,536 slots. + /// If the window is already at the end of the key's activation interval, + /// this is a no-op. + pub fn advance_preparation(&mut self) { + self.inner.advance_preparation(); + } +} + +/// Leansig-backed public-key access for [`Validator`]. +/// +/// `Validator` lives in `ethlambda-types`, which stays leansig-free, so these +/// helpers can't be inherent methods there. Import this trait to call +/// `validator.get_attestation_pubkey()` / `get_proposal_pubkey()` as before. +pub trait ValidatorPubkeys { + fn get_attestation_pubkey(&self) -> Result; + fn get_proposal_pubkey(&self) -> Result; +} + +impl ValidatorPubkeys for Validator { + fn get_attestation_pubkey(&self) -> Result { + ValidatorPublicKey::from_bytes(&self.attestation_pubkey) + } + + fn get_proposal_pubkey(&self) -> Result { + ValidatorPublicKey::from_bytes(&self.proposal_pubkey) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use leansig::serialization::Serializable; + use rand::{SeedableRng, rngs::StdRng}; + + const LEAVES_PER_BOTTOM_TREE: u32 = 1 << 16; // 65,536 + + /// Generate a ValidatorSecretKey with 3 bottom trees so advance_preparation can be tested. + /// + /// This is slow (~minutes) because it computes 3 bottom trees of 65,536 leaves each. + fn generate_key_with_three_bottom_trees() -> ValidatorSecretKey { + let mut rng = StdRng::seed_from_u64(42); + // Request enough active epochs for 3 bottom trees (> 2 * 65,536) + let num_active_epochs = (LEAVES_PER_BOTTOM_TREE as usize) * 2 + 1; + let (_pk, sk) = LeanSignatureScheme::key_gen(&mut rng, 0, num_active_epochs); + let sk_bytes = sk.to_bytes(); + ValidatorSecretKey::from_bytes(&sk_bytes).expect("valid secret key") + } + + #[test] + #[ignore = "slow: generates production-size XMSS key (~minutes)"] + fn test_advance_preparation_duration() { + println!("Generating XMSS key with 3 bottom trees (this takes a while)..."); + let keygen_start = std::time::Instant::now(); + let mut sk = generate_key_with_three_bottom_trees(); + println!("Key generation took: {:?}", keygen_start.elapsed()); + + // Initial window covers [0, 131072) + assert!(sk.is_prepared_for(0)); + assert!(sk.is_prepared_for(LEAVES_PER_BOTTOM_TREE - 1)); + assert!(sk.is_prepared_for(2 * LEAVES_PER_BOTTOM_TREE - 1)); + assert!(!sk.is_prepared_for(2 * LEAVES_PER_BOTTOM_TREE)); + + // Time the advance_preparation call + let advance_start = std::time::Instant::now(); + sk.advance_preparation(); + let advance_duration = advance_start.elapsed(); + + println!("advance_preparation() took: {advance_duration:?}"); + + // Window should now cover [65536, 196608) + assert!(!sk.is_prepared_for(0)); + assert!(sk.is_prepared_for(LEAVES_PER_BOTTOM_TREE)); + assert!(sk.is_prepared_for(3 * LEAVES_PER_BOTTOM_TREE - 1)); + + // Verify signing works in the new window + let message = H256::from([42u8; 32]); + let slot = 2 * LEAVES_PER_BOTTOM_TREE; // slot 131,072 — the one that crashed the devnet + let sign_start = std::time::Instant::now(); + let result = sk.sign(slot, &message); + println!("Signing at slot {slot} took: {:?}", sign_start.elapsed()); + assert!( + result.is_ok(), + "signing should succeed after advance: {}", + result.err().map_or(String::new(), |e| e.to_string()) + ); + } +} diff --git a/crates/common/types/src/attestation.rs b/crates/common/types/src/attestation.rs index 0d68addb..91d00105 100644 --- a/crates/common/types/src/attestation.rs +++ b/crates/common/types/src/attestation.rs @@ -7,7 +7,6 @@ use crate::{ block::SingleMessageAggregate, checkpoint::Checkpoint, primitives::{H256, HashTreeRoot as _}, - signature::SIGNATURE_SIZE, }; /// Validator specific attestation wrapping shared attestation data. @@ -55,6 +54,13 @@ pub struct SignedAttestation { pub signature: XmssSignature, } +/// Size of an XMSS signature in bytes. +/// +/// Computed from: path(32*8*4) + rho(7*4) + hashes(46*8*4) + ssz_offsets(3*4) = 2536. +/// This is the SSZ wire size, independent of the `leansig` scheme itself, so it +/// lives here (leansig-free) rather than in `ethlambda-crypto`. +pub const SIGNATURE_SIZE: usize = 2536; + /// XMSS signature as a fixed-length byte vector (`SIGNATURE_SIZE` bytes). pub type XmssSignature = SszVector; From dab4fd839df476ce995b1e67841908bf3edc559a Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Tue, 28 Jul 2026 11:54:17 +0530 Subject: [PATCH 2/3] refactor: update imports for signatures to crypto crate --- Cargo.lock | 2 +- bin/ethlambda/src/main.rs | 2 +- crates/blockchain/src/aggregation.rs | 4 +- crates/blockchain/src/block_builder.rs | 2 +- crates/blockchain/src/key_manager.rs | 2 +- crates/blockchain/src/lib.rs | 2 +- crates/blockchain/src/reaggregate.rs | 2 +- crates/blockchain/src/store.rs | 2 +- crates/common/crypto/src/lib.rs | 12 +- crates/common/test-fixtures/src/common.rs | 3 +- crates/common/types/Cargo.toml | 2 - crates/common/types/src/lib.rs | 1 - crates/common/types/src/signature.rs | 186 ---------------------- crates/common/types/src/state.rs | 11 -- crates/storage/Cargo.toml | 1 + crates/storage/src/store.rs | 4 +- 16 files changed, 19 insertions(+), 219 deletions(-) delete mode 100644 crates/common/types/src/signature.rs diff --git a/Cargo.lock b/Cargo.lock index ddd13c0c..7b514a8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2169,6 +2169,7 @@ dependencies = [ name = "ethlambda-storage" version = "0.1.0" dependencies = [ + "ethlambda-crypto", "ethlambda-types", "leansig", "libssz", @@ -2201,7 +2202,6 @@ dependencies = [ "datatest-stable 0.3.3", "ethlambda-test-fixtures", "hex", - "leansig", "libssz", "libssz-derive", "libssz-merkle", diff --git a/bin/ethlambda/src/main.rs b/bin/ethlambda/src/main.rs index 33e242e9..df4893e6 100644 --- a/bin/ethlambda/src/main.rs +++ b/bin/ethlambda/src/main.rs @@ -36,6 +36,7 @@ use cli::CliOptions; use ethlambda_blockchain::MILLISECONDS_PER_SLOT; use ethlambda_blockchain::block_builder::ProposerConfig; use ethlambda_blockchain::key_manager::ValidatorKeyPair; +use ethlambda_crypto::signature::ValidatorSecretKey; use ethlambda_network_api::{InitBlockChain, InitP2P, ToBlockChainToP2PRef, ToP2PToBlockChainRef}; use ethlambda_p2p::{ Bootnode, P2P, PeerId, SwarmConfig, attestation_subscription_subnets, build_swarm, parse_enrs, @@ -44,7 +45,6 @@ use ethlambda_types::primitives::{H256, HashTreeRoot as _}; use ethlambda_types::{ aggregator::AggregatorController, genesis::GenesisConfig, - signature::ValidatorSecretKey, state::{State, ValidatorPubkeyBytes}, }; use eyre::WrapErr; diff --git a/crates/blockchain/src/aggregation.rs b/crates/blockchain/src/aggregation.rs index 2a62b2a6..f3ec6bbe 100644 --- a/crates/blockchain/src/aggregation.rs +++ b/crates/blockchain/src/aggregation.rs @@ -20,13 +20,13 @@ use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant, SystemTime}; use ethlambda_crypto::aggregate_mixed; +use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey, ValidatorSignature}; use ethlambda_storage::Store; use ethlambda_types::{ ShortRoot, attestation::{AggregationBits, AttestationData, HashedAttestationData}, block::{ByteList512KiB, SingleMessageAggregate}, primitives::H256, - signature::{ValidatorPublicKey, ValidatorSignature}, state::Validator, }; use spawned_concurrency::message::Message; @@ -796,7 +796,7 @@ mod tests { /// never checks signature validity, only that it clones and carries a /// resolvable id — mirrors `ethlambda_storage::store::tests::make_dummy_sig`. fn dummy_sig() -> ValidatorSignature { - use ethlambda_types::signature::LeanSignatureScheme; + use ethlambda_crypto::signature::LeanSignatureScheme; use leansig::{serialization::Serializable, signature::SignatureScheme}; use rand::{SeedableRng, rngs::StdRng}; diff --git a/crates/blockchain/src/block_builder.rs b/crates/blockchain/src/block_builder.rs index 787c55b4..b6adec9f 100644 --- a/crates/blockchain/src/block_builder.rs +++ b/crates/blockchain/src/block_builder.rs @@ -15,7 +15,7 @@ use std::{ time::Instant, }; -use ethlambda_crypto::aggregate_proofs; +use ethlambda_crypto::{aggregate_proofs, signature::ValidatorPubkeys}; use ethlambda_state_transition::{ attestation_data_matches_chain, justified_slots_ops, process_block, process_slots, slot_is_justifiable_after, diff --git a/crates/blockchain/src/key_manager.rs b/crates/blockchain/src/key_manager.rs index cb58c354..eb2777a8 100644 --- a/crates/blockchain/src/key_manager.rs +++ b/crates/blockchain/src/key_manager.rs @@ -1,10 +1,10 @@ use std::collections::HashMap; use std::time::Instant; +use ethlambda_crypto::signature::{ValidatorSecretKey, ValidatorSignature}; use ethlambda_types::{ attestation::{AttestationData, XmssSignature}, primitives::{H256, HashTreeRoot as _}, - signature::{ValidatorSecretKey, ValidatorSignature}, }; use tracing::{info, warn}; diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index b2929ab6..a06d82f9 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::time::{Duration, Instant, SystemTime}; +use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey, ValidatorSignature}; use ethlambda_network_api::{BlockChainToP2PRef, InitP2P}; use ethlambda_state_transition::is_proposer; use ethlambda_storage::{ALL_TABLES, Store}; @@ -10,7 +11,6 @@ use ethlambda_types::{ attestation::{SignedAggregatedAttestation, SignedAttestation}, block::{ByteList512KiB, MultiMessageAggregate, SignedBlock}, primitives::{H256, HashTreeRoot as _}, - signature::{ValidatorPublicKey, ValidatorSignature}, }; use crate::aggregation::{ diff --git a/crates/blockchain/src/reaggregate.rs b/crates/blockchain/src/reaggregate.rs index f47d6c51..aa36143b 100644 --- a/crates/blockchain/src/reaggregate.rs +++ b/crates/blockchain/src/reaggregate.rs @@ -24,6 +24,7 @@ use std::collections::HashSet; +use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey}; use ethlambda_storage::Store; use ethlambda_types::{ attestation::{ @@ -32,7 +33,6 @@ use ethlambda_types::{ }, block::{SignedBlock, SingleMessageAggregate}, primitives::{H256, HashTreeRoot as _}, - signature::ValidatorPublicKey, }; use tracing::{debug, warn}; diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index 2bdf46b0..c0e3e7ce 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -1,5 +1,6 @@ use std::collections::{HashMap, HashSet}; +use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey, ValidatorSignature}; use ethlambda_state_transition::{is_proposer, slot_is_justifiable_after}; use ethlambda_storage::{ForkCheckpoints, Store}; use ethlambda_types::{ @@ -11,7 +12,6 @@ use ethlambda_types::{ block::{Block, BlockHeader, SignedBlock, SingleMessageAggregate}, checkpoint::Checkpoint, primitives::{H256, HashTreeRoot as _}, - signature::{ValidatorPublicKey, ValidatorSignature}, state::{HISTORICAL_ROOTS_LIMIT, State}, }; use tracing::{info, trace, warn}; diff --git a/crates/common/crypto/src/lib.rs b/crates/common/crypto/src/lib.rs index 4e081445..51d7d282 100644 --- a/crates/common/crypto/src/lib.rs +++ b/crates/common/crypto/src/lib.rs @@ -1,10 +1,8 @@ use std::sync::Once; -use ethlambda_types::{ - block::ByteList512KiB, - primitives::H256, - signature::{ValidatorPublicKey, ValidatorSignature}, -}; +use ethlambda_types::{block::ByteList512KiB, primitives::H256}; + +use crate::signature::{ValidatorPublicKey, ValidatorSignature}; use lean_multisig::{ MultiMessageAggregateSignature as LMType2, ProofError, SingleMessageAggregateSignature as LMType1, aggregate_single_message_signatures, @@ -14,6 +12,8 @@ use lean_multisig::{ use leansig_wrapper::{XmssPublicKey as LeanSigPubKey, XmssSignature as LeanSigSignature}; use thiserror::Error; +pub mod signature; + #[cfg(feature = "shadow-integration")] pub mod shadow_cost; @@ -508,7 +508,7 @@ pub fn split_type_2_by_message( #[cfg(test)] mod tests { use super::*; - use ethlambda_types::signature::LeanSignatureScheme; + use crate::signature::LeanSignatureScheme; use leansig::{serialization::Serializable, signature::SignatureScheme}; use rand::{SeedableRng, rngs::StdRng}; diff --git a/crates/common/test-fixtures/src/common.rs b/crates/common/test-fixtures/src/common.rs index 6293b897..6705292b 100644 --- a/crates/common/test-fixtures/src/common.rs +++ b/crates/common/test-fixtures/src/common.rs @@ -2,12 +2,11 @@ use ethlambda_types::{ attestation::{ AggregatedAttestation as DomainAggregatedAttestation, AggregationBits as DomainAggregationBits, AttestationData as DomainAttestationData, - XmssSignature, + SIGNATURE_SIZE, XmssSignature, }, block::{Block as DomainBlock, BlockBody as DomainBlockBody}, checkpoint::Checkpoint as DomainCheckpoint, primitives::H256, - signature::SIGNATURE_SIZE, state::{ ChainConfig, JustificationValidators, JustifiedSlots, State, Validator as DomainValidator, ValidatorPubkeyBytes, diff --git a/crates/common/types/Cargo.toml b/crates/common/types/Cargo.toml index 1de09a67..383f77a3 100644 --- a/crates/common/types/Cargo.toml +++ b/crates/common/types/Cargo.toml @@ -14,8 +14,6 @@ thiserror.workspace = true serde.workspace = true hex.workspace = true -leansig.workspace = true - libssz.workspace = true libssz-derive.workspace = true libssz-merkle.workspace = true diff --git a/crates/common/types/src/lib.rs b/crates/common/types/src/lib.rs index 6db9a590..88ba98b9 100644 --- a/crates/common/types/src/lib.rs +++ b/crates/common/types/src/lib.rs @@ -5,7 +5,6 @@ pub mod checkpoint; pub mod constants; pub mod genesis; pub mod primitives; -pub mod signature; pub mod state; /// Display helper for truncated root hashes (8 hex chars) diff --git a/crates/common/types/src/signature.rs b/crates/common/types/src/signature.rs deleted file mode 100644 index fa9c8da8..00000000 --- a/crates/common/types/src/signature.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::ops::Range; - -use leansig::{ - serialization::Serializable, - signature::{SignatureScheme, SignatureSchemeSecretKey as _, SigningError}, -}; - -use crate::primitives::H256; - -/// The XMSS signature scheme used for validator signatures. -/// -/// This is a post-quantum secure signature scheme based on hash functions. -/// Uses Poseidon1 hashing with an aborting hypercube message hash, -/// 32-bit lifetime (2^32 signatures per key), dimension 46, and base 8. -pub type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_aborting::lifetime_2_to_the_32::SIGAbortingTargetSumLifetime32Dim46Base8; - -/// The public key type from the leansig library. -pub type LeanSigPublicKey = ::PublicKey; - -/// The signature type from the leansig library. -pub type LeanSigSignature = ::Signature; - -/// The secret key type from the leansig library. -pub type LeanSigSecretKey = ::SecretKey; - -pub type Signature = LeanSigSignature; - -/// Size of an XMSS signature in bytes. -/// -/// Computed from: path(32*8*4) + rho(7*4) + hashes(46*8*4) + ssz_offsets(3*4) = 2536 -pub const SIGNATURE_SIZE: usize = 2536; - -/// Error returned when parsing signature or key bytes fails. -#[derive(Debug, Clone, thiserror::Error)] -#[error("signature parse error: {0}")] -pub struct SignatureParseError(pub String); - -#[derive(Clone)] -pub struct ValidatorSignature { - inner: LeanSigSignature, -} - -impl ValidatorSignature { - pub fn from_bytes(bytes: &[u8]) -> Result { - let sig = LeanSigSignature::from_bytes(bytes) - .map_err(|e| SignatureParseError(format!("{e:?}")))?; - Ok(Self { inner: sig }) - } - - pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes() - } - - pub fn is_valid(&self, pubkey: &ValidatorPublicKey, slot: u32, message: &H256) -> bool { - LeanSignatureScheme::verify(&pubkey.inner, slot, &message.0, &self.inner) - } - - pub fn into_inner(self) -> LeanSigSignature { - self.inner - } -} - -#[derive(Clone)] -pub struct ValidatorPublicKey { - inner: LeanSigPublicKey, -} - -impl ValidatorPublicKey { - pub fn from_bytes(bytes: &[u8]) -> Result { - let pk = LeanSigPublicKey::from_bytes(bytes) - .map_err(|e| SignatureParseError(format!("{e:?}")))?; - Ok(Self { inner: pk }) - } - - pub fn to_bytes(&self) -> Vec { - self.inner.to_bytes() - } - - pub fn into_inner(self) -> LeanSigPublicKey { - self.inner - } -} - -/// Validator private key for signing attestations and blocks. -pub struct ValidatorSecretKey { - inner: LeanSigSecretKey, -} - -impl ValidatorSecretKey { - pub fn from_bytes(bytes: &[u8]) -> Result { - let sk = LeanSigSecretKey::from_bytes(bytes) - .map_err(|e| SignatureParseError(format!("{e:?}")))?; - Ok(Self { inner: sk }) - } - - /// Sign a message with this private key. - /// - /// The slot is used as part of the XMSS signature scheme to track - /// one-time signature usage. - pub fn sign(&self, slot: u32, message: &H256) -> Result { - let sig = LeanSignatureScheme::sign(&self.inner, slot, &message.0)?; - Ok(ValidatorSignature { inner: sig }) - } - - /// Returns true if the key is prepared to sign at the given slot. - /// - /// XMSS keys maintain a sliding window of two bottom trees. Only slots - /// within this window can be signed without advancing the preparation. - pub fn is_prepared_for(&self, slot: u32) -> bool { - self.inner.get_prepared_interval().contains(&(slot as u64)) - } - - /// Returns the slot range currently covered by the prepared window. - pub fn get_prepared_interval(&self) -> Range { - self.inner.get_prepared_interval() - } - - /// Advance the prepared window forward by one bottom tree. - /// - /// Each call slides the window by sqrt(LIFETIME) = 65,536 slots. - /// If the window is already at the end of the key's activation interval, - /// this is a no-op. - pub fn advance_preparation(&mut self) { - self.inner.advance_preparation(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use leansig::serialization::Serializable; - use rand::{SeedableRng, rngs::StdRng}; - - const LEAVES_PER_BOTTOM_TREE: u32 = 1 << 16; // 65,536 - - /// Generate a ValidatorSecretKey with 3 bottom trees so advance_preparation can be tested. - /// - /// This is slow (~minutes) because it computes 3 bottom trees of 65,536 leaves each. - fn generate_key_with_three_bottom_trees() -> ValidatorSecretKey { - let mut rng = StdRng::seed_from_u64(42); - // Request enough active epochs for 3 bottom trees (> 2 * 65,536) - let num_active_epochs = (LEAVES_PER_BOTTOM_TREE as usize) * 2 + 1; - let (_pk, sk) = LeanSignatureScheme::key_gen(&mut rng, 0, num_active_epochs); - let sk_bytes = sk.to_bytes(); - ValidatorSecretKey::from_bytes(&sk_bytes).expect("valid secret key") - } - - #[test] - #[ignore = "slow: generates production-size XMSS key (~minutes)"] - fn test_advance_preparation_duration() { - println!("Generating XMSS key with 3 bottom trees (this takes a while)..."); - let keygen_start = std::time::Instant::now(); - let mut sk = generate_key_with_three_bottom_trees(); - println!("Key generation took: {:?}", keygen_start.elapsed()); - - // Initial window covers [0, 131072) - assert!(sk.is_prepared_for(0)); - assert!(sk.is_prepared_for(LEAVES_PER_BOTTOM_TREE - 1)); - assert!(sk.is_prepared_for(2 * LEAVES_PER_BOTTOM_TREE - 1)); - assert!(!sk.is_prepared_for(2 * LEAVES_PER_BOTTOM_TREE)); - - // Time the advance_preparation call - let advance_start = std::time::Instant::now(); - sk.advance_preparation(); - let advance_duration = advance_start.elapsed(); - - println!("advance_preparation() took: {advance_duration:?}"); - - // Window should now cover [65536, 196608) - assert!(!sk.is_prepared_for(0)); - assert!(sk.is_prepared_for(LEAVES_PER_BOTTOM_TREE)); - assert!(sk.is_prepared_for(3 * LEAVES_PER_BOTTOM_TREE - 1)); - - // Verify signing works in the new window - let message = H256::from([42u8; 32]); - let slot = 2 * LEAVES_PER_BOTTOM_TREE; // slot 131,072 — the one that crashed the devnet - let sign_start = std::time::Instant::now(); - let result = sk.sign(slot, &message); - println!("Signing at slot {slot} took: {:?}", sign_start.elapsed()); - assert!( - result.is_ok(), - "signing should succeed after advance: {}", - result.err().map_or(String::new(), |e| e.to_string()) - ); - } -} diff --git a/crates/common/types/src/state.rs b/crates/common/types/src/state.rs index 26ff110d..6cc25bb4 100644 --- a/crates/common/types/src/state.rs +++ b/crates/common/types/src/state.rs @@ -6,7 +6,6 @@ use crate::{ block::{Block, BlockBody, BlockHeader}, checkpoint::Checkpoint, primitives::{self, H256}, - signature::{SignatureParseError, ValidatorPublicKey}, }; // Convenience trait for calling hash_tree_root() without a hasher argument @@ -85,16 +84,6 @@ where serializer.serialize_str(&hex::encode(pubkey)) } -impl Validator { - pub fn get_attestation_pubkey(&self) -> Result { - ValidatorPublicKey::from_bytes(&self.attestation_pubkey) - } - - pub fn get_proposal_pubkey(&self) -> Result { - ValidatorPublicKey::from_bytes(&self.proposal_pubkey) - } -} - pub type ValidatorPubkeyBytes = [u8; 52]; impl State { diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 7381a53e..035d802a 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true version.workspace = true [dependencies] +ethlambda-crypto.workspace = true ethlambda-types.workspace = true tracing.workspace = true diff --git a/crates/storage/src/store.rs b/crates/storage/src/store.rs index 83f1e8fc..d5cac0ed 100644 --- a/crates/storage/src/store.rs +++ b/crates/storage/src/store.rs @@ -7,6 +7,7 @@ use lru::LruCache; use crate::api::{StorageBackend, StorageReadView, StorageWriteBatch, Table}; use crate::error::Error; +use ethlambda_crypto::signature::ValidatorSignature; use ethlambda_types::{ attestation::{AggregationBits, AttestationData, HashedAttestationData, bits_is_subset}, block::{ @@ -14,7 +15,6 @@ use ethlambda_types::{ }, checkpoint::Checkpoint, primitives::{H256, HashTreeRoot as _}, - signature::ValidatorSignature, state::{ChainConfig, State, anchor_pair_is_consistent}, }; use libssz::{SszDecode, SszEncode}; @@ -2695,7 +2695,7 @@ mod tests { // ============ GossipSignatureBuffer Tests ============ fn make_dummy_sig() -> ValidatorSignature { - use ethlambda_types::signature::LeanSignatureScheme; + use ethlambda_crypto::signature::LeanSignatureScheme; use leansig::{serialization::Serializable, signature::SignatureScheme}; use rand::{SeedableRng, rngs::StdRng}; From dbcfd1c269d14eb5ce6c03cbee9dc32fbfcc841a Mon Sep 17 00:00:00 2001 From: Sahilgill24 Date: Wed, 29 Jul 2026 06:01:53 +0530 Subject: [PATCH 3/3] refactor: inline validator public key calls --- crates/blockchain/src/aggregation.rs | 9 ++++++--- crates/blockchain/src/block_builder.rs | 8 ++++---- crates/blockchain/src/lib.rs | 9 ++++++--- crates/blockchain/src/reaggregate.rs | 13 +++++++++---- crates/blockchain/src/store.rs | 18 ++++++++---------- crates/common/crypto/src/signature.rs | 22 +--------------------- 6 files changed, 34 insertions(+), 45 deletions(-) diff --git a/crates/blockchain/src/aggregation.rs b/crates/blockchain/src/aggregation.rs index f3ec6bbe..4d5049b8 100644 --- a/crates/blockchain/src/aggregation.rs +++ b/crates/blockchain/src/aggregation.rs @@ -20,7 +20,7 @@ use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant, SystemTime}; use ethlambda_crypto::aggregate_mixed; -use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey, ValidatorSignature}; +use ethlambda_crypto::signature::{ValidatorPublicKey, ValidatorSignature}; use ethlambda_storage::Store; use ethlambda_types::{ ShortRoot, @@ -425,7 +425,7 @@ fn resolve_job( let Some(validator) = validators.get(*vid as usize) else { continue; }; - let Ok(pubkey) = validator.get_attestation_pubkey() else { + let Ok(pubkey) = ValidatorPublicKey::from_bytes(&validator.attestation_pubkey) else { continue; }; raw_by_id.insert(*vid, (pubkey, sig.clone())); @@ -492,7 +492,10 @@ fn resolve_child_pubkeys( let participant_ids: Vec = proof.participant_indices().collect(); let child_pubkeys: Vec = participant_ids .iter() - .filter_map(|&vid| validators.get(vid as usize)?.get_attestation_pubkey().ok()) + .filter_map(|&vid| { + let v = validators.get(vid as usize)?; + ValidatorPublicKey::from_bytes(&v.attestation_pubkey).ok() + }) .collect(); if child_pubkeys.len() != participant_ids.len() { warn!( diff --git a/crates/blockchain/src/block_builder.rs b/crates/blockchain/src/block_builder.rs index b6adec9f..4f0d62a6 100644 --- a/crates/blockchain/src/block_builder.rs +++ b/crates/blockchain/src/block_builder.rs @@ -15,7 +15,7 @@ use std::{ time::Instant, }; -use ethlambda_crypto::{aggregate_proofs, signature::ValidatorPubkeys}; +use ethlambda_crypto::{aggregate_proofs, signature::ValidatorPublicKey}; use ethlambda_state_transition::{ attestation_data_matches_chain, justified_slots_ops, process_block, process_slots, slot_is_justifiable_after, @@ -657,11 +657,11 @@ fn compact_attestations( let pubkeys = proof .participant_indices() .map(|vid| { - head_state + let validator = head_state .validators .get(vid as usize) - .ok_or(StoreError::InvalidValidatorIndex)? - .get_attestation_pubkey() + .ok_or(StoreError::InvalidValidatorIndex)?; + ValidatorPublicKey::from_bytes(&validator.attestation_pubkey) .map_err(|_| StoreError::PubkeyDecodingFailed(vid)) }) .collect::, _>>()?; diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index a06d82f9..abace771 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -1,7 +1,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::time::{Duration, Instant, SystemTime}; -use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey, ValidatorSignature}; +use ethlambda_crypto::signature::{ValidatorPublicKey, ValidatorSignature}; use ethlambda_network_api::{BlockChainToP2PRef, InitP2P}; use ethlambda_state_transition::is_proposer; use ethlambda_storage::{ALL_TABLES, Store}; @@ -763,7 +763,10 @@ impl BlockChainServer { // Decode the proposer's proposal pubkey once and reuse it both for the // singleton single-message aggregate wrap and for the multi-message // aggregate merge inputs. - let Ok(proposer_pubkey) = proposer_validator.get_proposal_pubkey().inspect_err( + let Ok(proposer_pubkey) = ValidatorPublicKey::from_bytes( + &proposer_validator.proposal_pubkey, + ) + .inspect_err( |err| error!(%slot, %validator_id, %err, "Failed to decode proposer proposal pubkey"), ) else { metrics::inc_block_building_failures(); @@ -802,7 +805,7 @@ impl BlockChainServer { resolve_failed = true; break; }; - match validator.get_attestation_pubkey() { + match ValidatorPublicKey::from_bytes(&validator.attestation_pubkey) { Ok(pk) => pubkeys.push(pk), Err(err) => { error!(%slot, %validator_id, vid, %err, "Failed to decode attestation pubkey"); diff --git a/crates/blockchain/src/reaggregate.rs b/crates/blockchain/src/reaggregate.rs index aa36143b..64e762e7 100644 --- a/crates/blockchain/src/reaggregate.rs +++ b/crates/blockchain/src/reaggregate.rs @@ -24,7 +24,7 @@ use std::collections::HashSet; -use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey}; +use ethlambda_crypto::signature::ValidatorPublicKey; use ethlambda_storage::Store; use ethlambda_types::{ attestation::{ @@ -83,7 +83,9 @@ pub fn reaggregate_from_block( warn!(vid, "Reaggregation aborted: participant out of range"); return Vec::new(); } - let Ok(pk) = validators[vid as usize].get_attestation_pubkey() else { + let Ok(pk) = + ValidatorPublicKey::from_bytes(&validators[vid as usize].attestation_pubkey) + else { warn!(vid, "Reaggregation aborted: bad attestation pubkey"); return Vec::new(); }; @@ -94,7 +96,8 @@ pub fn reaggregate_from_block( if block.proposer_index >= num_validators { return Vec::new(); } - let Ok(proposer_pubkey) = validators[block.proposer_index as usize].get_proposal_pubkey() + let Ok(proposer_pubkey) = + ValidatorPublicKey::from_bytes(&validators[block.proposer_index as usize].proposal_pubkey) else { return Vec::new(); }; @@ -161,7 +164,9 @@ pub fn reaggregate_from_block( bad = true; break; } - match validators[vid as usize].get_attestation_pubkey() { + match ValidatorPublicKey::from_bytes( + &validators[vid as usize].attestation_pubkey, + ) { Ok(pk) => pubkeys.push(pk), Err(_) => { bad = true; diff --git a/crates/blockchain/src/store.rs b/crates/blockchain/src/store.rs index c0e3e7ce..1a6d3884 100644 --- a/crates/blockchain/src/store.rs +++ b/crates/blockchain/src/store.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use ethlambda_crypto::signature::{ValidatorPubkeys, ValidatorPublicKey, ValidatorSignature}; +use ethlambda_crypto::signature::{ValidatorPublicKey, ValidatorSignature}; use ethlambda_state_transition::{is_proposer, slot_is_justifiable_after}; use ethlambda_storage::{ForkCheckpoints, Store}; use ethlambda_types::{ @@ -418,9 +418,10 @@ pub fn on_gossip_attestation( if validator_id >= target_state.validators.len() as u64 { return Err(StoreError::InvalidValidatorIndex); } - let validator_pubkey = target_state.validators[validator_id as usize] - .get_attestation_pubkey() - .map_err(|_| StoreError::PubkeyDecodingFailed(validator_id))?; + let validator_pubkey = ValidatorPublicKey::from_bytes( + &target_state.validators[validator_id as usize].attestation_pubkey, + ) + .map_err(|_| StoreError::PubkeyDecodingFailed(validator_id))?; // Verify the validator's XMSS signature let slot: u32 = attestation.data.slot.try_into().expect("slot exceeds u32"); @@ -513,8 +514,7 @@ fn on_gossip_aggregated_attestation_core( let pubkeys: Vec<_> = participant_indices .iter() .map(|&vid| { - validators[vid as usize] - .get_attestation_pubkey() + ValidatorPublicKey::from_bytes(&validators[vid as usize].attestation_pubkey) .map_err(|_| StoreError::PubkeyDecodingFailed(vid)) }) .collect::>()?; @@ -1142,8 +1142,7 @@ pub fn verify_block_signatures( let validator = validators .get(vid as usize) .ok_or(StoreError::InvalidValidatorIndex)?; - let pk = validator - .get_attestation_pubkey() + let pk = ValidatorPublicKey::from_bytes(&validator.attestation_pubkey) .map_err(|_| StoreError::PubkeyDecodingFailed(vid))?; pubkeys.push(pk); } @@ -1156,8 +1155,7 @@ pub fn verify_block_signatures( let proposer_validator = validators .get(block.proposer_index as usize) .ok_or(StoreError::InvalidValidatorIndex)?; - let proposer_pubkey = proposer_validator - .get_proposal_pubkey() + let proposer_pubkey = ValidatorPublicKey::from_bytes(&proposer_validator.proposal_pubkey) .map_err(|_| StoreError::PubkeyDecodingFailed(block.proposer_index))?; pubkeys_per_component.push(vec![proposer_pubkey]); let block_slot_u32 = diff --git a/crates/common/crypto/src/signature.rs b/crates/common/crypto/src/signature.rs index 7475928f..39515e53 100644 --- a/crates/common/crypto/src/signature.rs +++ b/crates/common/crypto/src/signature.rs @@ -3,7 +3,7 @@ use std::ops::Range; -use ethlambda_types::{primitives::H256, state::Validator}; +use ethlambda_types::primitives::H256; use leansig::{ serialization::Serializable, signature::{SignatureScheme, SignatureSchemeSecretKey as _, SigningError}, @@ -122,26 +122,6 @@ impl ValidatorSecretKey { } } -/// Leansig-backed public-key access for [`Validator`]. -/// -/// `Validator` lives in `ethlambda-types`, which stays leansig-free, so these -/// helpers can't be inherent methods there. Import this trait to call -/// `validator.get_attestation_pubkey()` / `get_proposal_pubkey()` as before. -pub trait ValidatorPubkeys { - fn get_attestation_pubkey(&self) -> Result; - fn get_proposal_pubkey(&self) -> Result; -} - -impl ValidatorPubkeys for Validator { - fn get_attestation_pubkey(&self) -> Result { - ValidatorPublicKey::from_bytes(&self.attestation_pubkey) - } - - fn get_proposal_pubkey(&self) -> Result { - ValidatorPublicKey::from_bytes(&self.proposal_pubkey) - } -} - #[cfg(test)] mod tests { use super::*;