diff --git a/Cargo.lock b/Cargo.lock index 6077800..a0d2b78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1813,6 +1813,7 @@ dependencies = [ "ethlambda-types", "prometheus", "spawned-concurrency", + "thiserror 2.0.17", "tracing", ] diff --git a/crates/blockchain/Cargo.toml b/crates/blockchain/Cargo.toml index fd0a09a..0926640 100644 --- a/crates/blockchain/Cargo.toml +++ b/crates/blockchain/Cargo.toml @@ -17,5 +17,6 @@ ethlambda-types.workspace = true spawned-concurrency.workspace = true tracing.workspace = true +thiserror.workspace = true prometheus.workspace = true diff --git a/crates/blockchain/src/lib.rs b/crates/blockchain/src/lib.rs index 3316194..5cfa86d 100644 --- a/crates/blockchain/src/lib.rs +++ b/crates/blockchain/src/lib.rs @@ -1,5 +1,5 @@ use ethlambda_storage::Store; -use ethlambda_types::{block::SignedBlockWithAttestation, primitives::TreeHash}; +use ethlambda_types::{block::{SignedBlockWithAttestation, VerifySignatureError}, primitives::TreeHash}; use spawned_concurrency::tasks::{CallResponse, CastResponse, GenServer, GenServerHandle}; use tracing::{error, warn}; @@ -7,6 +7,13 @@ pub struct BlockChain { handle: GenServerHandle, } + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Signed block signature verification failed")] + VerifySignatureError(#[from] VerifySignatureError), +} + impl BlockChain { pub fn spawn(store: Store) -> BlockChain { BlockChain { @@ -35,9 +42,9 @@ impl BlockChainServer { let slot = signed_block.message.block.slot; update_head_slot(slot); - let block = signed_block.message.block; - let proposer_attestation = signed_block.message.proposer_attestation; - let signatures = signed_block.signature; + let block = &signed_block.message.block; + let proposer_attestation = &signed_block.message.proposer_attestation; + let signatures = &signed_block.signature; let block_root = block.tree_hash_root(); @@ -51,7 +58,10 @@ impl BlockChainServer { return; }; - // TODO: validate block signatures + if let Err(err) = signed_block.verify_signatures(&pre_state) { + error!(%slot, %block_root, parent=%block.parent_root, "Signed block signature verification failed: {}", err); + return; + } let state_changes = ethlambda_state_transition::state_transition(&pre_state, &block); } diff --git a/crates/common/types/src/block.rs b/crates/common/types/src/block.rs index 8e8678f..5030a7e 100644 --- a/crates/common/types/src/block.rs +++ b/crates/common/types/src/block.rs @@ -1,12 +1,14 @@ +use leansig::{signature::SignatureScheme, serialization::Serializable}; use ssz_derive::{Decode, Encode}; use ssz_types::typenum::{Diff, U488, U3600, U4096}; +use tree_hash::TreeHash; use tree_hash_derive::TreeHash; - +use thiserror::Error; use crate::{ attestation::{Attestation, Attestations}, primitives::H256, - signature::{Signature, SignatureSize}, - state::ValidatorRegistryLimit, + signature::{LeanPublicKey, LeanSignatureScheme, Signature, SignatureSize}, + state::{State, ValidatorRegistryLimit}, }; /// Envelope carrying a block, an attestation from proposer, and aggregated signatures. @@ -27,6 +29,65 @@ pub struct SignedBlockWithAttestation { pub signature: BlockSignatures, } +#[derive(Error, Debug)] +pub enum VerifySignatureError { + #[error("Number of signatures {0} does not match number of attestations {1}")] + NumberOfSignaturesMismatch(usize, usize), + #[error("Validator {0} index out of range")] + ValidatorOutOfRange(u64), + #[error("Failed to deserialize public key")] + PublicKeyDeserializationError, + #[error("Failed to deserialize signature")] + SignatureDeserializationError, + #[error("Signature verification failed")] + SignatureVerificationFailed, +} + +impl SignedBlockWithAttestation { + /// Verify all XMSS signatures in this signed block. + + /// This function ensures that every attestation included in the block + /// (both on-chain attestations from the block body and the proposer's + /// own attestation) is properly signed by the claimed validator using + /// their registered XMSS public key. + pub fn verify_signatures(&self, parent_state: &State) -> Result { + let block = &self.message.block; + let signature = &self.signature; + + let all_attestations: Vec<_> = block.body.attestations + .iter() + .chain(std::iter::once(&self.message.proposer_attestation)) + .collect(); + + let validators = &parent_state.validators; + + if signature.len() != all_attestations.len() { + return Err(VerifySignatureError::NumberOfSignaturesMismatch(signature.len(), all_attestations.len())); + } + + for (attestation, signature) in all_attestations.iter().zip(signature.iter()) { + let validator_id = attestation.validator_id; + + // Ensure validator is in the active set + if validator_id < validators.len() as u64 { + return Err(VerifySignatureError::ValidatorOutOfRange(validator_id)); + } + + let validator = &validators[validator_id as usize]; + + // Verify the XMSS signature + let lean_public_key = LeanPublicKey::from_bytes(&validator.pubkey).map_err(|_| VerifySignatureError::PublicKeyDeserializationError)?; + let lean_signature = Signature::from_bytes(signature).map_err(|_| VerifySignatureError::SignatureDeserializationError)?; + if !LeanSignatureScheme::verify(&lean_public_key, attestation.data.slot as u32, &attestation.tree_hash_root(), &lean_signature) { + return Err(VerifySignatureError::SignatureVerificationFailed); + } + } + + + Ok(true) + } +} + // Manual Debug impl because leanSig signatures don't implement Debug. impl core::fmt::Debug for SignedBlockWithAttestation { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { diff --git a/crates/common/types/src/signature.rs b/crates/common/types/src/signature.rs index 4b91bb6..43b7112 100644 --- a/crates/common/types/src/signature.rs +++ b/crates/common/types/src/signature.rs @@ -1,10 +1,12 @@ use leansig::signature::SignatureScheme; use ssz_types::typenum::{Diff, U488, U3600}; -type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; +pub type LeanSignatureScheme = leansig::signature::generalized_xmss::instantiations_poseidon_top_level::lifetime_2_to_the_32::hashing_optimized::SIGTopLevelTargetSumLifetime32Dim64Base8; type LeanSigSignature = ::Signature; pub type Signature = LeanSigSignature; pub type SignatureSize = Diff; + +pub type LeanPublicKey = ::PublicKey;