use crate::{ doppelganger_service::DoppelgangerService, http_metrics::metrics, initialized_validators::InitializedValidators, signing_method::{Error as SigningError, SignableMessage, SigningContext, SigningMethod}, Config, }; use account_utils::validator_definitions::{PasswordStorage, ValidatorDefinition}; use eth2::types::VariableList; use parking_lot::{Mutex, RwLock}; use slashing_protection::{ interchange::Interchange, InterchangeError, NotSafe, Safe, SlashingDatabase, }; use slog::{crit, error, info, warn, Logger}; use slot_clock::SlotClock; use std::iter::FromIterator; use std::marker::PhantomData; use std::path::Path; use std::sync::Arc; use task_executor::TaskExecutor; use types::sidecar::Sidecar; use types::{ attestation::Error as AttestationError, graffiti::GraffitiString, AbstractExecPayload, Address, AggregateAndProof, Attestation, BeaconBlock, BlindedPayload, ChainSpec, ContributionAndProof, Domain, Epoch, EthSpec, Fork, ForkName, Graffiti, Hash256, Keypair, PublicKeyBytes, SelectionProof, SidecarList, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedContributionAndProof, SignedRoot, SignedSidecar, SignedSidecarList, SignedValidatorRegistrationData, SignedVoluntaryExit, Slot, SyncAggregatorSelectionData, SyncCommitteeContribution, SyncCommitteeMessage, SyncSelectionProof, SyncSubnetId, ValidatorRegistrationData, VoluntaryExit, }; use validator_dir::ValidatorDir; pub use crate::doppelganger_service::DoppelgangerStatus; use crate::preparation_service::ProposalData; #[derive(Debug, PartialEq)] pub enum Error { DoppelgangerProtected(PublicKeyBytes), UnknownToDoppelgangerService(PublicKeyBytes), UnknownPubkey(PublicKeyBytes), Slashable(NotSafe), SameData, GreaterThanCurrentSlot { slot: Slot, current_slot: Slot }, GreaterThanCurrentEpoch { epoch: Epoch, current_epoch: Epoch }, UnableToSignAttestation(AttestationError), UnableToSign(SigningError), } impl From for Error { fn from(e: SigningError) -> Self { Error::UnableToSign(e) } } /// Number of epochs of slashing protection history to keep. /// /// This acts as a maximum safe-guard against clock drift. const SLASHING_PROTECTION_HISTORY_EPOCHS: u64 = 512; /// Currently used as the default gas limit in execution clients. /// /// https://github.com/ethereum/builder-specs/issues/17 pub const DEFAULT_GAS_LIMIT: u64 = 30_000_000; struct LocalValidator { validator_dir: ValidatorDir, voting_keypair: Keypair, } /// We derive our own `PartialEq` to avoid doing equality checks between secret keys. /// /// It's nice to avoid secret key comparisons from a security perspective, but it's also a little /// risky when it comes to `HashMap` integrity (that's why we need `PartialEq`). /// /// Currently, we obtain keypairs from keystores where we derive the `PublicKey` from a `SecretKey` /// via a hash function. In order to have two equal `PublicKey` with different `SecretKey` we would /// need to have either: /// /// - A serious upstream integrity error. /// - A hash collision. /// /// It seems reasonable to make these two assumptions in order to avoid the equality checks. impl PartialEq for LocalValidator { fn eq(&self, other: &Self) -> bool { self.validator_dir == other.validator_dir && self.voting_keypair.pk == other.voting_keypair.pk } } pub struct ValidatorStore { validators: Arc>, slashing_protection: SlashingDatabase, slashing_protection_last_prune: Arc>, genesis_validators_root: Hash256, spec: Arc, log: Logger, doppelganger_service: Option>, slot_clock: T, fee_recipient_process: Option
, gas_limit: Option, builder_proposals: bool, task_executor: TaskExecutor, _phantom: PhantomData, } impl ValidatorStore { // All arguments are different types. Making the fields `pub` is undesired. A builder seems // unnecessary. #[allow(clippy::too_many_arguments)] pub fn new( validators: InitializedValidators, slashing_protection: SlashingDatabase, genesis_validators_root: Hash256, spec: ChainSpec, doppelganger_service: Option>, slot_clock: T, config: &Config, task_executor: TaskExecutor, log: Logger, ) -> Self { Self { validators: Arc::new(RwLock::new(validators)), slashing_protection, slashing_protection_last_prune: Arc::new(Mutex::new(Epoch::new(0))), genesis_validators_root, spec: Arc::new(spec), log, doppelganger_service, slot_clock, fee_recipient_process: config.fee_recipient, gas_limit: config.gas_limit, builder_proposals: config.builder_proposals, task_executor, _phantom: PhantomData, } } /// Register all local validators in doppelganger protection to try and prevent instances of /// duplicate validators operating on the network at the same time. /// /// This function has no effect if doppelganger protection is disabled. pub fn register_all_in_doppelganger_protection_if_enabled(&self) -> Result<(), String> { if let Some(doppelganger_service) = &self.doppelganger_service { for pubkey in self.validators.read().iter_voting_pubkeys() { doppelganger_service.register_new_validator::(*pubkey, &self.slot_clock)? } } Ok(()) } /// Returns `true` if doppelganger protection is enabled, or else `false`. pub fn doppelganger_protection_enabled(&self) -> bool { self.doppelganger_service.is_some() } pub fn initialized_validators(&self) -> Arc> { self.validators.clone() } /// Indicates if the `voting_public_key` exists in self and is enabled. pub fn has_validator(&self, voting_public_key: &PublicKeyBytes) -> bool { self.validators .read() .validator(voting_public_key) .is_some() } /// Insert a new validator to `self`, where the validator is represented by an EIP-2335 /// keystore on the filesystem. #[allow(clippy::too_many_arguments)] pub async fn add_validator_keystore>( &self, voting_keystore_path: P, password_storage: PasswordStorage, enable: bool, graffiti: Option, suggested_fee_recipient: Option
, gas_limit: Option, builder_proposals: Option, ) -> Result { let mut validator_def = ValidatorDefinition::new_keystore_with_password( voting_keystore_path, password_storage, graffiti.map(Into::into), suggested_fee_recipient, gas_limit, builder_proposals, ) .map_err(|e| format!("failed to create validator definitions: {:?}", e))?; validator_def.enabled = enable; self.add_validator(validator_def).await } /// Insert a new validator to `self`. /// /// This function includes: /// /// - Adding the validator definition to the YAML file, saving it to the filesystem. /// - Enabling the validator with the slashing protection database. /// - If `enable == true`, starting to perform duties for the validator. // FIXME: ignore this clippy lint until the validator store is refactored to use async locks #[allow(clippy::await_holding_lock)] pub async fn add_validator( &self, validator_def: ValidatorDefinition, ) -> Result { let validator_pubkey = validator_def.voting_public_key.compress(); self.slashing_protection .register_validator(validator_pubkey) .map_err(|e| format!("failed to register validator: {:?}", e))?; if let Some(doppelganger_service) = &self.doppelganger_service { doppelganger_service .register_new_validator::(validator_pubkey, &self.slot_clock)?; } self.validators .write() .add_definition_replace_disabled(validator_def.clone()) .await .map_err(|e| format!("Unable to add definition: {:?}", e))?; Ok(validator_def) } /// Returns `ProposalData` for the provided `pubkey` if it exists in `InitializedValidators`. /// `ProposalData` fields include defaulting logic described in `get_fee_recipient_defaulting`, /// `get_gas_limit_defaulting`, and `get_builder_proposals_defaulting`. pub fn proposal_data(&self, pubkey: &PublicKeyBytes) -> Option { self.validators .read() .validator(pubkey) .map(|validator| ProposalData { validator_index: validator.get_index(), fee_recipient: self .get_fee_recipient_defaulting(validator.get_suggested_fee_recipient()), gas_limit: self.get_gas_limit_defaulting(validator.get_gas_limit()), builder_proposals: self .get_builder_proposals_defaulting(validator.get_builder_proposals()), }) } /// Attempts to resolve the pubkey to a validator index. /// /// It may return `None` if the `pubkey` is: /// /// - Unknown. /// - Known, but with an unknown index. pub fn validator_index(&self, pubkey: &PublicKeyBytes) -> Option { self.validators.read().get_index(pubkey) } /// Returns all voting pubkeys for all enabled validators. /// /// The `filter_func` allows for filtering pubkeys based upon their `DoppelgangerStatus`. There /// are two primary functions used here: /// /// - `DoppelgangerStatus::only_safe`: only returns pubkeys which have passed doppelganger /// protection and are safe-enough to sign messages. /// - `DoppelgangerStatus::ignored`: returns all the pubkeys from `only_safe` *plus* those still /// undergoing protection. This is useful for collecting duties or other non-signing tasks. #[allow(clippy::needless_collect)] // Collect is required to avoid holding a lock. pub fn voting_pubkeys(&self, filter_func: F) -> I where I: FromIterator, F: Fn(DoppelgangerStatus) -> Option, { // Collect all the pubkeys first to avoid interleaving locks on `self.validators` and // `self.doppelganger_service()`. let pubkeys = self .validators .read() .iter_voting_pubkeys() .cloned() .collect::>(); pubkeys .into_iter() .map(|pubkey| { self.doppelganger_service .as_ref() .map(|doppelganger_service| doppelganger_service.validator_status(pubkey)) // Allow signing on all pubkeys if doppelganger protection is disabled. .unwrap_or_else(|| DoppelgangerStatus::SigningEnabled(pubkey)) }) .filter_map(filter_func) .collect() } /// Returns doppelganger statuses for all enabled validators. #[allow(clippy::needless_collect)] // Collect is required to avoid holding a lock. pub fn doppelganger_statuses(&self) -> Vec { // Collect all the pubkeys first to avoid interleaving locks on `self.validators` and // `self.doppelganger_service`. let pubkeys = self .validators .read() .iter_voting_pubkeys() .cloned() .collect::>(); pubkeys .into_iter() .map(|pubkey| { self.doppelganger_service .as_ref() .map(|doppelganger_service| doppelganger_service.validator_status(pubkey)) // Allow signing on all pubkeys if doppelganger protection is disabled. .unwrap_or_else(|| DoppelgangerStatus::SigningEnabled(pubkey)) }) .collect() } /// Check if the `validator_pubkey` is permitted by the doppleganger protection to sign /// messages. pub fn doppelganger_protection_allows_signing(&self, validator_pubkey: PublicKeyBytes) -> bool { self.doppelganger_service .as_ref() // If there's no doppelganger service then we assume it is purposefully disabled and // declare that all keys are safe with regard to it. .map_or(true, |doppelganger_service| { doppelganger_service .validator_status(validator_pubkey) .only_safe() .is_some() }) } pub fn num_voting_validators(&self) -> usize { self.validators.read().num_enabled() } fn fork(&self, epoch: Epoch) -> Fork { self.spec.fork_at_epoch(epoch) } /// Returns a `SigningMethod` for `validator_pubkey` *only if* that validator is considered safe /// by doppelganger protection. fn doppelganger_checked_signing_method( &self, validator_pubkey: PublicKeyBytes, ) -> Result, Error> { if self.doppelganger_protection_allows_signing(validator_pubkey) { self.validators .read() .signing_method(&validator_pubkey) .ok_or(Error::UnknownPubkey(validator_pubkey)) } else { Err(Error::DoppelgangerProtected(validator_pubkey)) } } /// Returns a `SigningMethod` for `validator_pubkey` regardless of that validators doppelganger /// protection status. /// /// ## Warning /// /// This method should only be used for signing non-slashable messages. fn doppelganger_bypassed_signing_method( &self, validator_pubkey: PublicKeyBytes, ) -> Result, Error> { self.validators .read() .signing_method(&validator_pubkey) .ok_or(Error::UnknownPubkey(validator_pubkey)) } fn signing_context(&self, domain: Domain, signing_epoch: Epoch) -> SigningContext { if domain == Domain::VoluntaryExit { match self.spec.fork_name_at_epoch(signing_epoch) { ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { SigningContext { domain, epoch: signing_epoch, fork: self.fork(signing_epoch), genesis_validators_root: self.genesis_validators_root, } } // EIP-7044 ForkName::Deneb => SigningContext { domain, epoch: signing_epoch, fork: Fork { previous_version: self.spec.capella_fork_version, current_version: self.spec.capella_fork_version, epoch: signing_epoch, }, genesis_validators_root: self.genesis_validators_root, }, } } else { SigningContext { domain, epoch: signing_epoch, fork: self.fork(signing_epoch), genesis_validators_root: self.genesis_validators_root, } } } pub async fn randao_reveal( &self, validator_pubkey: PublicKeyBytes, signing_epoch: Epoch, ) -> Result { let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signing_context = self.signing_context(Domain::Randao, signing_epoch); let signature = signing_method .get_signature::>( SignableMessage::RandaoReveal(signing_epoch), signing_context, &self.spec, &self.task_executor, ) .await?; Ok(signature) } pub fn graffiti(&self, validator_pubkey: &PublicKeyBytes) -> Option { self.validators.read().graffiti(validator_pubkey) } /// Returns the fee recipient for the given public key. The priority order for fetching /// the fee recipient is: /// 1. validator_definitions.yml /// 2. process level fee recipient pub fn get_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
{ // If there is a `suggested_fee_recipient` in the validator definitions yaml // file, use that value. self.get_fee_recipient_defaulting(self.suggested_fee_recipient(validator_pubkey)) } pub fn get_fee_recipient_defaulting(&self, fee_recipient: Option
) -> Option
{ // If there's nothing in the file, try the process-level default value. fee_recipient.or(self.fee_recipient_process) } /// Returns the suggested_fee_recipient from `validator_definitions.yml` if any. /// This has been pulled into a private function so the read lock is dropped easily fn suggested_fee_recipient(&self, validator_pubkey: &PublicKeyBytes) -> Option
{ self.validators .read() .suggested_fee_recipient(validator_pubkey) } /// Returns the gas limit for the given public key. The priority order for fetching /// the gas limit is: /// /// 1. validator_definitions.yml /// 2. process level gas limit /// 3. `DEFAULT_GAS_LIMIT` pub fn get_gas_limit(&self, validator_pubkey: &PublicKeyBytes) -> u64 { self.get_gas_limit_defaulting(self.validators.read().gas_limit(validator_pubkey)) } fn get_gas_limit_defaulting(&self, gas_limit: Option) -> u64 { // If there is a `gas_limit` in the validator definitions yaml // file, use that value. gas_limit // If there's nothing in the file, try the process-level default value. .or(self.gas_limit) // If there's no process-level default, use the `DEFAULT_GAS_LIMIT`. .unwrap_or(DEFAULT_GAS_LIMIT) } /// Returns a `bool` for the given public key that denotes whther this validator should use the /// builder API. The priority order for fetching this value is: /// /// 1. validator_definitions.yml /// 2. process level flag pub fn get_builder_proposals(&self, validator_pubkey: &PublicKeyBytes) -> bool { // If there is a `suggested_fee_recipient` in the validator definitions yaml // file, use that value. self.get_builder_proposals_defaulting( self.validators.read().builder_proposals(validator_pubkey), ) } fn get_builder_proposals_defaulting(&self, builder_proposals: Option) -> bool { builder_proposals // If there's nothing in the file, try the process-level default value. .unwrap_or(self.builder_proposals) } pub async fn sign_block>( &self, validator_pubkey: PublicKeyBytes, block: BeaconBlock, current_slot: Slot, ) -> Result, Error> { // Make sure the block slot is not higher than the current slot to avoid potential attacks. if block.slot() > current_slot { warn!( self.log, "Not signing block with slot greater than current slot"; "block_slot" => block.slot().as_u64(), "current_slot" => current_slot.as_u64() ); return Err(Error::GreaterThanCurrentSlot { slot: block.slot(), current_slot, }); } let signing_epoch = block.epoch(); let signing_context = self.signing_context(Domain::BeaconProposer, signing_epoch); let domain_hash = signing_context.domain_hash(&self.spec); // Check for slashing conditions. let slashing_status = self.slashing_protection.check_and_insert_block_proposal( &validator_pubkey, &block.block_header(), domain_hash, ); match slashing_status { // We can safely sign this block without slashing. Ok(Safe::Valid) => { metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SUCCESS]); let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::( SignableMessage::BeaconBlock(&block), signing_context, &self.spec, &self.task_executor, ) .await?; Ok(SignedBeaconBlock::from_block(block, signature)) } Ok(Safe::SameData) => { warn!( self.log, "Skipping signing of previously signed block"; ); metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SAME_DATA]); Err(Error::SameData) } Err(NotSafe::UnregisteredValidator(pk)) => { warn!( self.log, "Not signing block for unregistered validator"; "msg" => "Carefully consider running with --init-slashing-protection (see --help)", "public_key" => format!("{:?}", pk) ); metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::UNREGISTERED]); Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) } Err(e) => { crit!( self.log, "Not signing slashable block"; "error" => format!("{:?}", e) ); metrics::inc_counter_vec(&metrics::SIGNED_BLOCKS_TOTAL, &[metrics::SLASHABLE]); Err(Error::Slashable(e)) } } } pub async fn sign_blobs>( &self, validator_pubkey: PublicKeyBytes, blob_sidecars: SidecarList, ) -> Result, Error> { let mut signed_blob_sidecars = Vec::new(); for blob_sidecar in blob_sidecars.into_iter() { let slot = blob_sidecar.slot(); let signing_epoch = slot.epoch(E::slots_per_epoch()); let signing_context = self.signing_context(Domain::BlobSidecar, signing_epoch); let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::( SignableMessage::BlobSidecar(blob_sidecar.as_ref()), signing_context, &self.spec, &self.task_executor, ) .await?; metrics::inc_counter_vec(&metrics::SIGNED_BLOBS_TOTAL, &[metrics::SUCCESS]); signed_blob_sidecars.push(SignedSidecar { message: blob_sidecar, signature, _phantom: PhantomData, }); } Ok(VariableList::from(signed_blob_sidecars)) } pub async fn sign_attestation( &self, validator_pubkey: PublicKeyBytes, validator_committee_position: usize, attestation: &mut Attestation, current_epoch: Epoch, ) -> Result<(), Error> { // Make sure the target epoch is not higher than the current epoch to avoid potential attacks. if attestation.data.target.epoch > current_epoch { return Err(Error::GreaterThanCurrentEpoch { epoch: attestation.data.target.epoch, current_epoch, }); } // Checking for slashing conditions. let signing_epoch = attestation.data.target.epoch; let signing_context = self.signing_context(Domain::BeaconAttester, signing_epoch); let domain_hash = signing_context.domain_hash(&self.spec); let slashing_status = self.slashing_protection.check_and_insert_attestation( &validator_pubkey, &attestation.data, domain_hash, ); match slashing_status { // We can safely sign this attestation. Ok(Safe::Valid) => { let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::>( SignableMessage::AttestationData(&attestation.data), signing_context, &self.spec, &self.task_executor, ) .await?; attestation .add_signature(&signature, validator_committee_position) .map_err(Error::UnableToSignAttestation)?; metrics::inc_counter_vec(&metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::SUCCESS]); Ok(()) } Ok(Safe::SameData) => { warn!( self.log, "Skipping signing of previously signed attestation" ); metrics::inc_counter_vec( &metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::SAME_DATA], ); Err(Error::SameData) } Err(NotSafe::UnregisteredValidator(pk)) => { warn!( self.log, "Not signing attestation for unregistered validator"; "msg" => "Carefully consider running with --init-slashing-protection (see --help)", "public_key" => format!("{:?}", pk) ); metrics::inc_counter_vec( &metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::UNREGISTERED], ); Err(Error::Slashable(NotSafe::UnregisteredValidator(pk))) } Err(e) => { crit!( self.log, "Not signing slashable attestation"; "attestation" => format!("{:?}", attestation.data), "error" => format!("{:?}", e) ); metrics::inc_counter_vec( &metrics::SIGNED_ATTESTATIONS_TOTAL, &[metrics::SLASHABLE], ); Err(Error::Slashable(e)) } } } pub async fn sign_voluntary_exit( &self, validator_pubkey: PublicKeyBytes, voluntary_exit: VoluntaryExit, ) -> Result { let signing_epoch = voluntary_exit.epoch; let signing_context = self.signing_context(Domain::VoluntaryExit, signing_epoch); let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::>( SignableMessage::VoluntaryExit(&voluntary_exit), signing_context, &self.spec, &self.task_executor, ) .await?; metrics::inc_counter_vec(&metrics::SIGNED_VOLUNTARY_EXITS_TOTAL, &[metrics::SUCCESS]); Ok(SignedVoluntaryExit { message: voluntary_exit, signature, }) } pub async fn sign_validator_registration_data( &self, validator_registration_data: ValidatorRegistrationData, ) -> Result { let domain_hash = self.spec.get_builder_domain(); let signing_root = validator_registration_data.signing_root(domain_hash); let signing_method = self.doppelganger_bypassed_signing_method(validator_registration_data.pubkey)?; let signature = signing_method .get_signature_from_root::>( SignableMessage::ValidatorRegistration(&validator_registration_data), signing_root, &self.task_executor, None, ) .await?; metrics::inc_counter_vec( &metrics::SIGNED_VALIDATOR_REGISTRATIONS_TOTAL, &[metrics::SUCCESS], ); Ok(SignedValidatorRegistrationData { message: validator_registration_data, signature, }) } /// Signs an `AggregateAndProof` for a given validator. /// /// The resulting `SignedAggregateAndProof` is sent on the aggregation channel and cannot be /// modified by actors other than the signing validator. pub async fn produce_signed_aggregate_and_proof( &self, validator_pubkey: PublicKeyBytes, aggregator_index: u64, aggregate: Attestation, selection_proof: SelectionProof, ) -> Result, Error> { let signing_epoch = aggregate.data.target.epoch; let signing_context = self.signing_context(Domain::AggregateAndProof, signing_epoch); let message = AggregateAndProof { aggregator_index, aggregate, selection_proof: selection_proof.into(), }; let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::>( SignableMessage::SignedAggregateAndProof(&message), signing_context, &self.spec, &self.task_executor, ) .await?; metrics::inc_counter_vec(&metrics::SIGNED_AGGREGATES_TOTAL, &[metrics::SUCCESS]); Ok(SignedAggregateAndProof { message, signature }) } /// Produces a `SelectionProof` for the `slot`, signed by with corresponding secret key to /// `validator_pubkey`. pub async fn produce_selection_proof( &self, validator_pubkey: PublicKeyBytes, slot: Slot, ) -> Result { let signing_epoch = slot.epoch(E::slots_per_epoch()); let signing_context = self.signing_context(Domain::SelectionProof, signing_epoch); // Bypass the `with_validator_signing_method` function. // // This is because we don't care about doppelganger protection when it comes to selection // proofs. They are not slashable and we need them to subscribe to subnets on the BN. // // As long as we disallow `SignedAggregateAndProof` then these selection proofs will never // be published on the network. let signing_method = self.doppelganger_bypassed_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::>( SignableMessage::SelectionProof(slot), signing_context, &self.spec, &self.task_executor, ) .await .map_err(Error::UnableToSign)?; metrics::inc_counter_vec(&metrics::SIGNED_SELECTION_PROOFS_TOTAL, &[metrics::SUCCESS]); Ok(signature.into()) } /// Produce a `SyncSelectionProof` for `slot` signed by the secret key of `validator_pubkey`. pub async fn produce_sync_selection_proof( &self, validator_pubkey: &PublicKeyBytes, slot: Slot, subnet_id: SyncSubnetId, ) -> Result { let signing_epoch = slot.epoch(E::slots_per_epoch()); let signing_context = self.signing_context(Domain::SyncCommitteeSelectionProof, signing_epoch); // Bypass `with_validator_signing_method`: sync committee messages are not slashable. let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; metrics::inc_counter_vec( &metrics::SIGNED_SYNC_SELECTION_PROOFS_TOTAL, &[metrics::SUCCESS], ); let message = SyncAggregatorSelectionData { slot, subcommittee_index: subnet_id.into(), }; let signature = signing_method .get_signature::>( SignableMessage::SyncSelectionProof(&message), signing_context, &self.spec, &self.task_executor, ) .await .map_err(Error::UnableToSign)?; Ok(signature.into()) } pub async fn produce_sync_committee_signature( &self, slot: Slot, beacon_block_root: Hash256, validator_index: u64, validator_pubkey: &PublicKeyBytes, ) -> Result { let signing_epoch = slot.epoch(E::slots_per_epoch()); let signing_context = self.signing_context(Domain::SyncCommittee, signing_epoch); // Bypass `with_validator_signing_method`: sync committee messages are not slashable. let signing_method = self.doppelganger_bypassed_signing_method(*validator_pubkey)?; let signature = signing_method .get_signature::>( SignableMessage::SyncCommitteeSignature { beacon_block_root, slot, }, signing_context, &self.spec, &self.task_executor, ) .await .map_err(Error::UnableToSign)?; metrics::inc_counter_vec( &metrics::SIGNED_SYNC_COMMITTEE_MESSAGES_TOTAL, &[metrics::SUCCESS], ); Ok(SyncCommitteeMessage { slot, beacon_block_root, validator_index, signature, }) } pub async fn produce_signed_contribution_and_proof( &self, aggregator_index: u64, aggregator_pubkey: PublicKeyBytes, contribution: SyncCommitteeContribution, selection_proof: SyncSelectionProof, ) -> Result, Error> { let signing_epoch = contribution.slot.epoch(E::slots_per_epoch()); let signing_context = self.signing_context(Domain::ContributionAndProof, signing_epoch); // Bypass `with_validator_signing_method`: sync committee messages are not slashable. let signing_method = self.doppelganger_bypassed_signing_method(aggregator_pubkey)?; let message = ContributionAndProof { aggregator_index, contribution, selection_proof: selection_proof.into(), }; let signature = signing_method .get_signature::>( SignableMessage::SignedContributionAndProof(&message), signing_context, &self.spec, &self.task_executor, ) .await .map_err(Error::UnableToSign)?; metrics::inc_counter_vec( &metrics::SIGNED_SYNC_COMMITTEE_CONTRIBUTIONS_TOTAL, &[metrics::SUCCESS], ); Ok(SignedContributionAndProof { message, signature }) } pub fn import_slashing_protection( &self, interchange: Interchange, ) -> Result<(), InterchangeError> { self.slashing_protection .import_interchange_info(interchange, self.genesis_validators_root)?; Ok(()) } /// Export slashing protection data while also disabling the given keys in the database. /// /// If any key is unknown to the slashing protection database it will be silently omitted /// from the result. It is the caller's responsibility to check whether all keys provided /// had data returned for them. pub fn export_slashing_protection_for_keys( &self, pubkeys: &[PublicKeyBytes], ) -> Result { self.slashing_protection.with_transaction(|txn| { let known_pubkeys = pubkeys .iter() .filter_map(|pubkey| { let validator_id = self .slashing_protection .get_validator_id_ignoring_status(txn, pubkey) .ok()?; Some( self.slashing_protection .update_validator_status(txn, validator_id, false) .map(|()| *pubkey), ) }) .collect::, _>>()?; self.slashing_protection.export_interchange_info_in_txn( self.genesis_validators_root, Some(&known_pubkeys), txn, ) }) } /// Prune the slashing protection database so that it remains performant. /// /// This function will only do actual pruning periodically, so it should usually be /// cheap to call. The `first_run` flag can be used to print a more verbose message when pruning /// runs. pub fn prune_slashing_protection_db(&self, current_epoch: Epoch, first_run: bool) { // Attempt to prune every SLASHING_PROTECTION_HISTORY_EPOCHs, with a tolerance for // missing the epoch that aligns exactly. let mut last_prune = self.slashing_protection_last_prune.lock(); if current_epoch / SLASHING_PROTECTION_HISTORY_EPOCHS <= *last_prune / SLASHING_PROTECTION_HISTORY_EPOCHS { return; } if first_run { info!( self.log, "Pruning slashing protection DB"; "epoch" => current_epoch, "msg" => "pruning may take several minutes the first time it runs" ); } else { info!(self.log, "Pruning slashing protection DB"; "epoch" => current_epoch); } let _timer = metrics::start_timer(&metrics::SLASHING_PROTECTION_PRUNE_TIMES); let new_min_target_epoch = current_epoch.saturating_sub(SLASHING_PROTECTION_HISTORY_EPOCHS); let new_min_slot = new_min_target_epoch.start_slot(E::slots_per_epoch()); let all_pubkeys: Vec<_> = self.voting_pubkeys(DoppelgangerStatus::ignored); if let Err(e) = self .slashing_protection .prune_all_signed_attestations(all_pubkeys.iter(), new_min_target_epoch) { error!( self.log, "Error during pruning of signed attestations"; "error" => ?e, ); return; } if let Err(e) = self .slashing_protection .prune_all_signed_blocks(all_pubkeys.iter(), new_min_slot) { error!( self.log, "Error during pruning of signed blocks"; "error" => ?e, ); return; } *last_prune = current_epoch; info!(self.log, "Completed pruning of slashing protection DB"); } }