//! Provides verification for the following attestations: //! //! - "Unaggregated" `Attestation` received from either gossip or the HTTP API. //! - "Aggregated" `SignedAggregateAndProof` received from gossip or the HTTP API. //! //! For clarity, we define: //! //! - Unaggregated: an `Attestation` object that has exactly one aggregation bit set. //! - Aggregated: a `SignedAggregateAndProof` which has zero or more signatures. //! - Note: "zero or more" may soon change to "one or more". //! //! Similar to the `crate::block_verification` module, we try to avoid doing duplicate verification //! work as an attestation passes through different stages of verification. We represent these //! different stages of verification with wrapper types. These wrapper-types flow in a particular //! pattern: //! //! ```ignore //! types::Attestation types::SignedAggregateAndProof //! | | //! ▼ ▼ //! IndexedUnaggregatedAttestation IndexedAggregatedAttestation //! | | //! VerifiedUnaggregatedAttestation VerifiedAggregatedAttestation //! | | //! ------------------------------------- //! | //! ▼ //! impl VerifiedAttestation //! ``` // Ignore this lint for `AttestationSlashInfo` which is of comparable size to the non-error types it // is returned alongside. #![allow(clippy::result_large_err)] mod batch; use crate::{ BeaconChain, BeaconChainError, BeaconChainTypes, metrics, observed_aggregates::{ObserveOutcome, ObservedAttestationKey}, observed_attesters::Error as ObservedAttestersError, single_attestation::single_attestation_to_attestation, }; use bls::verify_signature_sets; use itertools::Itertools; use proto_array::Block as ProtoBlock; use slot_clock::SlotClock; use state_processing::{ common::{ attesting_indices_base, attesting_indices_electra::{self, get_committee_indices}, }, per_block_processing::errors::{AttestationValidationError, BlockOperationError}, signature_sets::{ indexed_attestation_signature_set_from_pubkeys, signed_aggregate_selection_proof_signature_set, signed_aggregate_signature_set, }, }; use std::borrow::Cow; use strum::AsRefStr; use tracing::debug; use tree_hash::TreeHash; use types::{ Attestation, AttestationData, AttestationRef, BeaconCommittee, BeaconStateError::NoCommitteeFound, ChainSpec, CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, SelectionProof, SignedAggregateAndProof, SingleAttestation, Slot, SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; /// Returned when an attestation was not successfully verified. It might not have been verified for /// two reasons: /// /// - The attestation is malformed or inappropriate for the context (indicated by all variants /// other than `BeaconChainError`). /// - The application encountered an internal error whilst attempting to determine validity /// (the `BeaconChainError` variant) #[derive(Debug, AsRefStr)] pub enum Error { /// The attestation is from a slot that is later than the current slot (with respect to the /// gossip clock disparity). /// /// ## Peer scoring /// /// Assuming the local clock is correct, the peer has sent an invalid message. FutureSlot { attestation_slot: Slot, latest_permissible_slot: Slot, }, /// The attestation is from a slot that is prior to the earliest permissible slot (with /// respect to the gossip clock disparity). /// /// ## Peer scoring /// /// Assuming the local clock is correct, the peer has sent an invalid message. PastSlot { attestation_slot: Slot, earliest_permissible_slot: Slot, }, /// The attestations aggregation bits were empty when they shouldn't be. /// /// ## Peer scoring /// /// The peer has sent an invalid message. EmptyAggregationBitfield, /// The `selection_proof` on the aggregate attestation does not elect it as an aggregator. /// /// ## Peer scoring /// /// The peer has sent an invalid message. InvalidSelectionProof { aggregator_index: u64 }, /// The `selection_proof` on the aggregate attestation selects it as a validator, however the /// aggregator index is not in the committee for that attestation. /// /// ## Peer scoring /// /// The peer has sent an invalid message. AggregatorNotInCommittee { aggregator_index: u64 }, /// The `attester_index` for a `SingleAttestation` is not a member of the committee defined /// by its `beacon_block_root`, `committee_index` and `slot`. /// /// ## Peer scoring /// /// The peer has sent an invalid message. AttesterNotInCommittee { attester_index: u64, committee_index: u64, slot: Slot, }, /// The aggregator index refers to a validator index that we have not seen. /// /// ## Peer scoring /// /// The peer has sent an invalid message. AggregatorPubkeyUnknown(u64), /// The attestation or a superset of this attestation's aggregations bits for the same data /// has been seen before; either in a block, on the gossip network or from a local validator. /// /// ## Peer scoring /// /// It's unclear if this attestation is valid, however we have already observed it and do not /// need to observe it again. AttestationSupersetKnown(Hash256), /// There has already been an aggregation observed for this validator, we refuse to process a /// second. /// /// ## Peer scoring /// /// It's unclear if this attestation is valid, however we have already observed an aggregate /// attestation from this validator for this epoch and should not observe another. AggregatorAlreadyKnown(u64), /// The aggregator index is higher than the maximum possible validator count. /// /// ## Peer scoring /// /// The peer has sent an invalid message. ValidatorIndexTooHigh(usize), /// The validator index is not set to zero after Electra. /// /// ## Peer scoring /// /// The peer has sent an invalid message. CommitteeIndexNonZero(usize), /// The `attestation.data.beacon_block_root` block is unknown. /// /// ## Peer scoring /// /// The attestation points to a block we have not yet imported. It's unclear if the attestation /// is valid or not. UnknownHeadBlock { beacon_block_root: Hash256 }, /// The `attestation.data.beacon_block_root` block is from before the finalized checkpoint. /// /// ## Peer scoring /// /// The attestation is not descended from the finalized checkpoint, which is a REJECT according /// to the spec. We downscore lightly because this could also happen if we are processing /// attestations extremely slowly. HeadBlockFinalized { beacon_block_root: Hash256 }, /// The `attestation.data.slot` is not from the same epoch as `data.target.epoch`. /// /// ## Peer scoring /// /// The peer has sent an invalid message. BadTargetEpoch, /// The target root of the attestation points to a block that we have not verified. /// /// This is invalid behaviour whilst we first check for `UnknownHeadBlock`. /// /// ## Peer scoring /// /// The peer has sent an invalid message. UnknownTargetRoot(Hash256), /// A signature on the attestation is invalid. /// /// ## Peer scoring /// /// The peer has sent an invalid message. InvalidSignature, /// There is no committee for the slot and committee index of this attestation and the /// attestation should not have been produced. /// /// ## Peer scoring /// /// The peer has sent an invalid message. NoCommitteeForSlotAndIndex { slot: Slot, index: CommitteeIndex }, /// The attestation doesn't have only one aggregation bit set. /// /// ## Peer scoring /// /// The peer has sent an invalid message. NotExactlyOneCommitteeBitSet(usize), /// We have already observed an attestation for the `validator_index` and refuse to process /// another. /// /// ## Peer scoring /// /// It's unclear if this attestation is valid, however we have already observed a /// single-participant attestation from this validator for this epoch and should not observe /// another. PriorAttestationKnown { validator_index: u64, epoch: Epoch }, /// The attestation is attesting to a state that is later than itself. (Viz., attesting to the /// future). /// /// ## Peer scoring /// /// The peer has sent an invalid message. AttestsToFutureBlock { block: Slot, attestation: Slot }, /// The attestation was received on an invalid attestation subnet. /// /// ## Peer scoring /// /// The peer has sent an invalid message. InvalidSubnetId { received: SubnetId, expected: SubnetId, }, /// The attestation failed the `state_processing` verification stage. /// /// ## Peer scoring /// /// The peer has sent an invalid message. Invalid(AttestationValidationError), /// The attestation head block is too far behind the attestation slot, causing many skip slots. /// This is deemed a DoS risk. TooManySkippedSlots { head_block_slot: Slot, attestation_slot: Slot, }, /// The attestation has an invalid target epoch. /// /// ## Peer scoring /// /// The peer has sent an invalid message. InvalidTargetEpoch { slot: Slot, epoch: Epoch }, /// The attestation references an invalid target block. /// /// ## Peer scoring /// /// The peer has sent an invalid message. InvalidTargetRoot { attestation: Hash256, expected: Option, }, /// There was an error whilst processing the attestation. It is not known if it is valid or invalid. /// /// ## Peer scoring /// /// We were unable to process this attestation due to an internal error. It's unclear if the /// attestation is valid. BeaconChainError(Box), } impl From for Error { fn from(e: BeaconChainError) -> Self { Self::BeaconChainError(Box::new(e)) } } /// Used to avoid double-checking signatures. #[derive(Copy, Clone)] enum CheckAttestationSignature { Yes, No, } /// Wraps a `SignedAggregateAndProof` that has been verified up until the point that an /// `IndexedAttestation` can be derived. /// /// These attestations have *not* undergone signature verification. /// The `observed_attestation_key_root` is the hashed value of an `ObservedAttestationKey`. struct IndexedAggregatedAttestation<'a, T: BeaconChainTypes> { signed_aggregate: &'a SignedAggregateAndProof, indexed_attestation: IndexedAttestation, observed_attestation_key_root: Hash256, } /// Wraps a `Attestation` that has been verified up until the point that an `IndexedAttestation` can /// be derived. /// /// These attestations have *not* undergone signature verification. struct IndexedUnaggregatedAttestation<'a, T: BeaconChainTypes> { attestation: &'a SingleAttestation, indexed_attestation: IndexedAttestation, subnet_id: Option, validator_index: u64, } /// Wraps a `SignedAggregateAndProof` that has been fully verified for propagation on the gossip /// network. pub struct VerifiedAggregatedAttestation<'a, T: BeaconChainTypes> { signed_aggregate: &'a SignedAggregateAndProof, indexed_attestation: IndexedAttestation, } impl VerifiedAggregatedAttestation<'_, T> { pub fn into_indexed_attestation(self) -> IndexedAttestation { self.indexed_attestation } } #[derive(Clone)] /// Wraps an `Attestation` that has been fully verified for propagation on the gossip network. pub struct VerifiedUnaggregatedAttestation<'a, T: BeaconChainTypes> { attestation: Attestation, single_attestation: &'a SingleAttestation, indexed_attestation: IndexedAttestation, subnet_id: SubnetId, } impl VerifiedUnaggregatedAttestation<'_, T> { pub fn into_indexed_attestation(self) -> IndexedAttestation { self.indexed_attestation } pub fn single_attestation(&self) -> SingleAttestation { self.single_attestation.clone() } } /// Custom `Clone` implementation is to avoid the restrictive trait bounds applied by the usual derive /// macro. impl Clone for IndexedUnaggregatedAttestation<'_, T> { fn clone(&self) -> Self { Self { attestation: self.attestation, indexed_attestation: self.indexed_attestation.clone(), subnet_id: self.subnet_id, validator_index: self.validator_index, } } } /// A helper trait implemented on wrapper types that can be progressed to a state where they can be /// verified for application to fork choice. pub trait VerifiedAttestation: Sized { fn attestation(&self) -> AttestationRef<'_, T::EthSpec>; fn indexed_attestation(&self) -> &IndexedAttestation; // Inefficient default implementation. This is overridden for gossip verified attestations. fn into_attestation_and_indices(self) -> (Attestation, Vec) { let attestation = self.attestation().clone_as_attestation(); let attesting_indices = self.indexed_attestation().attesting_indices_to_vec(); (attestation, attesting_indices) } } impl VerifiedAttestation for VerifiedAggregatedAttestation<'_, T> { fn attestation(&self) -> AttestationRef<'_, T::EthSpec> { self.attestation() } fn indexed_attestation(&self) -> &IndexedAttestation { &self.indexed_attestation } } impl VerifiedAttestation for VerifiedUnaggregatedAttestation<'_, T> { fn attestation(&self) -> AttestationRef<'_, T::EthSpec> { self.attestation.to_ref() } fn indexed_attestation(&self) -> &IndexedAttestation { &self.indexed_attestation } } /// Information about invalid attestations which might still be slashable despite being invalid. pub enum AttestationSlashInfo<'a, T: BeaconChainTypes, TErr> { /// The attestation is invalid, but its signature wasn't checked. SignatureNotChecked(AttestationRef<'a, T::EthSpec>, TErr), /// As for `SignatureNotChecked`, but we know the `IndexedAttestation`. SignatureNotCheckedIndexed(IndexedAttestation, TErr), /// As for `SignatureNotChecked`, but for the `SingleAttestation`. SignatureNotCheckedSingle(&'a SingleAttestation, TErr), /// The attestation's signature is invalid, so it will never be slashable. SignatureInvalid(TErr), /// The signature is valid but the attestation is invalid in some other way. SignatureValid(IndexedAttestation, TErr), } /// After processing an attestation normally, optionally process it further for the slasher. /// /// This maps an `AttestationSlashInfo` error back into a regular `Error`, performing signature /// checks on attestations that failed verification for other reasons. /// /// No substantial extra work will be done if there is no slasher configured. fn process_slash_info( slash_info: AttestationSlashInfo, chain: &BeaconChain, ) -> Error { use AttestationSlashInfo::*; if let Some(slasher) = chain.slasher.as_ref() { let (indexed_attestation, check_signature, err) = match slash_info { SignatureNotChecked(attestation, err) => { if let Error::UnknownHeadBlock { .. } = err && attestation.data().beacon_block_root == attestation.data().target.root { return err; } match obtain_indexed_attestation_and_committees_per_slot(chain, attestation) { Ok((indexed, _)) => (indexed, true, err), Err(e) => { debug!( attestation_root = ?attestation.tree_hash_root(), error = ?e, "Unable to obtain indexed form of attestation for slasher" ); return err; } } } SignatureNotCheckedSingle(attestation, err) => { if let Error::UnknownHeadBlock { .. } = err && attestation.data.beacon_block_root == attestation.data.target.root { return err; } let fork_name = chain .spec .fork_name_at_slot::(attestation.data.slot); let indexed_attestation = attestation.to_indexed(fork_name); (indexed_attestation, true, err) } SignatureNotCheckedIndexed(indexed, err) => (indexed, true, err), SignatureInvalid(e) => return e, SignatureValid(indexed, err) => (indexed, false, err), }; if check_signature && let Err(e) = verify_attestation_signature(chain, &indexed_attestation) { debug!( error = ?e, "Signature verification for slasher failed" ); return err; } // Supply to slasher. slasher.accept_attestation(indexed_attestation); err } else { match slash_info { SignatureNotChecked(_, e) | SignatureNotCheckedIndexed(_, e) | SignatureNotCheckedSingle(_, e) | SignatureInvalid(e) | SignatureValid(_, e) => e, } } } impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { /// Returns `Ok(Self)` if the `signed_aggregate` is valid to be (re)published on the gossip /// network. pub fn verify( signed_aggregate: &'a SignedAggregateAndProof, chain: &BeaconChain, ) -> Result { Self::verify_slashable(signed_aggregate, chain) .inspect(|verified_aggregate| { if let Some(slasher) = chain.slasher.as_ref() { slasher.accept_attestation(verified_aggregate.indexed_attestation.clone()); } }) .map_err(|slash_info| process_slash_info(slash_info, chain)) } /// Run the checks that happen before an indexed attestation is constructed. fn verify_early_checks( signed_aggregate: &SignedAggregateAndProof, chain: &BeaconChain, ) -> Result { let attestation = signed_aggregate.message().aggregate(); // Ensure attestation is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). // // We do not queue future attestations for later processing. verify_propagation_slot_range::<_, T::EthSpec>( &chain.slot_clock, attestation.data(), &chain.spec, )?; // Check the attestation's epoch matches its target. if attestation.data().slot.epoch(T::EthSpec::slots_per_epoch()) != attestation.data().target.epoch { return Err(Error::InvalidTargetEpoch { slot: attestation.data().slot, epoch: attestation.data().target.epoch, }); } let observed_attestation_key_root = ObservedAttestationKey { committee_index: attestation .committee_index() .ok_or(Error::NotExactlyOneCommitteeBitSet(0))?, attestation_data: attestation.data().clone(), } .tree_hash_root(); // [New in Electra:EIP7549] verify_committee_index(attestation)?; if chain .observed_attestations .write() .is_known_subset(attestation, observed_attestation_key_root) .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); return Err(Error::AttestationSupersetKnown( observed_attestation_key_root, )); } let aggregator_index = signed_aggregate.message().aggregator_index(); // Ensure there has been no other observed aggregate for the given `aggregator_index`. // // Note: do not observe yet, only observe once the attestation has been verified. match chain .observed_aggregators .read() .validator_has_been_observed(attestation.data().target.epoch, aggregator_index as usize) { Ok(true) => Err(Error::AggregatorAlreadyKnown(aggregator_index)), Ok(false) => Ok(()), Err(ObservedAttestersError::ValidatorIndexTooHigh(i)) => { Err(Error::ValidatorIndexTooHigh(i)) } Err(e) => Err(BeaconChainError::from(e).into()), }?; // Ensure the block being voted for (attestation.data.beacon_block_root) passes validation. // Don't enforce the skip slot restriction for aggregates. // // This indirectly checks to see if the `attestation.data.beacon_block_root` is in our fork // choice. Any known, non-finalized, processed block should be in fork choice, so this // check immediately filters out attestations that attest to a block that has not been // processed. // // Attestations must be for a known block. If the block is unknown, we simply drop the // attestation and do not delay consideration for later. let head_block = verify_head_block_is_known(chain, attestation.data(), None)?; // Check the attestation target root is consistent with the head root. // // This check is not in the specification, however we guard against it since it opens us up // to weird edge cases during verification. // // Whilst this attestation *technically* could be used to add value to a block, it is // invalid in the spirit of the protocol. Here we choose safety over profit. verify_attestation_target_root::(&head_block, attestation.data())?; // Ensure that the attestation has participants. if attestation.is_aggregation_bits_zero() { Err(Error::EmptyAggregationBitfield) } else { Ok(observed_attestation_key_root) } } /// Verify the attestation, producing extra information about whether it might be slashable. pub fn verify_slashable( signed_aggregate: &'a SignedAggregateAndProof, chain: &BeaconChain, ) -> Result> { use AttestationSlashInfo::*; let observed_attestation_key_root = match Self::verify_early_checks(signed_aggregate, chain) { Ok(root) => root, Err(e) => { return Err(SignatureNotChecked( signed_aggregate.message().aggregate(), e, )); } }; // Committees must be sorted by ascending index order 0..committees_per_slot let get_indexed_attestation_with_committee = |(committees, _): (Vec, CommitteesPerSlot)| { let (index, aggregator_index, selection_proof, data) = match signed_aggregate { SignedAggregateAndProof::Base(signed_aggregate) => ( signed_aggregate.message.aggregate.data.index, signed_aggregate.message.aggregator_index, // Note: this clones the signature which is known to be a relatively slow operation. // Future optimizations should remove this clone. signed_aggregate.message.selection_proof.clone(), signed_aggregate.message.aggregate.data.clone(), ), SignedAggregateAndProof::Electra(signed_aggregate) => ( signed_aggregate .message .aggregate .committee_index() .ok_or(Error::NotExactlyOneCommitteeBitSet(0))?, signed_aggregate.message.aggregator_index, signed_aggregate.message.selection_proof.clone(), signed_aggregate.message.aggregate.data.clone(), ), }; let slot = data.slot; let committee = committees .get(index as usize) .ok_or(Error::NoCommitteeForSlotAndIndex { slot, index })?; if !SelectionProof::from(selection_proof) .is_aggregator(committee.committee.len(), &chain.spec) .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { return Err(Error::InvalidSelectionProof { aggregator_index }); } // Ensure the aggregator is a member of the committee for which it is aggregating. if !committee.committee.contains(&(aggregator_index as usize)) { return Err(Error::AggregatorNotInCommittee { aggregator_index }); } // p2p aggregates have a single committee, we can assert that aggregation_bits is always // less then MaxValidatorsPerCommittee match signed_aggregate { SignedAggregateAndProof::Base(signed_aggregate) => { attesting_indices_base::get_indexed_attestation( committee.committee, &signed_aggregate.message.aggregate, ) .map_err(|e| BeaconChainError::from(e).into()) } SignedAggregateAndProof::Electra(signed_aggregate) => { attesting_indices_electra::get_indexed_attestation( &committees, &signed_aggregate.message.aggregate, ) .map_err(|e| BeaconChainError::from(e).into()) } } }; let attestation = signed_aggregate.message().aggregate(); let indexed_attestation = match map_attestation_committees( chain, attestation, get_indexed_attestation_with_committee, ) { Ok(indexed_attestation) => indexed_attestation, Err(e) => { return Err(SignatureNotChecked( signed_aggregate.message().aggregate(), e, )); } }; Ok(IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, observed_attestation_key_root, }) } } impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { /// Run the checks that happen after the indexed attestation and signature have been checked. fn verify_late_checks( signed_aggregate: &SignedAggregateAndProof, observed_attestation_key_root: Hash256, chain: &BeaconChain, ) -> Result<(), Error> { let attestation = signed_aggregate.message().aggregate(); let aggregator_index = signed_aggregate.message().aggregator_index(); // Observe the valid attestation so we do not re-process it. // // It's important to double check that the attestation is not already known, otherwise two // attestations processed at the same time could be published. if let ObserveOutcome::Subset = chain .observed_attestations .write() .observe_item(attestation, Some(observed_attestation_key_root)) .map_err(|e| Error::BeaconChainError(Box::new(e.into())))? { metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); return Err(Error::AttestationSupersetKnown( observed_attestation_key_root, )); } // Observe the aggregator so we don't process another aggregate from them. // // It's important to double check that the attestation is not already known, otherwise two // attestations processed at the same time could be published. if chain .observed_aggregators .write() .observe_validator(attestation.data().target.epoch, aggregator_index as usize) .map_err(BeaconChainError::from)? { return Err(Error::PriorAttestationKnown { validator_index: aggregator_index, epoch: attestation.data().target.epoch, }); } Ok(()) } /// Verify the `signed_aggregate`. pub fn verify( signed_aggregate: &'a SignedAggregateAndProof, chain: &BeaconChain, ) -> Result { let indexed = IndexedAggregatedAttestation::verify(signed_aggregate, chain)?; Self::from_indexed(indexed, chain, CheckAttestationSignature::Yes) } /// Complete the verification of an indexed attestation. fn from_indexed( signed_aggregate: IndexedAggregatedAttestation<'a, T>, chain: &BeaconChain, check_signature: CheckAttestationSignature, ) -> Result { Self::verify_slashable(signed_aggregate, chain, check_signature) .map(|verified_aggregate| verified_aggregate.apply_to_slasher(chain)) .map_err(|slash_info| process_slash_info(slash_info, chain)) } fn apply_to_slasher(self, chain: &BeaconChain) -> Self { if let Some(slasher) = chain.slasher.as_ref() { slasher.accept_attestation(self.indexed_attestation.clone()); } self } /// Verify the attestation, producing extra information about whether it might be slashable. fn verify_slashable( signed_aggregate: IndexedAggregatedAttestation<'a, T>, chain: &BeaconChain, check_signature: CheckAttestationSignature, ) -> Result> { use AttestationSlashInfo::*; let IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, observed_attestation_key_root, } = signed_aggregate; match check_signature { CheckAttestationSignature::Yes => { // Ensure that all signatures are valid. if let Err(e) = verify_signed_aggregate_signatures( chain, signed_aggregate, &indexed_attestation, ) .and_then(|is_valid| { if !is_valid { Err(Error::InvalidSignature) } else { Ok(()) } }) { return Err(SignatureInvalid(e)); } } CheckAttestationSignature::No => (), }; if let Err(e) = Self::verify_late_checks(signed_aggregate, observed_attestation_key_root, chain) { return Err(SignatureValid(indexed_attestation, e)); } Ok(VerifiedAggregatedAttestation { signed_aggregate, indexed_attestation, }) } /// Returns the underlying `attestation` for the `signed_aggregate`. pub fn attestation(&self) -> AttestationRef<'a, T::EthSpec> { self.signed_aggregate.message().aggregate() } /// Returns the underlying `signed_aggregate`. pub fn aggregate(&self) -> &SignedAggregateAndProof { self.signed_aggregate } } impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { /// Run the checks that happen before an indexed attestation is constructed. pub fn verify_early_checks( attestation: &'a SingleAttestation, chain: &BeaconChain, ) -> Result<(), Error> { let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()); // Check the attestation's epoch matches its target. if attestation_epoch != attestation.data.target.epoch { return Err(Error::InvalidTargetEpoch { slot: attestation.data.slot, epoch: attestation.data.target.epoch, }); } // Ensure attestation is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). // // We do not queue future attestations for later processing. verify_propagation_slot_range::<_, T::EthSpec>( &chain.slot_clock, &attestation.data, &chain.spec, )?; let fork_name = chain .spec .fork_name_at_slot::(attestation.data.slot); if fork_name.electra_enabled() { // [New in Electra:EIP7549] if attestation.data.index != 0 { return Err(Error::CommitteeIndexNonZero( attestation.data.index as usize, )); } } // Attestations must be for a known block. If the block is unknown, we simply drop the // attestation and do not delay consideration for later. // // Enforce a maximum skip distance for unaggregated attestations. let head_block = verify_head_block_is_known( chain, &attestation.data, chain.config.import_max_skip_slots, )?; // Check the attestation target root is consistent with the head root. verify_attestation_target_root::(&head_block, &attestation.data)?; Ok(()) } /// Run the checks that apply to the indexed attestation before the signature is checked. pub fn verify_middle_checks( attestation: &'a SingleAttestation, chain: &BeaconChain, ) -> Result { let validator_index = attestation.attester_index; /* * The attestation is the first valid attestation received for the participating validator * for the slot, attestation.data.slot. */ if chain .observed_gossip_attesters .read() .validator_has_been_observed(attestation.data.target.epoch, validator_index as usize) .map_err(BeaconChainError::from)? { return Err(Error::PriorAttestationKnown { validator_index, epoch: attestation.data.target.epoch, }); } Ok(validator_index) } /// Returns `Ok(Self)` if the `attestation` is valid to be (re)published on the gossip /// network. /// /// `subnet_id` is the subnet from which we received this attestation. This function will /// verify that it was received on the correct subnet. pub fn verify( attestation: &'a SingleAttestation, subnet_id: Option, chain: &BeaconChain, ) -> Result { Self::verify_slashable(attestation, subnet_id, chain) .inspect(|verified_unaggregated| { if let Some(slasher) = chain.slasher.as_ref() { slasher.accept_attestation(verified_unaggregated.indexed_attestation.clone()); } }) .map_err(|slash_info| process_slash_info(slash_info, chain)) } /// Verify the attestation, producing extra information about whether it might be slashable. pub fn verify_slashable( attestation: &'a SingleAttestation, subnet_id: Option, chain: &BeaconChain, ) -> Result> { use AttestationSlashInfo::*; if let Err(e) = Self::verify_early_checks(attestation, chain) { return Err(SignatureNotCheckedSingle(attestation, e)); } let fork_name = chain .spec .fork_name_at_slot::(attestation.data.slot); let indexed_attestation = attestation.to_indexed(fork_name); let validator_index = match Self::verify_middle_checks(attestation, chain) { Ok(t) => t, Err(e) => return Err(SignatureNotCheckedIndexed(indexed_attestation, e)), }; Ok(Self { attestation, indexed_attestation, subnet_id, validator_index, }) } /// Returns a mutable reference to the underlying attestation. /// /// Only use during testing since modifying the `IndexedAttestation` can cause the attestation /// to no-longer be valid. pub fn __indexed_attestation_mut(&mut self) -> &mut IndexedAttestation { &mut self.indexed_attestation } } impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { /// Run the checks that apply after the signature has been checked. fn verify_late_checks( attestation: &'a SingleAttestation, validator_index: u64, subnet_id: Option, chain: &BeaconChain, ) -> Result<(Attestation, SubnetId), Error> { // Check that the attester is a member of the committee let (committee_opt, committees_per_slot) = chain.with_committee_cache( attestation.data.target.root, attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()), |committee_cache, _| { let committee_opt = committee_cache .get_beacon_committee(attestation.data.slot, attestation.committee_index) .map(|beacon_committee| beacon_committee.committee.to_vec()); Ok((committee_opt, committee_cache.committees_per_slot())) }, )?; let Some(committee) = committee_opt else { return Err(Error::NoCommitteeForSlotAndIndex { slot: attestation.data.slot, index: attestation.committee_index, }); }; if !committee.contains(&(attestation.attester_index as usize)) { return Err(Error::AttesterNotInCommittee { attester_index: attestation.attester_index, committee_index: attestation.committee_index, slot: attestation.data.slot, }); } let expected_subnet_id = SubnetId::compute_subnet_for_single_attestation::( attestation, committees_per_slot, &chain.spec, ) .map_err(BeaconChainError::from)?; // If a subnet was specified, ensure that subnet is correct. if let Some(subnet_id) = subnet_id && subnet_id != expected_subnet_id { return Err(Error::InvalidSubnetId { received: subnet_id, expected: expected_subnet_id, }); }; // Now that the attestation has been fully verified, store that we have received a valid // attestation from this validator. // // It's important to double check that the attestation still hasn't been observed, since // there can be a race-condition if we receive two attestations at the same time and // process them in different threads. if chain .observed_gossip_attesters .write() .observe_validator(attestation.data.target.epoch, validator_index as usize) .map_err(BeaconChainError::from)? { return Err(Error::PriorAttestationKnown { validator_index, epoch: attestation.data.target.epoch, }); } let fork_name = chain .spec .fork_name_at_slot::(attestation.data.slot); let unaggregated_attestation = single_attestation_to_attestation(attestation, &committee, fork_name)?; Ok((unaggregated_attestation, expected_subnet_id)) } /// Verify the `unaggregated_attestation`. pub fn verify( unaggregated_attestation: &'a SingleAttestation, subnet_id: Option, chain: &BeaconChain, ) -> Result { let indexed = IndexedUnaggregatedAttestation::verify(unaggregated_attestation, subnet_id, chain)?; Self::from_indexed(indexed, chain, CheckAttestationSignature::Yes) } /// Complete the verification of an indexed attestation. fn from_indexed( attestation: IndexedUnaggregatedAttestation<'a, T>, chain: &BeaconChain, check_signature: CheckAttestationSignature, ) -> Result { Self::verify_slashable(attestation, chain, check_signature) .map(|verified_unaggregated| verified_unaggregated.apply_to_slasher(chain)) .map_err(|slash_info| process_slash_info(slash_info, chain)) } fn apply_to_slasher(self, chain: &BeaconChain) -> Self { if let Some(slasher) = chain.slasher.as_ref() { slasher.accept_attestation(self.indexed_attestation.clone()); } self } /// Verify the attestation, producing extra information about whether it might be slashable. fn verify_slashable( attestation: IndexedUnaggregatedAttestation<'a, T>, chain: &BeaconChain, check_signature: CheckAttestationSignature, ) -> Result> { use AttestationSlashInfo::*; let IndexedUnaggregatedAttestation { attestation, indexed_attestation, subnet_id, validator_index, } = attestation; match check_signature { CheckAttestationSignature::Yes => { if let Err(e) = verify_attestation_signature(chain, &indexed_attestation) { return Err(SignatureInvalid(e)); } } CheckAttestationSignature::No => (), }; let (unaggregated_attestation, subnet_id) = match Self::verify_late_checks(attestation, validator_index, subnet_id, chain) { Ok(a) => a, Err(e) => return Err(SignatureValid(indexed_attestation, e)), }; Ok(Self { single_attestation: attestation, attestation: unaggregated_attestation, indexed_attestation, subnet_id, }) } /// Returns the correct subnet for the attestation. pub fn subnet_id(&self) -> SubnetId { self.subnet_id } /// Returns the wrapped `indexed_attestation`. pub fn indexed_attestation(&self) -> &IndexedAttestation { &self.indexed_attestation } /// Returns a mutable reference to the underlying attestation. /// /// Only use during testing since modifying the `IndexedAttestation` can cause the attestation /// to no-longer be valid. pub fn __indexed_attestation_mut(&mut self) -> &mut IndexedAttestation { &mut self.indexed_attestation } } /// Returns `Ok(())` if the `attestation.data.beacon_block_root` is known to this chain. /// /// The block root may not be known for two reasons: /// /// 1. The block has never been verified by our application. /// 2. The block is prior to the latest finalized block. /// /// Case (1) is the exact thing we're trying to detect. However case (2) is a little different, but /// it's still fine to reject here because there's no need for us to handle attestations that are /// already finalized. fn verify_head_block_is_known( chain: &BeaconChain, attestation_data: &AttestationData, max_skip_slots: Option, ) -> Result { let block_opt = chain .canonical_head .fork_choice_read_lock() .get_block(&attestation_data.beacon_block_root) .or_else(|| { chain .early_attester_cache .get_proto_block(attestation_data.beacon_block_root) }); if let Some(block) = block_opt { // Reject any block that exceeds our limit on skipped slots. if let Some(max_skip_slots) = max_skip_slots && attestation_data.slot > block.slot + max_skip_slots { return Err(Error::TooManySkippedSlots { head_block_slot: block.slot, attestation_slot: attestation_data.slot, }); } if !verify_attestation_is_finalized_checkpoint_or_descendant(attestation_data, chain) { return Err(Error::HeadBlockFinalized { beacon_block_root: attestation_data.beacon_block_root, }); } Ok(block) } else if chain.is_pre_finalization_block(attestation_data.beacon_block_root)? { Err(Error::HeadBlockFinalized { beacon_block_root: attestation_data.beacon_block_root, }) } else { // The block is either: // // 1) A pre-finalization block that has been pruned. We'll do one network lookup // for it and when it fails we will penalise all involved peers. // 2) A post-finalization block that we don't know about yet. We'll queue // the attestation until the block becomes available (or we time out). Err(Error::UnknownHeadBlock { beacon_block_root: attestation_data.beacon_block_root, }) } } /// Verify that the `attestation` is within the acceptable gossip propagation range, with reference /// to the current slot of the `chain`. /// /// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. pub fn verify_propagation_slot_range( slot_clock: &S, attestation: &AttestationData, spec: &ChainSpec, ) -> Result<(), Error> { let attestation_slot = attestation.slot; let latest_permissible_slot = slot_clock .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)?; if attestation_slot > latest_permissible_slot { return Err(Error::FutureSlot { attestation_slot, latest_permissible_slot, }); } // Taking advantage of saturating subtraction on `Slot`. let one_epoch_prior = slot_clock .now_with_past_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)? - E::slots_per_epoch(); let current_fork = spec.fork_name_at_slot::(slot_clock.now().ok_or(BeaconChainError::UnableToReadSlot)?); let earliest_permissible_slot = if current_fork.deneb_enabled() { // EIP-7045 one_epoch_prior .epoch(E::slots_per_epoch()) .start_slot(E::slots_per_epoch()) } else { one_epoch_prior }; if attestation_slot < earliest_permissible_slot { return Err(Error::PastSlot { attestation_slot, earliest_permissible_slot, }); } Ok(()) } /// Verifies that the signature of the `indexed_attestation` is valid. pub fn verify_attestation_signature( chain: &BeaconChain, indexed_attestation: &IndexedAttestation, ) -> Result<(), Error> { let signature_setup_timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SIGNATURE_SETUP_TIMES); let pubkey_cache = chain.validator_pubkey_cache.read(); let fork = chain .spec .fork_at_epoch(indexed_attestation.data().target.epoch); let signature_set = indexed_attestation_signature_set_from_pubkeys( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), indexed_attestation.signature(), indexed_attestation, &fork, chain.genesis_validators_root, &chain.spec, ) .map_err(BeaconChainError::SignatureSetError)?; metrics::stop_timer(signature_setup_timer); let _signature_verification_timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_SIGNATURE_TIMES); if signature_set.verify() { Ok(()) } else { Err(Error::InvalidSignature) } } /// Verifies that the `attestation.data.target.root` is indeed the target root of the block at /// `attestation.data.beacon_block_root`. pub fn verify_attestation_target_root( head_block: &ProtoBlock, attestation_data: &AttestationData, ) -> Result<(), Error> { // Check the attestation target root. let head_block_epoch = head_block.slot.epoch(E::slots_per_epoch()); let attestation_epoch = attestation_data.slot.epoch(E::slots_per_epoch()); if head_block_epoch > attestation_epoch { // The epoch references an invalid head block from a future epoch. // // This check is not in the specification, however we guard against it since it opens us up // to weird edge cases during verification. // // Whilst this attestation *technically* could be used to add value to a block, it is // invalid in the spirit of the protocol. Here we choose safety over profit. // // Reference: // https://github.com/ethereum/eth2.0-specs/pull/2001#issuecomment-699246659 return Err(Error::InvalidTargetRoot { attestation: attestation_data.target.root, // It is not clear what root we should expect in this case, since the attestation is // fundamentally invalid. expected: None, }); } else { let target_root = if head_block_epoch == attestation_epoch { // If the block is in the same epoch as the attestation, then use the target root // from the block. head_block.target_root } else { // If the head block is from a previous epoch then skip slots will cause the head block // root to become the target block root. // // We know the head block is from a previous epoch due to a previous check. head_block.root }; // Reject any attestation with an invalid target root. if target_root != attestation_data.target.root { return Err(Error::InvalidTargetRoot { attestation: attestation_data.target.root, expected: Some(target_root), }); } } Ok(()) } /// Verifies all the signatures in a `SignedAggregateAndProof` using BLS batch verification. This /// includes three signatures: /// /// - `signed_aggregate.signature` /// - `signed_aggregate.message.selection_proof` /// - `signed_aggregate.message.aggregate.signature` /// /// # Returns /// /// - `Ok(true)`: if all signatures are valid. /// - `Ok(false)`: if one or more signatures are invalid. /// - `Err(e)`: if there was an error preventing signature verification. pub fn verify_signed_aggregate_signatures( chain: &BeaconChain, signed_aggregate: &SignedAggregateAndProof, indexed_attestation: &IndexedAttestation, ) -> Result { let pubkey_cache = chain.validator_pubkey_cache.read(); let aggregator_index = signed_aggregate.message().aggregator_index(); if aggregator_index >= pubkey_cache.len() as u64 { return Err(Error::AggregatorPubkeyUnknown(aggregator_index)); } let fork = chain .spec .fork_at_epoch(indexed_attestation.data().target.epoch); let signature_sets = vec![ signed_aggregate_selection_proof_signature_set( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), signed_aggregate, &fork, chain.genesis_validators_root, &chain.spec, ) .map_err(BeaconChainError::SignatureSetError)?, signed_aggregate_signature_set( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), signed_aggregate, &fork, chain.genesis_validators_root, &chain.spec, ) .map_err(BeaconChainError::SignatureSetError)?, indexed_attestation_signature_set_from_pubkeys( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), indexed_attestation.signature(), indexed_attestation, &fork, chain.genesis_validators_root, &chain.spec, ) .map_err(BeaconChainError::SignatureSetError)?, ]; Ok(verify_signature_sets(signature_sets.iter())) } /// Verify that the `attestation` committee index is properly set for the attestation's fork. /// This function will only apply verification post-Electra. pub fn verify_committee_index(attestation: AttestationRef) -> Result<(), Error> { if let Ok(committee_bits) = attestation.committee_bits() { // Check to ensure that the attestation is for a single committee. let num_committee_bits = get_committee_indices::(committee_bits); if num_committee_bits.len() != 1 { return Err(Error::NotExactlyOneCommitteeBitSet( num_committee_bits.len(), )); } // Ensure the attestation index is set to zero post Electra. if attestation.data().index != 0 { return Err(Error::CommitteeIndexNonZero( attestation.data().index as usize, )); } } Ok(()) } fn verify_attestation_is_finalized_checkpoint_or_descendant( attestation_data: &AttestationData, chain: &BeaconChain, ) -> bool { // If we have a split block newer than finalization then we also ban attestations which are not // descended from that split block. It's important not to try checking `is_descendant` if // finality is ahead of the split and the split block has been pruned, as `is_descendant` will // return `false` in this case. let fork_choice = chain.canonical_head.fork_choice_read_lock(); let attestation_block_root = attestation_data.beacon_block_root; let finalized_slot = fork_choice .finalized_checkpoint() .epoch .start_slot(T::EthSpec::slots_per_epoch()); let split = chain.store.get_split_info(); let is_descendant_from_split_block = split.slot == 0 || split.slot <= finalized_slot || fork_choice.is_descendant(split.block_root, attestation_block_root); fork_choice.is_finalized_checkpoint_or_descendant(attestation_block_root) && is_descendant_from_split_block } /// Assists in readability. type CommitteesPerSlot = u64; /// Returns the `indexed_attestation` and committee count per slot for the `attestation` using the /// public keys cached in the `chain`. pub fn obtain_indexed_attestation_and_committees_per_slot( chain: &BeaconChain, attestation: AttestationRef, ) -> Result<(IndexedAttestation, CommitteesPerSlot), Error> { map_attestation_committees(chain, attestation, |(committees, committees_per_slot)| { match attestation { AttestationRef::Base(att) => { let committee = committees .iter() .filter(|&committee| committee.index == att.data.index) .at_most_one() .map_err(|_| Error::NoCommitteeForSlotAndIndex { slot: att.data.slot, index: att.data.index, })?; if let Some(committee) = committee { attesting_indices_base::get_indexed_attestation(committee.committee, att) .map(|attestation| (attestation, committees_per_slot)) .map_err(Error::Invalid) } else { Err(Error::NoCommitteeForSlotAndIndex { slot: att.data.slot, index: att.data.index, }) } } AttestationRef::Electra(att) => { attesting_indices_electra::get_indexed_attestation(&committees, att) .map(|attestation| (attestation, committees_per_slot)) .map_err(|e| { if let BlockOperationError::BeaconStateError(NoCommitteeFound(index)) = e { Error::NoCommitteeForSlotAndIndex { slot: att.data.slot, index, } } else { Error::Invalid(e) } }) } } }) } /// Runs the `map_fn` with the committee and committee count per slot for the given `attestation`. /// /// This function exists in this odd "map" pattern because efficiently obtaining the committees for /// an attestation's slot can be complex. It might involve reading straight from the /// `beacon_chain.shuffling_cache` or it might involve reading it from a state from the DB. Due to /// the complexities of `RwLock`s on the shuffling cache, a simple `Cow` isn't suitable here. /// /// If the committees for an `attestation`'s slot aren't found in the `shuffling_cache`, we will read a state /// from disk and then update the `shuffling_cache`. /// /// Committees are sorted by ascending index order 0..committees_per_slot fn map_attestation_committees( chain: &BeaconChain, attestation: AttestationRef, map_fn: F, ) -> Result where T: BeaconChainTypes, F: Fn((Vec, CommitteesPerSlot)) -> Result, { let attestation_epoch = attestation.data().slot.epoch(T::EthSpec::slots_per_epoch()); let target = &attestation.data().target; // Attestation target must be for a known block. // // We use fork choice to find the target root, which means that we reject any attestation // that has a `target.root` earlier than our latest finalized root. There's no point in // processing an attestation that does not include our latest finalized block in its chain. // // We do not delay consideration for later, we simply drop the attestation. if !chain .canonical_head .fork_choice_read_lock() .contains_block(&target.root) && !chain.early_attester_cache.contains_block(target.root) { return Err(Error::UnknownTargetRoot(target.root)); } chain.with_committee_cache(target.root, attestation_epoch, |committee_cache, _| { let committees_per_slot = committee_cache.committees_per_slot(); Ok(committee_cache .get_beacon_committees_at_slot(attestation.data().slot) .map(|committees| map_fn((committees, committees_per_slot))) .unwrap_or_else(|_| { Err(Error::NoCommitteeForSlotAndIndex { slot: attestation.data().slot, index: attestation.committee_index().unwrap_or(0), }) })) })? }