use crate::keys::bls::{BlsKeypair, BlsSignature};
use crate::metrics;
use crate::policy::ethereum::EthereumPolicy;
use crate::policy::types::{PolicyDecision, RefusalCode, SigningType};
use crate::state::integrity::{DecisionRecord, IntegrityError, SigningContext, StateIntegrity};
use crate::state::validator::ValidatorState;
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Instant;
use thiserror::Error;
pub struct SigningService {
keypairs: RwLock<HashMap<[u8; 48], BlsKeypair>>,
validators: RwLock<HashMap<[u8; 48], ValidatorState>>,
integrity: RwLock<StateIntegrity>,
policy: EthereumPolicy,
genesis_validators_root: RwLock<Option<[u8; 32]>>,
}
impl SigningService {
pub fn new(keypairs: Vec<BlsKeypair>) -> Self {
let keypair_map: HashMap<[u8; 48], BlsKeypair> = keypairs
.into_iter()
.map(|kp| {
let pubkey = kp.public_key_bytes();
(pubkey, kp)
})
.collect();
Self {
keypairs: RwLock::new(keypair_map),
validators: RwLock::new(HashMap::new()),
integrity: RwLock::new(StateIntegrity::new()),
policy: EthereumPolicy::new(),
genesis_validators_root: RwLock::new(None),
}
}
pub fn with_state(
keypairs: Vec<BlsKeypair>,
validators: HashMap<[u8; 48], ValidatorState>,
integrity: StateIntegrity,
) -> Self {
let keypair_map: HashMap<[u8; 48], BlsKeypair> = keypairs
.into_iter()
.map(|kp| {
let pubkey = kp.public_key_bytes();
(pubkey, kp)
})
.collect();
let genesis_root = integrity.genesis_validators_root;
Self {
keypairs: RwLock::new(keypair_map),
validators: RwLock::new(validators),
integrity: RwLock::new(integrity),
policy: EthereumPolicy::new(),
genesis_validators_root: RwLock::new(genesis_root),
}
}
pub fn public_keys(&self) -> Vec<[u8; 48]> {
self.keypairs.read().unwrap().keys().copied().collect()
}
pub fn public_keys_hex(&self) -> Vec<String> {
self.keypairs
.read()
.unwrap()
.keys()
.map(|pk| format!("0x{}", hex::encode(pk)))
.collect()
}
pub fn has_validator(&self, pubkey: &[u8; 48]) -> bool {
self.keypairs.read().unwrap().contains_key(pubkey)
}
pub fn add_keys(&self, keypairs: Vec<BlsKeypair>) -> usize {
let mut keypair_map = self.keypairs.write().unwrap();
let initial_count = keypair_map.len();
for kp in keypairs {
let pubkey = kp.public_key_bytes();
keypair_map.entry(pubkey).or_insert(kp);
}
keypair_map.len() - initial_count
}
pub fn validator_count(&self) -> usize {
self.keypairs.read().unwrap().len()
}
pub fn set_genesis_validators_root(
&self,
root: [u8; 32],
) -> Result<(), SigningServiceError> {
let mut genesis_root = self.genesis_validators_root.write().unwrap();
match *genesis_root {
Some(existing) if existing != root => {
Err(SigningServiceError::GenesisRootMismatch {
expected: existing,
actual: root,
})
}
Some(_) => Ok(()), None => {
*genesis_root = Some(root);
let mut integrity = self.integrity.write().unwrap();
integrity
.set_genesis_validators_root(root)
.map_err(SigningServiceError::Integrity)?;
Ok(())
}
}
}
pub fn sign_block_proposal(
&self,
pubkey: &[u8; 48],
slot: u64,
signing_root: [u8; 32],
) -> Result<SigningResult, SigningServiceError> {
let start = Instant::now();
let validator_hex = format!("0x{}...{}", &hex::encode(&pubkey[..4]), &hex::encode(&pubkey[44..]));
let keypairs = self.keypairs.read().unwrap();
let keypair = keypairs
.get(pubkey)
.ok_or(SigningServiceError::UnknownValidator(*pubkey))?;
let mut validators = self.validators.write().unwrap();
let state = validators
.entry(*pubkey)
.or_insert_with(|| ValidatorState::new(*pubkey));
let decision = self.policy.check_block_proposal(state, slot, &signing_root);
match decision {
PolicyDecision::Allow => {
let signature = keypair.sign(&signing_root);
state.record_block_signing(slot, signing_root);
let mut integrity = self.integrity.write().unwrap();
let record = integrity.prepare_record_with_context(
*pubkey,
SigningType::BlockProposal,
PolicyDecision::Allow,
signing_root,
SigningContext::BlockProposal { slot },
);
integrity
.record_decision(&record)
.map_err(SigningServiceError::Integrity)?;
let elapsed = start.elapsed().as_secs_f64();
metrics::record_signing_success("block_proposal", &validator_hex);
metrics::record_signing_latency("block_proposal", elapsed);
metrics::record_block_signed(&validator_hex);
metrics::set_last_signed_slot(&validator_hex, slot);
metrics::set_state_sequence(integrity.sequence_number);
Ok(SigningResult {
signature,
decision_record: record,
})
}
PolicyDecision::Refuse(code) => {
let mut integrity = self.integrity.write().unwrap();
let record = integrity.prepare_record_with_context(
*pubkey,
SigningType::BlockProposal,
PolicyDecision::Refuse(code),
signing_root,
SigningContext::BlockProposal { slot },
);
integrity
.record_decision(&record)
.map_err(SigningServiceError::Integrity)?;
let elapsed = start.elapsed().as_secs_f64();
metrics::record_signing_refusal("block_proposal", &code.to_string(), &validator_hex);
metrics::record_signing_latency("block_proposal", elapsed);
Err(SigningServiceError::SlashingProtection(code))
}
}
}
pub fn sign_attestation(
&self,
pubkey: &[u8; 48],
source_epoch: u64,
target_epoch: u64,
signing_root: [u8; 32],
) -> Result<SigningResult, SigningServiceError> {
let start = Instant::now();
let validator_hex = format!("0x{}...{}", &hex::encode(&pubkey[..4]), &hex::encode(&pubkey[44..]));
let keypairs = self.keypairs.read().unwrap();
let keypair = keypairs
.get(pubkey)
.ok_or(SigningServiceError::UnknownValidator(*pubkey))?;
let mut validators = self.validators.write().unwrap();
let state = validators
.entry(*pubkey)
.or_insert_with(|| ValidatorState::new(*pubkey));
let decision = self
.policy
.check_attestation(state, source_epoch, target_epoch, &signing_root);
match decision {
PolicyDecision::Allow => {
let signature = keypair.sign(&signing_root);
state.record_attestation_signing(source_epoch, target_epoch, signing_root);
let mut integrity = self.integrity.write().unwrap();
let record = integrity.prepare_record_with_context(
*pubkey,
SigningType::Attestation,
PolicyDecision::Allow,
signing_root,
SigningContext::Attestation {
source_epoch,
target_epoch,
},
);
integrity
.record_decision(&record)
.map_err(SigningServiceError::Integrity)?;
let elapsed = start.elapsed().as_secs_f64();
metrics::record_signing_success("attestation", &validator_hex);
metrics::record_signing_latency("attestation", elapsed);
metrics::record_attestation_signed(&validator_hex);
metrics::set_last_signed_target_epoch(&validator_hex, target_epoch);
metrics::set_state_sequence(integrity.sequence_number);
Ok(SigningResult {
signature,
decision_record: record,
})
}
PolicyDecision::Refuse(code) => {
let mut integrity = self.integrity.write().unwrap();
let record = integrity.prepare_record_with_context(
*pubkey,
SigningType::Attestation,
PolicyDecision::Refuse(code),
signing_root,
SigningContext::Attestation {
source_epoch,
target_epoch,
},
);
integrity
.record_decision(&record)
.map_err(SigningServiceError::Integrity)?;
let elapsed = start.elapsed().as_secs_f64();
metrics::record_signing_refusal("attestation", &code.to_string(), &validator_hex);
metrics::record_signing_latency("attestation", elapsed);
Err(SigningServiceError::SlashingProtection(code))
}
}
}
pub fn sign_generic(
&self,
pubkey: &[u8; 48],
signing_type: SigningType,
signing_root: [u8; 32],
) -> Result<SigningResult, SigningServiceError> {
let start = Instant::now();
let validator_hex = format!("0x{}...{}", &hex::encode(&pubkey[..4]), &hex::encode(&pubkey[44..]));
let type_name = signing_type.as_str();
let keypairs = self.keypairs.read().unwrap();
let keypair = keypairs
.get(pubkey)
.ok_or(SigningServiceError::UnknownValidator(*pubkey))?;
let signature = keypair.sign(&signing_root);
let mut integrity = self.integrity.write().unwrap();
let record = integrity.prepare_record_with_context(
*pubkey,
signing_type,
PolicyDecision::Allow,
signing_root,
SigningContext::Other,
);
integrity
.record_decision(&record)
.map_err(SigningServiceError::Integrity)?;
let elapsed = start.elapsed().as_secs_f64();
metrics::record_signing_success(type_name, &validator_hex);
metrics::record_signing_latency(type_name, elapsed);
metrics::set_state_sequence(integrity.sequence_number);
Ok(SigningResult {
signature,
decision_record: record,
})
}
pub fn replay_records(&self, records: Vec<DecisionRecord>) -> Result<u64, SigningServiceError> {
let mut validators = self.validators.write().unwrap();
let mut integrity = self.integrity.write().unwrap();
let mut replayed = 0u64;
let mut state_restored = 0u64;
for record in records {
if record.sequence <= integrity.sequence_number {
continue;
}
integrity
.record_decision(&record)
.map_err(SigningServiceError::Integrity)?;
if record.decision.is_allowed() {
let state = validators
.entry(record.validator_pubkey)
.or_insert_with(|| ValidatorState::new(record.validator_pubkey));
match &record.signing_context {
Some(SigningContext::BlockProposal { slot }) => {
state.record_block_signing(*slot, record.signing_root);
tracing::debug!(
sequence = record.sequence,
slot = slot,
"Replayed block proposal - restored slot state"
);
state_restored += 1;
}
Some(SigningContext::Attestation {
source_epoch,
target_epoch,
}) => {
state.record_attestation_signing(
*source_epoch,
*target_epoch,
record.signing_root,
);
tracing::debug!(
sequence = record.sequence,
source_epoch = source_epoch,
target_epoch = target_epoch,
"Replayed attestation - restored epoch state"
);
state_restored += 1;
}
Some(SigningContext::CosmosVote {
height,
round,
vote_type,
block_hash,
}) => {
if let Some(cosmos_state) = state.cosmos_state_mut() {
let msg_type = match vote_type {
1 => crate::state::validator::CosmosSignedMsgType::Prevote,
2 => crate::state::validator::CosmosSignedMsgType::Precommit,
_ => crate::state::validator::CosmosSignedMsgType::Prevote,
};
cosmos_state.record_vote(*height, *round, msg_type, *block_hash);
tracing::debug!(
sequence = record.sequence,
height = height,
round = round,
"Replayed Cosmos vote - restored state"
);
state_restored += 1;
}
}
Some(SigningContext::CosmosProposal {
height,
round,
block_hash,
}) => {
if let Some(cosmos_state) = state.cosmos_state_mut() {
cosmos_state.record_vote(
*height,
*round,
crate::state::validator::CosmosSignedMsgType::Proposal,
Some(*block_hash),
);
tracing::debug!(
sequence = record.sequence,
height = height,
round = round,
"Replayed Cosmos proposal - restored state"
);
state_restored += 1;
}
}
Some(SigningContext::Other) | None => {
tracing::debug!(
sequence = record.sequence,
request_type = ?record.request_type,
"Replayed generic signing record (no state to restore)"
);
}
}
} else {
tracing::debug!(
sequence = record.sequence,
decision = ?record.decision,
"Replayed refusal record"
);
}
replayed += 1;
}
tracing::info!(
replayed = replayed,
state_restored = state_restored,
"Replayed decision records"
);
Ok(replayed)
}
pub fn validator_states(&self) -> HashMap<[u8; 48], ValidatorState> {
self.validators.read().unwrap().clone()
}
pub fn integrity(&self) -> StateIntegrity {
self.integrity.read().unwrap().clone()
}
pub fn last_sequence(&self) -> u64 {
self.integrity.read().unwrap().sequence_number
}
}
#[derive(Debug, Clone)]
pub struct SigningResult {
pub signature: BlsSignature,
pub decision_record: DecisionRecord,
}
impl SigningResult {
pub fn signature_hex(&self) -> String {
self.signature.to_hex()
}
pub fn signature_bytes(&self) -> [u8; 96] {
self.signature.to_bytes()
}
}
#[derive(Debug, Error)]
pub enum SigningServiceError {
#[error("Unknown validator: 0x{}", hex::encode(.0))]
UnknownValidator([u8; 48]),
#[error("Slashing protection: {0}")]
SlashingProtection(RefusalCode),
#[error("Integrity error: {0}")]
Integrity(#[from] IntegrityError),
#[error("Genesis validators root mismatch: expected 0x{}, got 0x{}", hex::encode(.expected), hex::encode(.actual))]
GenesisRootMismatch { expected: [u8; 32], actual: [u8; 32] },
#[error("Invalid signing root: {0}")]
InvalidSigningRoot(String),
}
pub type SharedSigningService = Arc<SigningService>;
#[cfg(test)]
mod tests {
use super::*;
fn make_service() -> SigningService {
let keypair = BlsKeypair::generate();
SigningService::new(vec![keypair])
}
#[test]
fn test_sign_block_proposal() {
let service = make_service();
let pubkey = service.public_keys()[0];
let signing_root = [1u8; 32];
let result = service.sign_block_proposal(&pubkey, 100, signing_root);
assert!(result.is_ok());
let signing_result = result.unwrap();
assert!(!signing_result.signature_bytes().iter().all(|&b| b == 0));
}
#[test]
fn test_double_proposal_rejected() {
let service = make_service();
let pubkey = service.public_keys()[0];
let result1 = service.sign_block_proposal(&pubkey, 100, [1u8; 32]);
assert!(result1.is_ok());
let result2 = service.sign_block_proposal(&pubkey, 100, [2u8; 32]);
assert!(matches!(
result2,
Err(SigningServiceError::SlashingProtection(RefusalCode::DoubleProposal))
));
}
#[test]
fn test_sign_attestation() {
let service = make_service();
let pubkey = service.public_keys()[0];
let signing_root = [1u8; 32];
let result = service.sign_attestation(&pubkey, 10, 11, signing_root);
assert!(result.is_ok());
}
#[test]
fn test_double_vote_rejected() {
let service = make_service();
let pubkey = service.public_keys()[0];
let result1 = service.sign_attestation(&pubkey, 10, 11, [1u8; 32]);
assert!(result1.is_ok());
let result2 = service.sign_attestation(&pubkey, 10, 11, [2u8; 32]);
assert!(matches!(
result2,
Err(SigningServiceError::SlashingProtection(RefusalCode::DoubleVote))
));
}
#[test]
fn test_surround_vote_rejected() {
let service = make_service();
let pubkey = service.public_keys()[0];
let result1 = service.sign_attestation(&pubkey, 5, 10, [1u8; 32]);
assert!(result1.is_ok());
let result2 = service.sign_attestation(&pubkey, 3, 12, [2u8; 32]);
assert!(matches!(
result2,
Err(SigningServiceError::SlashingProtection(RefusalCode::SurroundVote))
));
}
#[test]
fn test_unknown_validator() {
let service = make_service();
let unknown_pubkey = [99u8; 48];
let result = service.sign_block_proposal(&unknown_pubkey, 100, [1u8; 32]);
assert!(matches!(
result,
Err(SigningServiceError::UnknownValidator(_))
));
}
#[test]
fn test_sign_generic() {
let service = make_service();
let pubkey = service.public_keys()[0];
let result = service.sign_generic(&pubkey, SigningType::RandaoReveal, [1u8; 32]);
assert!(result.is_ok());
}
#[test]
fn test_genesis_root() {
let service = make_service();
let root1 = [1u8; 32];
let root2 = [2u8; 32];
assert!(service.set_genesis_validators_root(root1).is_ok());
assert!(service.set_genesis_validators_root(root1).is_ok());
assert!(matches!(
service.set_genesis_validators_root(root2),
Err(SigningServiceError::GenesisRootMismatch { .. })
));
}
}