From 27ed90e4dc88b61c7e7c190d088560baf2412362 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 21 Jun 2024 00:20:10 -0400 Subject: [PATCH 01/14] Electra attestation changes sean review (#5972) * instantiate empty bitlist in unreachable code * clean up error conversion * fork enabled bool cleanup * remove a couple todos * return bools instead of options in `aggregate` and use the result * delete commented out code * use map macros in simple transformations * remove signers_disjoint_from * get ef tests compiling * get ef tests compiling * update intentionally excluded files --- .../src/attestation_verification.rs | 32 +++---------------- beacon_node/operation_pool/src/attestation.rs | 4 +-- .../operation_pool/src/attestation_storage.rs | 24 +++++++------- beacon_node/operation_pool/src/lib.rs | 8 ++--- .../state_processing/src/verify_operation.rs | 1 - consensus/types/src/aggregate_and_proof.rs | 19 ++++++----- consensus/types/src/attestation.rs | 23 ++----------- consensus/types/src/fork_name.rs | 4 +++ .../types/src/signed_aggregate_and_proof.rs | 24 +++++++------- .../types/src/sync_committee_contribution.rs | 8 ----- testing/ef_tests/check_all_files_accessed.py | 4 ++- testing/ef_tests/src/type_name.rs | 4 +++ testing/ef_tests/tests/tests.rs | 1 - 13 files changed, 56 insertions(+), 100 deletions(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 32c33f3e61..c7f7bb58da 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -61,10 +61,9 @@ use std::borrow::Cow; use strum::AsRefStr; use tree_hash::TreeHash; use types::{ - Attestation, AttestationRef, BeaconCommittee, - BeaconStateError::{self, NoCommitteeFound}, - ChainSpec, CommitteeIndex, Epoch, EthSpec, ForkName, Hash256, IndexedAttestation, - SelectionProof, SignedAggregateAndProof, Slot, SubnetId, + Attestation, AttestationRef, BeaconCommittee, BeaconStateError::NoCommitteeFound, ChainSpec, + CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, SelectionProof, + SignedAggregateAndProof, Slot, SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -266,30 +265,9 @@ pub enum Error { BeaconChainError(BeaconChainError), } -// TODO(electra) the error conversion changes here are to get a test case to pass -// this could easily be cleaned up impl From for Error { fn from(e: BeaconChainError) -> Self { - match &e { - BeaconChainError::BeaconStateError(beacon_state_error) => { - if let BeaconStateError::AggregatorNotInCommittee { aggregator_index } = - beacon_state_error - { - Self::AggregatorNotInCommittee { - aggregator_index: *aggregator_index, - } - } else if let BeaconStateError::InvalidSelectionProof { aggregator_index } = - beacon_state_error - { - Self::InvalidSelectionProof { - aggregator_index: *aggregator_index, - } - } else { - Error::BeaconChainError(e) - } - } - _ => Error::BeaconChainError(e), - } + Self::BeaconChainError(e) } } @@ -1169,7 +1147,7 @@ pub fn verify_propagation_slot_range( let current_fork = spec.fork_name_at_slot::(slot_clock.now().ok_or(BeaconChainError::UnableToReadSlot)?); - let earliest_permissible_slot = if current_fork < ForkName::Deneb { + let earliest_permissible_slot = if !current_fork.deneb_enabled() { one_epoch_prior // EIP-7045 } else { diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index c6ed6eb7f6..97d0583e34 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -184,8 +184,8 @@ pub fn earliest_attestation_validators( // Bitfield of validators whose attestations are new/fresh. let mut new_validators = match attestation.indexed { CompactIndexedAttestation::Base(indexed_att) => indexed_att.aggregation_bits.clone(), - // TODO(electra) per the comments above, this code path is obsolete post altair fork, so maybe we should just return an empty bitlist here? - CompactIndexedAttestation::Electra(_) => todo!(), + // This code path is obsolete post altair fork, so we just return an empty bitlist here. + CompactIndexedAttestation::Electra(_) => return BitList::with_capacity(0).unwrap(), }; let state_attestations = if attestation.checkpoint.target_epoch == state.current_epoch() { diff --git a/beacon_node/operation_pool/src/attestation_storage.rs b/beacon_node/operation_pool/src/attestation_storage.rs index ad4ffe6d3c..4de9d351f3 100644 --- a/beacon_node/operation_pool/src/attestation_storage.rs +++ b/beacon_node/operation_pool/src/attestation_storage.rs @@ -165,22 +165,22 @@ impl CompactIndexedAttestation { CompactIndexedAttestation::Electra(this), CompactIndexedAttestation::Electra(other), ) => this.should_aggregate(other), - // TODO(electra) is a mix of electra and base compact indexed attestations an edge case we need to deal with? _ => false, } } - pub fn aggregate(&mut self, other: &Self) -> Option<()> { + /// Returns `true` if aggregated, otherwise `false`. + pub fn aggregate(&mut self, other: &Self) -> bool { match (self, other) { (CompactIndexedAttestation::Base(this), CompactIndexedAttestation::Base(other)) => { - this.aggregate(other) + this.aggregate(other); + true } ( CompactIndexedAttestation::Electra(this), CompactIndexedAttestation::Electra(other), ) => this.aggregate_same_committee(other), - // TODO(electra) is a mix of electra and base compact indexed attestations an edge case we need to deal with? - _ => None, + _ => false, } } } @@ -192,7 +192,7 @@ impl CompactIndexedAttestationBase { .is_zero() } - pub fn aggregate(&mut self, other: &Self) -> Option<()> { + pub fn aggregate(&mut self, other: &Self) { self.attesting_indices = self .attesting_indices .drain(..) @@ -201,8 +201,6 @@ impl CompactIndexedAttestationBase { .collect(); self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); self.signature.add_assign_aggregate(&other.signature); - - Some(()) } } @@ -216,9 +214,10 @@ impl CompactIndexedAttestationElectra { .is_zero() } - pub fn aggregate_same_committee(&mut self, other: &Self) -> Option<()> { + /// Returns `true` if aggregated, otherwise `false`. + pub fn aggregate_same_committee(&mut self, other: &Self) -> bool { if self.committee_bits != other.committee_bits { - return None; + return false; } self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); self.attesting_indices = self @@ -228,7 +227,7 @@ impl CompactIndexedAttestationElectra { .dedup() .collect(); self.signature.add_assign_aggregate(&other.signature); - Some(()) + true } pub fn aggregate_with_disjoint_committees(&mut self, other: &Self) -> Option<()> { @@ -318,8 +317,7 @@ impl AttestationMap { for existing_attestation in attestations.iter_mut() { if existing_attestation.should_aggregate(&indexed) { - existing_attestation.aggregate(&indexed); - aggregated = true; + aggregated = existing_attestation.aggregate(&indexed); } else if *existing_attestation == indexed { aggregated = true; } diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 49fbed5686..a1c9ada03a 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -39,7 +39,7 @@ use std::ptr; use types::{ sync_aggregate::Error as SyncAggregateError, typenum::Unsigned, AbstractExecPayload, Attestation, AttestationData, AttesterSlashing, BeaconState, BeaconStateError, ChainSpec, - Epoch, EthSpec, ForkName, ProposerSlashing, SignedBeaconBlock, SignedBlsToExecutionChange, + Epoch, EthSpec, ProposerSlashing, SignedBeaconBlock, SignedBlsToExecutionChange, SignedVoluntaryExit, Slot, SyncAggregate, SyncCommitteeContribution, Validator, }; @@ -316,10 +316,10 @@ impl OperationPool { ) .inspect(|_| num_curr_valid += 1); - let curr_epoch_limit = if fork_name < ForkName::Electra { - E::MaxAttestations::to_usize() - } else { + let curr_epoch_limit = if fork_name.electra_enabled() { E::MaxAttestationsElectra::to_usize() + } else { + E::MaxAttestations::to_usize() }; let prev_epoch_limit = if let BeaconState::Base(base_state) = state { std::cmp::min( diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index f218b806d2..c4b7c6a026 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -48,7 +48,6 @@ pub trait TransformPersist { pub struct SigVerifiedOp { op: T, verified_against: VerifiedAgainst, - //#[ssz(skip_serializing, skip_deserializing)] _phantom: PhantomData, } diff --git a/consensus/types/src/aggregate_and_proof.rs b/consensus/types/src/aggregate_and_proof.rs index 6ae0df4ad7..223b12e768 100644 --- a/consensus/types/src/aggregate_and_proof.rs +++ b/consensus/types/src/aggregate_and_proof.rs @@ -33,7 +33,8 @@ use tree_hash_derive::TreeHash; derive(Debug, PartialEq, TreeHash, Serialize,), serde(untagged, bound = "E: EthSpec"), tree_hash(enum_behaviour = "transparent") - ) + ), + map_ref_into(AttestationRef) )] #[derive( arbitrary::Arbitrary, Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash, @@ -59,19 +60,17 @@ pub struct AggregateAndProof { impl<'a, E: EthSpec> AggregateAndProofRef<'a, E> { /// Returns `true` if `validator_pubkey` signed over `self.aggregate.data.slot`. pub fn aggregate(self) -> AttestationRef<'a, E> { - match self { - AggregateAndProofRef::Base(a) => AttestationRef::Base(&a.aggregate), - AggregateAndProofRef::Electra(a) => AttestationRef::Electra(&a.aggregate), - } + map_aggregate_and_proof_ref_into_attestation_ref!(&'a _, self, |inner, cons| { + cons(&inner.aggregate) + }) } } impl AggregateAndProof { /// Returns `true` if `validator_pubkey` signed over `self.aggregate.data.slot`. - pub fn aggregate(&self) -> AttestationRef { - match self { - AggregateAndProof::Base(a) => AttestationRef::Base(&a.aggregate), - AggregateAndProof::Electra(a) => AttestationRef::Electra(&a.aggregate), - } + pub fn aggregate<'a>(&'a self) -> AttestationRef<'a, E> { + map_aggregate_and_proof_ref_into_attestation_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(&inner.aggregate) + }) } } diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index c8d1c3fb9b..88993267a9 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -1,6 +1,6 @@ use crate::slot_data::SlotData; +use crate::Checkpoint; use crate::{test_utils::TestRandom, Hash256, Slot}; -use crate::{Checkpoint, ForkName}; use derivative::Derivative; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; @@ -99,7 +99,7 @@ impl Attestation { target: Checkpoint, spec: &ChainSpec, ) -> Result { - if spec.fork_name_at_slot::(slot) >= ForkName::Electra { + if spec.fork_name_at_slot::(slot).electra_enabled() { let mut committee_bits: BitVector = BitVector::default(); committee_bits .set(committee_index as usize, true) @@ -277,16 +277,6 @@ impl<'a, E: EthSpec> AttestationRef<'a, E> { } impl AttestationElectra { - /// Are the aggregation bitfields of these attestations disjoint? - // TODO(electra): check whether the definition from CompactIndexedAttestation::should_aggregate - // is useful where this is used, i.e. only consider attestations disjoint when their committees - // match AND their aggregation bits do not intersect. - pub fn signers_disjoint_from(&self, other: &Self) -> bool { - self.aggregation_bits - .intersection(&other.aggregation_bits) - .is_zero() - } - pub fn committee_index(&self) -> Option { self.get_committee_indices().first().cloned() } @@ -304,7 +294,6 @@ impl AttestationElectra { /// The aggregation bitfields must be disjoint, and the data must be the same. pub fn aggregate(&mut self, other: &Self) { debug_assert_eq!(self.data, other.data); - debug_assert!(self.signers_disjoint_from(other)); self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); self.signature.add_assign_aggregate(&other.signature); } @@ -358,19 +347,11 @@ impl AttestationElectra { } impl AttestationBase { - /// Are the aggregation bitfields of these attestations disjoint? - pub fn signers_disjoint_from(&self, other: &Self) -> bool { - self.aggregation_bits - .intersection(&other.aggregation_bits) - .is_zero() - } - /// Aggregate another Attestation into this one. /// /// The aggregation bitfields must be disjoint, and the data must be the same. pub fn aggregate(&mut self, other: &Self) { debug_assert_eq!(self.data, other.data); - debug_assert!(self.signers_disjoint_from(other)); self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); self.signature.add_assign_aggregate(&other.signature); } diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork_name.rs index f0810e2bdb..96f206d454 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork_name.rs @@ -120,6 +120,10 @@ impl ForkName { } } + pub fn deneb_enabled(self) -> bool { + self >= ForkName::Deneb + } + pub fn electra_enabled(self) -> bool { self >= ForkName::Electra } diff --git a/consensus/types/src/signed_aggregate_and_proof.rs b/consensus/types/src/signed_aggregate_and_proof.rs index d339cecaae..26eca19bf1 100644 --- a/consensus/types/src/signed_aggregate_and_proof.rs +++ b/consensus/types/src/signed_aggregate_and_proof.rs @@ -34,7 +34,9 @@ use tree_hash_derive::TreeHash; ), serde(bound = "E: EthSpec"), arbitrary(bound = "E: EthSpec"), - ) + ), + map_into(Attestation), + map_ref_into(AggregateAndProofRef) )] #[derive( arbitrary::Arbitrary, Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash, @@ -102,19 +104,17 @@ impl SignedAggregateAndProof { } } - pub fn message(&self) -> AggregateAndProofRef { - match self { - SignedAggregateAndProof::Base(message) => AggregateAndProofRef::Base(&message.message), - SignedAggregateAndProof::Electra(message) => { - AggregateAndProofRef::Electra(&message.message) - } - } + pub fn message<'a>(&'a self) -> AggregateAndProofRef<'a, E> { + map_signed_aggregate_and_proof_ref_into_aggregate_and_proof_ref!( + &'a _, + self.to_ref(), + |inner, cons| { cons(&inner.message) } + ) } pub fn into_attestation(self) -> Attestation { - match self { - Self::Base(att) => Attestation::Base(att.message.aggregate), - Self::Electra(att) => Attestation::Electra(att.message.aggregate), - } + map_signed_aggregate_and_proof_into_attestation!(self, |inner, cons| { + cons(inner.message.aggregate) + }) } } diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee_contribution.rs index 81de83ba3b..c348c3e8be 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee_contribution.rs @@ -63,13 +63,6 @@ impl SyncCommitteeContribution { }) } - /// Are the aggregation bitfields of these sync contribution disjoint? - pub fn signers_disjoint_from(&self, other: &Self) -> bool { - self.aggregation_bits - .intersection(&other.aggregation_bits) - .is_zero() - } - /// Aggregate another `SyncCommitteeContribution` into this one. /// /// The aggregation bitfields must be disjoint, and the data must be the same. @@ -77,7 +70,6 @@ impl SyncCommitteeContribution { debug_assert_eq!(self.slot, other.slot); debug_assert_eq!(self.beacon_block_root, other.beacon_block_root); debug_assert_eq!(self.subcommittee_index, other.subcommittee_index); - debug_assert!(self.signers_disjoint_from(other)); self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); self.signature.add_assign_aggregate(&other.signature); diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 7629d61827..5f425ab98b 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -42,7 +42,9 @@ excluded_paths = [ "bls12-381-tests/deserialization_G2", "bls12-381-tests/hash_to_G2", "tests/.*/eip6110", - "tests/.*/whisk" + "tests/.*/whisk", + # TODO(electra): re-enable in https://github.com/sigp/lighthouse/pull/5758 + "tests/.*/.*/ssz_static/IndexedAttestation" ] diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index cbea78dabf..f886431b40 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -41,6 +41,8 @@ type_name_generic!(AggregateAndProof); type_name_generic!(AggregateAndProofBase, "AggregateAndProof"); type_name_generic!(AggregateAndProofElectra, "AggregateAndProof"); type_name_generic!(Attestation); +type_name_generic!(AttestationBase, "Attestation"); +type_name_generic!(AttestationElectra, "Attestation"); type_name!(AttestationData); type_name_generic!(AttesterSlashing); type_name_generic!(AttesterSlashingBase, "AttesterSlashing"); @@ -76,6 +78,8 @@ type_name!(Fork); type_name!(ForkData); type_name_generic!(HistoricalBatch); type_name_generic!(IndexedAttestation); +type_name_generic!(IndexedAttestationBase, "IndexedAttestation"); +type_name_generic!(IndexedAttestationElectra, "IndexedAttestation"); type_name_generic!(LightClientBootstrap); type_name_generic!(LightClientBootstrapAltair, "LightClientBootstrap"); type_name_generic!(LightClientBootstrapCapella, "LightClientBootstrap"); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 9e8b375b2c..6068514010 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -232,7 +232,6 @@ mod ssz_static { ssz_static_test!(fork, Fork); ssz_static_test!(fork_data, ForkData); ssz_static_test!(historical_batch, HistoricalBatch<_>); - ssz_static_test!(indexed_attestation, IndexedAttestation<_>); ssz_static_test!(pending_attestation, PendingAttestation<_>); ssz_static_test!(proposer_slashing, ProposerSlashing); ssz_static_test!( From b6913ae542e163f2d688afa65549cd79f1c5d06a Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 21 Jun 2024 12:02:23 +1000 Subject: [PATCH 02/14] Avoid changing slasher schema for Electra --- Cargo.lock | 1 + beacon_node/src/lib.rs | 10 +- consensus/types/src/indexed_attestation.rs | 32 ----- consensus/types/src/lib.rs | 3 +- slasher/Cargo.toml | 1 + slasher/src/database.rs | 137 +++++++++++++++++++-- slasher/src/error.rs | 7 ++ slasher/src/slasher.rs | 7 +- slasher/src/test_utils.rs | 9 +- slasher/tests/attester_slashings.rs | 9 +- slasher/tests/proposer_slashings.rs | 8 +- slasher/tests/random.rs | 6 +- slasher/tests/wrap_around.rs | 8 +- 13 files changed, 178 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 753763fe96..90196ea5b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7653,6 +7653,7 @@ dependencies = [ "safe_arith", "serde", "slog", + "ssz_types", "strum", "tempfile", "tree_hash", diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index ab400d2e73..385da5b4fe 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -80,7 +80,7 @@ impl ProductionBeaconNode { let builder = ClientBuilder::new(context.eth_spec_instance.clone()) .runtime_context(context) - .chain_spec(spec) + .chain_spec(spec.clone()) .beacon_processor(client_config.beacon_processor.clone()) .http_api_config(client_config.http_api.clone()) .disk_store( @@ -113,8 +113,12 @@ impl ProductionBeaconNode { _ => {} } let slasher = Arc::new( - Slasher::open(slasher_config, log.new(slog::o!("service" => "slasher"))) - .map_err(|e| format!("Slasher open error: {:?}", e))?, + Slasher::open( + slasher_config, + Arc::new(spec), + log.new(slog::o!("service" => "slasher")), + ) + .map_err(|e| format!("Slasher open error: {:?}", e))?, ); builder.slasher(slasher) } else { diff --git a/consensus/types/src/indexed_attestation.rs b/consensus/types/src/indexed_attestation.rs index 900c905938..19d1501075 100644 --- a/consensus/types/src/indexed_attestation.rs +++ b/consensus/types/src/indexed_attestation.rs @@ -240,38 +240,6 @@ mod quoted_variable_list_u64 { } } -#[derive(Debug, Clone, Encode, Decode, PartialEq)] -#[ssz(enum_behaviour = "union")] -pub enum IndexedAttestationOnDisk { - Base(IndexedAttestationBase), - Electra(IndexedAttestationElectra), -} - -#[derive(Debug, Clone, Encode, PartialEq)] -#[ssz(enum_behaviour = "union")] -pub enum IndexedAttestationRefOnDisk<'a, E: EthSpec> { - Base(&'a IndexedAttestationBase), - Electra(&'a IndexedAttestationElectra), -} - -impl<'a, E: EthSpec> From<&'a IndexedAttestation> for IndexedAttestationRefOnDisk<'a, E> { - fn from(attestation: &'a IndexedAttestation) -> Self { - match attestation { - IndexedAttestation::Base(attestation) => Self::Base(attestation), - IndexedAttestation::Electra(attestation) => Self::Electra(attestation), - } - } -} - -impl From> for IndexedAttestation { - fn from(attestation: IndexedAttestationOnDisk) -> Self { - match attestation { - IndexedAttestationOnDisk::Base(attestation) => Self::Base(attestation), - IndexedAttestationOnDisk::Electra(attestation) => Self::Electra(attestation), - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 2a73ff0f37..75456446bf 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -176,8 +176,7 @@ pub use crate::fork_versioned_response::{ForkVersionDeserialize, ForkVersionedRe pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; pub use crate::indexed_attestation::{ - IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, - IndexedAttestationOnDisk, IndexedAttestationRef, IndexedAttestationRefOnDisk, + IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, }; pub use crate::light_client_bootstrap::{ LightClientBootstrap, LightClientBootstrapAltair, LightClientBootstrapCapella, diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index ef5cb8249e..563c4599d8 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -29,6 +29,7 @@ tree_hash = { workspace = true } tree_hash_derive = { workspace = true } types = { workspace = true } strum = { workspace = true } +ssz_types = { workspace = true } # MDBX is pinned at the last version with Windows and macOS support. mdbx = { package = "libmdbx", git = "https://github.com/sigp/libmdbx-rs", tag = "v0.1.4", optional = true } diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 16089706a0..2463c77f41 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -13,13 +13,15 @@ use parking_lot::Mutex; use serde::de::DeserializeOwned; use slog::{info, Logger}; use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; use std::borrow::{Borrow, Cow}; use std::marker::PhantomData; use std::sync::Arc; use tree_hash::TreeHash; use types::{ - Epoch, EthSpec, Hash256, IndexedAttestation, IndexedAttestationOnDisk, - IndexedAttestationRefOnDisk, ProposerSlashing, SignedBeaconBlockHeader, Slot, + AggregateSignature, AttestationData, ChainSpec, Epoch, EthSpec, ForkName, Hash256, + IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, ProposerSlashing, + SignedBeaconBlockHeader, Slot, VariableList, }; /// Current database schema version, to check compatibility of on-disk DB with software. @@ -70,6 +72,7 @@ pub struct SlasherDB { /// LRU cache mapping indexed attestation IDs to their attestation data roots. attestation_root_cache: Mutex>, pub(crate) config: Arc, + pub(crate) spec: Arc, _phantom: PhantomData, } @@ -236,6 +239,43 @@ impl AsRef<[u8]> for IndexedAttestationId { } } +/// Indexed attestation that abstracts over Phase0 and Electra variants by using a plain `Vec` for +/// the attesting indices. +/// +/// This allows us to avoid rewriting the entire indexed attestation database at Electra, which +/// saves a lot of execution time. The bytes that it encodes to are the same as the bytes that a +/// regular IndexedAttestation encodes to, because SSZ doesn't care about the length-bound. +#[derive(Debug, PartialEq, Decode, Encode)] +pub struct IndexedAttestationOnDisk { + attesting_indices: Vec, + data: AttestationData, + signature: AggregateSignature, +} + +impl IndexedAttestationOnDisk { + fn into_indexed_attestation( + self, + spec: &ChainSpec, + ) -> Result, Error> { + let fork_at_target_epoch = spec.fork_name_at_epoch(self.data.target.epoch); + if fork_at_target_epoch >= ForkName::Electra { + let attesting_indices = VariableList::new(self.attesting_indices)?; + Ok(IndexedAttestation::Electra(IndexedAttestationElectra { + attesting_indices, + data: self.data, + signature: self.signature, + })) + } else { + let attesting_indices = VariableList::new(self.attesting_indices)?; + Ok(IndexedAttestation::Base(IndexedAttestationBase { + attesting_indices, + data: self.data, + signature: self.signature, + })) + } + } +} + /// Bincode deserialization specialised to `Cow<[u8]>`. fn bincode_deserialize(bytes: Cow<[u8]>) -> Result { Ok(bincode::deserialize(bytes.borrow())?) @@ -246,7 +286,7 @@ fn ssz_decode(bytes: Cow<[u8]>) -> Result { } impl SlasherDB { - pub fn open(config: Arc, log: Logger) -> Result { + pub fn open(config: Arc, spec: Arc, log: Logger) -> Result { info!(log, "Opening slasher database"; "backend" => %config.backend); std::fs::create_dir_all(&config.database_path)?; @@ -269,6 +309,7 @@ impl SlasherDB { databases, attestation_root_cache, config, + spec, _phantom: PhantomData, }; @@ -458,9 +499,8 @@ impl SlasherDB { }; let attestation_key = IndexedAttestationId::new(indexed_att_id); - let indexed_attestation_on_disk: IndexedAttestationRefOnDisk = - indexed_attestation.into(); - let data = indexed_attestation_on_disk.as_ssz_bytes(); + // IndexedAttestationOnDisk and IndexedAttestation have compatible encodings. + let data = indexed_attestation.as_ssz_bytes(); cursor.put(attestation_key.as_ref(), &data)?; drop(cursor); @@ -484,8 +524,8 @@ impl SlasherDB { .ok_or(Error::MissingIndexedAttestation { id: indexed_attestation_id.as_u64(), })?; - let indexed_attestation: IndexedAttestationOnDisk = ssz_decode(bytes)?; - Ok(indexed_attestation.into()) + let indexed_attestation_on_disk: IndexedAttestationOnDisk = ssz_decode(bytes)?; + indexed_attestation_on_disk.into_indexed_attestation(&self.spec) } fn get_attestation_data_root( @@ -775,3 +815,84 @@ impl SlasherDB { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use types::{Checkpoint, MainnetEthSpec}; + + type E = MainnetEthSpec; + + fn indexed_attestation_on_disk_roundtrip_test( + spec: &ChainSpec, + make_attestation: fn( + Vec, + AttestationData, + AggregateSignature, + ) -> IndexedAttestation, + ) { + let attestation_data = AttestationData { + slot: Slot::new(1000), + index: 0, + beacon_block_root: Hash256::repeat_byte(0xaa), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::repeat_byte(0xbb), + }, + target: Checkpoint { + epoch: Epoch::new(31), + root: Hash256::repeat_byte(0xcc), + }, + }; + + let attesting_indices = vec![1, 14, 160, 812737]; + let signature = AggregateSignature::infinity(); + + let fork_attestation = make_attestation( + attesting_indices.clone(), + attestation_data.clone(), + signature.clone(), + ); + + let on_disk = IndexedAttestationOnDisk { + attesting_indices, + data: attestation_data, + signature, + }; + let encoded = on_disk.as_ssz_bytes(); + assert_eq!(encoded, fork_attestation.as_ssz_bytes()); + + let decoded_on_disk = IndexedAttestationOnDisk::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(decoded_on_disk, on_disk); + + let decoded = on_disk.into_indexed_attestation(&spec).unwrap(); + assert_eq!(decoded, fork_attestation); + } + + /// Check that `IndexedAttestationOnDisk` and `IndexedAttestation` have compatible encodings. + #[test] + fn indexed_attestation_on_disk_roundtrip_base() { + let spec = ForkName::Base.make_genesis_spec(E::default_spec()); + let make_attestation = |attesting_indices, data, signature| { + IndexedAttestation::::Base(IndexedAttestationBase { + attesting_indices: VariableList::new(attesting_indices).unwrap(), + data, + signature, + }) + }; + indexed_attestation_on_disk_roundtrip_test(&spec, make_attestation) + } + + #[test] + fn indexed_attestation_on_disk_roundtrip_electra() { + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let make_attestation = |attesting_indices, data, signature| { + IndexedAttestation::::Electra(IndexedAttestationElectra { + attesting_indices: VariableList::new(attesting_indices).unwrap(), + data, + signature, + }) + }; + indexed_attestation_on_disk_roundtrip_test(&spec, make_attestation) + } +} diff --git a/slasher/src/error.rs b/slasher/src/error.rs index b939c281e9..8d3295b22a 100644 --- a/slasher/src/error.rs +++ b/slasher/src/error.rs @@ -13,6 +13,7 @@ pub enum Error { DatabaseIOError(io::Error), DatabasePermissionsError(filesystem::Error), SszDecodeError(ssz::DecodeError), + SszTypesError(ssz_types::Error), BincodeError(bincode::Error), ArithError(safe_arith::ArithError), ChunkIndexOutOfBounds(usize), @@ -100,6 +101,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: ssz_types::Error) -> Self { + Error::SszTypesError(e) + } +} + impl From for Error { fn from(e: bincode::Error) -> Self { Error::BincodeError(e) diff --git a/slasher/src/slasher.rs b/slasher/src/slasher.rs index fc8e8453c8..0bb7c9c3ff 100644 --- a/slasher/src/slasher.rs +++ b/slasher/src/slasher.rs @@ -13,7 +13,8 @@ use slog::{debug, error, info, Logger}; use std::collections::HashSet; use std::sync::Arc; use types::{ - AttesterSlashing, Epoch, EthSpec, IndexedAttestation, ProposerSlashing, SignedBeaconBlockHeader, + AttesterSlashing, ChainSpec, Epoch, EthSpec, IndexedAttestation, ProposerSlashing, + SignedBeaconBlockHeader, }; #[derive(Debug)] @@ -28,10 +29,10 @@ pub struct Slasher { } impl Slasher { - pub fn open(config: Config, log: Logger) -> Result { + pub fn open(config: Config, spec: Arc, log: Logger) -> Result { config.validate()?; let config = Arc::new(config); - let db = SlasherDB::open(config.clone(), log.clone())?; + let db = SlasherDB::open(config.clone(), spec, log.clone())?; let attester_slashings = Mutex::new(HashSet::new()); let proposer_slashings = Mutex::new(HashSet::new()); let attestation_queue = AttestationQueue::default(); diff --git a/slasher/src/test_utils.rs b/slasher/src/test_utils.rs index 7019a8aed7..453d0e6667 100644 --- a/slasher/src/test_utils.rs +++ b/slasher/src/test_utils.rs @@ -1,9 +1,10 @@ use std::collections::HashSet; +use std::sync::Arc; use types::{ indexed_attestation::{IndexedAttestationBase, IndexedAttestationElectra}, AggregateSignature, AttestationData, AttesterSlashing, AttesterSlashingBase, - AttesterSlashingElectra, BeaconBlockHeader, Checkpoint, Epoch, Hash256, IndexedAttestation, - MainnetEthSpec, Signature, SignedBeaconBlockHeader, Slot, + AttesterSlashingElectra, BeaconBlockHeader, ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, + IndexedAttestation, MainnetEthSpec, Signature, SignedBeaconBlockHeader, Slot, }; pub type E = MainnetEthSpec; @@ -145,3 +146,7 @@ pub fn block(slot: u64, proposer_index: u64, block_root: u64) -> SignedBeaconBlo signature: Signature::empty(), } } + +pub fn chain_spec() -> Arc { + Arc::new(E::default_spec()) +} diff --git a/slasher/tests/attester_slashings.rs b/slasher/tests/attester_slashings.rs index b74e491e4b..902141d971 100644 --- a/slasher/tests/attester_slashings.rs +++ b/slasher/tests/attester_slashings.rs @@ -6,7 +6,8 @@ use rayon::prelude::*; use slasher::{ config::DEFAULT_CHUNK_SIZE, test_utils::{ - att_slashing, indexed_att, indexed_att_electra, slashed_validators_from_slashings, E, + att_slashing, chain_spec, indexed_att, indexed_att_electra, + slashed_validators_from_slashings, E, }, Config, Slasher, }; @@ -270,7 +271,8 @@ fn slasher_test( ) { let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); - let slasher = Slasher::open(config, test_logger()).unwrap(); + let spec = chain_spec(); + let slasher = Slasher::open(config, spec, test_logger()).unwrap(); let current_epoch = Epoch::new(current_epoch); for (i, attestation) in attestations.iter().enumerate() { @@ -299,7 +301,8 @@ fn parallel_slasher_test( ) { let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); - let slasher = Slasher::open(config, test_logger()).unwrap(); + let spec = chain_spec(); + let slasher = Slasher::open(config, spec, test_logger()).unwrap(); let current_epoch = Epoch::new(current_epoch); attestations diff --git a/slasher/tests/proposer_slashings.rs b/slasher/tests/proposer_slashings.rs index 3b7b8ed583..2d2738087d 100644 --- a/slasher/tests/proposer_slashings.rs +++ b/slasher/tests/proposer_slashings.rs @@ -2,7 +2,7 @@ use logging::test_logger; use slasher::{ - test_utils::{block as test_block, E}, + test_utils::{block as test_block, chain_spec, E}, Config, Slasher, }; use tempfile::tempdir; @@ -12,7 +12,8 @@ use types::{Epoch, EthSpec}; fn empty_pruning() { let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); - let slasher = Slasher::::open(config, test_logger()).unwrap(); + let spec = chain_spec(); + let slasher = Slasher::::open(config, spec, test_logger()).unwrap(); slasher.prune_database(Epoch::new(0)).unwrap(); } @@ -24,8 +25,9 @@ fn block_pruning() { let mut config = Config::new(tempdir.path().into()); config.chunk_size = 2; config.history_length = 2; + let spec = chain_spec(); - let slasher = Slasher::::open(config.clone(), test_logger()).unwrap(); + let slasher = Slasher::::open(config.clone(), spec, test_logger()).unwrap(); let current_epoch = Epoch::from(2 * config.history_length); // Pruning the empty database should be safe. diff --git a/slasher/tests/random.rs b/slasher/tests/random.rs index ce0e42df1d..ebfe0ef4e9 100644 --- a/slasher/tests/random.rs +++ b/slasher/tests/random.rs @@ -4,7 +4,7 @@ use logging::test_logger; use rand::prelude::*; use slasher::{ test_utils::{ - block, indexed_att, slashed_validators_from_attestations, + block, chain_spec, indexed_att, slashed_validators_from_attestations, slashed_validators_from_slashings, E, }, Config, Slasher, @@ -49,7 +49,9 @@ fn random_test(seed: u64, test_config: TestConfig) { config.chunk_size = 1 << chunk_size_exponent; config.history_length = 1 << rng.gen_range(chunk_size_exponent..chunk_size_exponent + 3); - let slasher = Slasher::::open(config.clone(), test_logger()).unwrap(); + let spec = chain_spec(); + + let slasher = Slasher::::open(config.clone(), spec, test_logger()).unwrap(); let validators = (0..num_validators as u64).collect::>(); diff --git a/slasher/tests/wrap_around.rs b/slasher/tests/wrap_around.rs index d2c876d363..9a42aeb60b 100644 --- a/slasher/tests/wrap_around.rs +++ b/slasher/tests/wrap_around.rs @@ -1,7 +1,10 @@ #![cfg(any(feature = "mdbx", feature = "lmdb"))] use logging::test_logger; -use slasher::{test_utils::indexed_att, Config, Slasher}; +use slasher::{ + test_utils::{chain_spec, indexed_att}, + Config, Slasher, +}; use tempfile::tempdir; use types::Epoch; @@ -9,11 +12,12 @@ use types::Epoch; fn attestation_pruning_empty_wrap_around() { let tempdir = tempdir().unwrap(); let mut config = Config::new(tempdir.path().into()); + let spec = chain_spec(); config.validator_chunk_size = 1; config.chunk_size = 16; config.history_length = 16; - let slasher = Slasher::open(config.clone(), test_logger()).unwrap(); + let slasher = Slasher::open(config.clone(), spec, test_logger()).unwrap(); let v = vec![0]; let history_length = config.history_length as u64; From ebbb17b6bcef883072aa1acb24dc4985a6d0b1ee Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 21 Jun 2024 14:13:22 +1000 Subject: [PATCH 03/14] Delete slasher schema v4 --- slasher/src/database.rs | 2 +- slasher/src/migrate.rs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 2463c77f41..81e9faa5bd 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -25,7 +25,7 @@ use types::{ }; /// Current database schema version, to check compatibility of on-disk DB with software. -pub const CURRENT_SCHEMA_VERSION: u64 = 4; +pub const CURRENT_SCHEMA_VERSION: u64 = 3; /// Metadata about the slashing database itself. const METADATA_DB: &str = "metadata"; diff --git a/slasher/src/migrate.rs b/slasher/src/migrate.rs index ce298caaf2..674ab9c132 100644 --- a/slasher/src/migrate.rs +++ b/slasher/src/migrate.rs @@ -17,10 +17,6 @@ impl SlasherDB { software_schema_version: CURRENT_SCHEMA_VERSION, }), (x, y) if x == y => Ok(self), - (3, 4) => { - // TODO(electra): db migration due to `IndexedAttestationOnDisk` - Ok(self) - } (_, _) => Err(Error::IncompatibleSchemaVersion { database_schema_version: schema_version, software_schema_version: CURRENT_SCHEMA_VERSION, From 13b1b0596058ab37f1c1070d6ce88ab5c981ea62 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 21 Jun 2024 17:03:06 +1000 Subject: [PATCH 04/14] Fix clippy --- slasher/src/database.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 81e9faa5bd..13c5d2b774 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -865,7 +865,7 @@ mod test { let decoded_on_disk = IndexedAttestationOnDisk::from_ssz_bytes(&encoded).unwrap(); assert_eq!(decoded_on_disk, on_disk); - let decoded = on_disk.into_indexed_attestation(&spec).unwrap(); + let decoded = on_disk.into_indexed_attestation(spec).unwrap(); assert_eq!(decoded, fork_attestation); } From 339d1b8229d1b4c36ff9c13b9df64cdeba5254b7 Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 21 Jun 2024 17:36:45 +1000 Subject: [PATCH 05/14] Fix compilation of beacon_chain tests --- beacon_node/beacon_chain/tests/block_verification.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 4c7a499697..d9c9a3b6a7 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -2,7 +2,9 @@ use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock}; use beacon_chain::{ - test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, + test_utils::{ + test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + }, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, ExecutionPendingBlock, }; use beacon_chain::{ @@ -1210,8 +1212,14 @@ async fn block_gossip_verification() { #[tokio::test] async fn verify_block_for_gossip_slashing_detection() { let slasher_dir = tempdir().unwrap(); + let spec = Arc::new(test_spec::()); let slasher = Arc::new( - Slasher::open(SlasherConfig::new(slasher_dir.path().into()), test_logger()).unwrap(), + Slasher::open( + SlasherConfig::new(slasher_dir.path().into()), + spec, + test_logger(), + ) + .unwrap(), ); let inner_slasher = slasher.clone(); From 70a80d5da0441c94047da2f51d48a96312e9c9a6 Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:48:35 +0200 Subject: [PATCH 06/14] Update database.rs --- slasher/src/database.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 13c5d2b774..673e15d3b2 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -819,7 +819,7 @@ impl SlasherDB { #[cfg(test)] mod test { use super::*; - use types::{Checkpoint, MainnetEthSpec}; + use types::{Checkpoint, MainnetEthSpec, Unsigned}; type E = MainnetEthSpec; @@ -830,6 +830,7 @@ mod test { AttestationData, AggregateSignature, ) -> IndexedAttestation, + committee_len: u64, ) { let attestation_data = AttestationData { slot: Slot::new(1000), @@ -845,7 +846,7 @@ mod test { }, }; - let attesting_indices = vec![1, 14, 160, 812737]; + let attesting_indices = (0..committee_len).collect::>(); let signature = AggregateSignature::infinity(); let fork_attestation = make_attestation( @@ -880,7 +881,11 @@ mod test { signature, }) }; - indexed_attestation_on_disk_roundtrip_test(&spec, make_attestation) + indexed_attestation_on_disk_roundtrip_test( + &spec, + make_attestation, + ::MaxValidatorsPerCommittee::to_u64(), + ) } #[test] @@ -893,6 +898,10 @@ mod test { signature, }) }; - indexed_attestation_on_disk_roundtrip_test(&spec, make_attestation) + indexed_attestation_on_disk_roundtrip_test( + &spec, + make_attestation, + ::MaxValidatorsPerSlot::to_u64(), + ) } } From 7509cf6d3b6a704a627bb430ff072f45d81cd01c Mon Sep 17 00:00:00 2001 From: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:55:23 +0200 Subject: [PATCH 07/14] Update per_block_processing.rs --- consensus/state_processing/src/per_block_processing.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/consensus/state_processing/src/per_block_processing.rs b/consensus/state_processing/src/per_block_processing.rs index 05621d188b..e7655b453a 100644 --- a/consensus/state_processing/src/per_block_processing.rs +++ b/consensus/state_processing/src/per_block_processing.rs @@ -626,6 +626,7 @@ pub fn process_withdrawals>( // Update pending partial withdrawals [New in Electra:EIP7251] if let Some(partial_withdrawals_count) = partial_withdrawals_count { + // TODO(electra): Use efficient pop_front after milhouse release https://github.com/sigp/milhouse/pull/38 let new_partial_withdrawals = state .pending_partial_withdrawals()? .iter_from(partial_withdrawals_count)? From 8715589e40d8b39433cf02b7c0c6cd2612e28749 Mon Sep 17 00:00:00 2001 From: dapplion <35266934+dapplion@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:36:02 +0200 Subject: [PATCH 08/14] Add electra lightclient types --- .../types/src/execution_payload_header.rs | 8 +- consensus/types/src/lib.rs | 8 +- consensus/types/src/light_client_bootstrap.rs | 36 ++++--- .../types/src/light_client_finality_update.rs | 99 ++++++++++--------- consensus/types/src/light_client_header.rs | 54 ++++++++-- .../src/light_client_optimistic_update.rs | 37 ++++--- consensus/types/src/light_client_update.rs | 31 +++++- 7 files changed, 181 insertions(+), 92 deletions(-) diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index a3df69652c..b8d0c8c2b0 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -118,12 +118,8 @@ impl ExecutionPayloadHeader { pub fn ssz_max_var_len_for_fork(fork_name: ForkName) -> usize { // Matching here in case variable fields are added in future forks. match fork_name { - ForkName::Base - | ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Base | ForkName::Altair => 0, + ForkName::Bellatrix | ForkName::Capella | ForkName::Deneb | ForkName::Electra => { // Max size of variable length `extra_data` field E::max_extra_data_bytes() * ::ssz_fixed_len() } diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index 2a73ff0f37..1683ae6c97 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -181,22 +181,24 @@ pub use crate::indexed_attestation::{ }; pub use crate::light_client_bootstrap::{ LightClientBootstrap, LightClientBootstrapAltair, LightClientBootstrapCapella, - LightClientBootstrapDeneb, + LightClientBootstrapDeneb, LightClientBootstrapElectra, }; pub use crate::light_client_finality_update::{ LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientFinalityUpdateCapella, - LightClientFinalityUpdateDeneb, + LightClientFinalityUpdateDeneb, LightClientFinalityUpdateElectra, }; pub use crate::light_client_header::{ LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, + LightClientHeaderElectra, }; pub use crate::light_client_optimistic_update::{ LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, LightClientOptimisticUpdateCapella, LightClientOptimisticUpdateDeneb, + LightClientOptimisticUpdateElectra, }; pub use crate::light_client_update::{ Error as LightClientError, LightClientUpdate, LightClientUpdateAltair, - LightClientUpdateCapella, LightClientUpdateDeneb, + LightClientUpdateCapella, LightClientUpdateDeneb, LightClientUpdateElectra, }; pub use crate::participation_flags::ParticipationFlags; pub use crate::participation_list::ParticipationList; diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index 61da0e1b11..e3a85744de 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -1,7 +1,8 @@ use crate::{ light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, EthSpec, FixedVector, ForkName, ForkVersionDeserialize, Hash256, LightClientHeader, LightClientHeaderAltair, - LightClientHeaderCapella, LightClientHeaderDeneb, SignedBeaconBlock, Slot, SyncCommittee, + LightClientHeaderCapella, LightClientHeaderDeneb, LightClientHeaderElectra, SignedBeaconBlock, + Slot, SyncCommittee, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -16,7 +17,7 @@ use tree_hash_derive::TreeHash; /// A LightClientBootstrap is the initializer we send over to light_client nodes /// that are trying to generate their basic storage when booting up. #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -51,6 +52,8 @@ pub struct LightClientBootstrap { pub header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "header_deneb"))] pub header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "header_electra"))] + pub header: LightClientHeaderElectra, /// The `SyncCommittee` used in the requested period. pub current_sync_committee: Arc>, /// Merkle proof for sync committee @@ -66,6 +69,7 @@ impl LightClientBootstrap { Self::Altair(_) => func(ForkName::Altair), Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), + Self::Electra(_) => func(ForkName::Electra), } } @@ -82,9 +86,8 @@ impl LightClientBootstrap { Self::Altair(LightClientBootstrapAltair::from_ssz_bytes(bytes)?) } ForkName::Capella => Self::Capella(LightClientBootstrapCapella::from_ssz_bytes(bytes)?), - ForkName::Deneb | ForkName::Electra => { - Self::Deneb(LightClientBootstrapDeneb::from_ssz_bytes(bytes)?) - } + ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => Self::Electra(LightClientBootstrapElectra::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientBootstrap decoding for {fork_name} not implemented" @@ -97,18 +100,16 @@ impl LightClientBootstrap { #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { - // TODO(electra): review electra changes - match fork_name { + let fixed_len = match fork_name { ForkName::Base => 0, - ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Altair | ForkName::Bellatrix => { as Encode>::ssz_fixed_len() - + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } - } + ForkName::Capella => as Encode>::ssz_fixed_len(), + ForkName::Deneb => as Encode>::ssz_fixed_len(), + ForkName::Electra => as Encode>::ssz_fixed_len(), + }; + fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } pub fn from_beacon_state( @@ -138,11 +139,16 @@ impl LightClientBootstrap { current_sync_committee, current_sync_committee_branch, }), - ForkName::Deneb | ForkName::Electra => Self::Deneb(LightClientBootstrapDeneb { + ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb { header: LightClientHeaderDeneb::block_to_light_client_header(block)?, current_sync_committee, current_sync_committee_branch, }), + ForkName::Electra => Self::Electra(LightClientBootstrapElectra { + header: LightClientHeaderElectra::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), }; Ok(light_client_bootstrap) diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client_finality_update.rs index 29c526e291..a9e24e03db 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client_finality_update.rs @@ -2,7 +2,8 @@ use super::{EthSpec, FixedVector, Hash256, LightClientHeader, Slot, SyncAggregat use crate::ChainSpec; use crate::{ light_client_update::*, test_utils::TestRandom, ForkName, ForkVersionDeserialize, - LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, SignedBeaconBlock, + LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, + LightClientHeaderElectra, SignedBeaconBlock, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -15,7 +16,7 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -50,6 +51,8 @@ pub struct LightClientFinalityUpdate { pub attested_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] pub attested_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] + pub attested_header: LightClientHeaderElectra, /// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch). #[superstruct(only(Altair), partial_getter(rename = "finalized_header_altair"))] pub finalized_header: LightClientHeaderAltair, @@ -57,6 +60,8 @@ pub struct LightClientFinalityUpdate { pub finalized_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "finalized_header_deneb"))] pub finalized_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "finalized_header_electra"))] + pub finalized_header: LightClientHeaderElectra, /// Merkle proof attesting finalized header. #[test_random(default)] pub finality_branch: FixedVector, @@ -80,7 +85,7 @@ impl LightClientFinalityUpdate { .map_err(|_| Error::InconsistentFork)? { ForkName::Altair | ForkName::Bellatrix => { - let finality_update = LightClientFinalityUpdateAltair { + Self::Altair(LightClientFinalityUpdateAltair { attested_header: LightClientHeaderAltair::block_to_light_client_header( attested_block, )?, @@ -90,37 +95,42 @@ impl LightClientFinalityUpdate { finality_branch, sync_aggregate, signature_slot, - }; - Self::Altair(finality_update) - } - ForkName::Capella => { - let finality_update = LightClientFinalityUpdateCapella { - attested_header: LightClientHeaderCapella::block_to_light_client_header( - attested_block, - )?, - finalized_header: LightClientHeaderCapella::block_to_light_client_header( - finalized_block, - )?, - finality_branch, - sync_aggregate, - signature_slot, - }; - Self::Capella(finality_update) - } - ForkName::Deneb | ForkName::Electra => { - let finality_update = LightClientFinalityUpdateDeneb { - attested_header: LightClientHeaderDeneb::block_to_light_client_header( - attested_block, - )?, - finalized_header: LightClientHeaderDeneb::block_to_light_client_header( - finalized_block, - )?, - finality_branch, - sync_aggregate, - signature_slot, - }; - Self::Deneb(finality_update) + }) } + ForkName::Capella => Self::Capella(LightClientFinalityUpdateCapella { + attested_header: LightClientHeaderCapella::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderCapella::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }), + ForkName::Deneb => Self::Deneb(LightClientFinalityUpdateDeneb { + attested_header: LightClientHeaderDeneb::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderDeneb::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }), + ForkName::Electra => Self::Electra(LightClientFinalityUpdateElectra { + attested_header: LightClientHeaderElectra::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderElectra::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }), + ForkName::Base => return Err(Error::AltairForkNotActive), }; @@ -135,6 +145,7 @@ impl LightClientFinalityUpdate { Self::Altair(_) => func(ForkName::Altair), Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), + Self::Electra(_) => func(ForkName::Electra), } } @@ -153,8 +164,9 @@ impl LightClientFinalityUpdate { ForkName::Capella => { Self::Capella(LightClientFinalityUpdateCapella::from_ssz_bytes(bytes)?) } - ForkName::Deneb | ForkName::Electra => { - Self::Deneb(LightClientFinalityUpdateDeneb::from_ssz_bytes(bytes)?) + ForkName::Deneb => Self::Deneb(LightClientFinalityUpdateDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => { + Self::Electra(LightClientFinalityUpdateElectra::from_ssz_bytes(bytes)?) } ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( @@ -168,18 +180,17 @@ impl LightClientFinalityUpdate { #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { - // TODO(electra): review electra changes - match fork_name { + let fixed_size = match fork_name { ForkName::Base => 0, - ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Altair | ForkName::Bellatrix => { as Encode>::ssz_fixed_len() - + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } - } + ForkName::Capella => as Encode>::ssz_fixed_len(), + ForkName::Deneb => as Encode>::ssz_fixed_len(), + ForkName::Electra => as Encode>::ssz_fixed_len(), + }; + // `2 *` because there are two headers in the update + fixed_size + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } } diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client_header.rs index 213ec90f95..1d6432ed6f 100644 --- a/consensus/types/src/light_client_header.rs +++ b/consensus/types/src/light_client_header.rs @@ -4,7 +4,7 @@ use crate::ForkVersionDeserialize; use crate::{light_client_update::*, BeaconBlockBody}; use crate::{ test_utils::TestRandom, EthSpec, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, - FixedVector, Hash256, SignedBeaconBlock, + ExecutionPayloadHeaderElectra, FixedVector, Hash256, SignedBeaconBlock, }; use crate::{BeaconBlockHeader, ExecutionPayloadHeader}; use derivative::Derivative; @@ -17,7 +17,7 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -54,8 +54,13 @@ pub struct LightClientHeader { pub execution: ExecutionPayloadHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_header_deneb"))] pub execution: ExecutionPayloadHeaderDeneb, + #[superstruct( + only(Electra), + partial_getter(rename = "execution_payload_header_electra") + )] + pub execution: ExecutionPayloadHeaderElectra, - #[superstruct(only(Capella, Deneb))] + #[superstruct(only(Capella, Deneb, Electra))] pub execution_branch: FixedVector, #[ssz(skip_serializing, skip_deserializing)] @@ -81,9 +86,12 @@ impl LightClientHeader { ForkName::Capella => LightClientHeader::Capella( LightClientHeaderCapella::block_to_light_client_header(block)?, ), - ForkName::Deneb | ForkName::Electra => LightClientHeader::Deneb( + ForkName::Deneb => LightClientHeader::Deneb( LightClientHeaderDeneb::block_to_light_client_header(block)?, ), + ForkName::Electra => LightClientHeader::Electra( + LightClientHeaderElectra::block_to_light_client_header(block)?, + ), }; Ok(header) } @@ -96,9 +104,12 @@ impl LightClientHeader { ForkName::Capella => { LightClientHeader::Capella(LightClientHeaderCapella::from_ssz_bytes(bytes)?) } - ForkName::Deneb | ForkName::Electra => { + ForkName::Deneb => { LightClientHeader::Deneb(LightClientHeaderDeneb::from_ssz_bytes(bytes)?) } + ForkName::Electra => { + LightClientHeader::Electra(LightClientHeaderElectra::from_ssz_bytes(bytes)?) + } ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientHeader decoding for {fork_name} not implemented" @@ -192,6 +203,34 @@ impl LightClientHeaderDeneb { } } +impl LightClientHeaderElectra { + pub fn block_to_light_client_header(block: &SignedBeaconBlock) -> Result { + let payload = block + .message() + .execution_payload()? + .execution_payload_electra()?; + + let header = ExecutionPayloadHeaderElectra::from(payload); + let beacon_block_body = BeaconBlockBody::from( + block + .message() + .body_electra() + .map_err(|_| Error::BeaconBlockBodyError)? + .to_owned(), + ); + + let execution_branch = + beacon_block_body.block_body_merkle_proof(EXECUTION_PAYLOAD_INDEX)?; + + Ok(LightClientHeaderElectra { + beacon: block.message().block_header(), + execution: header, + execution_branch: FixedVector::new(execution_branch)?, + _phantom_data: PhantomData, + }) + } +} + impl ForkVersionDeserialize for LightClientHeader { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, @@ -204,9 +243,12 @@ impl ForkVersionDeserialize for LightClientHeader { ForkName::Capella => serde_json::from_value(value) .map(|light_client_header| Self::Capella(light_client_header)) .map_err(serde::de::Error::custom), - ForkName::Deneb | ForkName::Electra => serde_json::from_value(value) + ForkName::Deneb => serde_json::from_value(value) .map(|light_client_header| Self::Deneb(light_client_header)) .map_err(serde::de::Error::custom), + ForkName::Electra => serde_json::from_value(value) + .map(|light_client_header| Self::Electra(light_client_header)) + .map_err(serde::de::Error::custom), ForkName::Base => Err(serde::de::Error::custom(format!( "LightClientHeader deserialization for {fork_name} not implemented" ))), diff --git a/consensus/types/src/light_client_optimistic_update.rs b/consensus/types/src/light_client_optimistic_update.rs index 4727673f6c..708f24e770 100644 --- a/consensus/types/src/light_client_optimistic_update.rs +++ b/consensus/types/src/light_client_optimistic_update.rs @@ -2,7 +2,7 @@ use super::{EthSpec, ForkName, ForkVersionDeserialize, LightClientHeader, Slot, use crate::test_utils::TestRandom; use crate::{ light_client_update::*, ChainSpec, LightClientHeaderAltair, LightClientHeaderCapella, - LightClientHeaderDeneb, SignedBeaconBlock, + LightClientHeaderDeneb, LightClientHeaderElectra, SignedBeaconBlock, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -18,7 +18,7 @@ use tree_hash_derive::TreeHash; /// A LightClientOptimisticUpdate is the update we send on each slot, /// it is based off the current unfinalized epoch is verified only against BLS signature. #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -53,6 +53,8 @@ pub struct LightClientOptimisticUpdate { pub attested_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] pub attested_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] + pub attested_header: LightClientHeaderElectra, /// current sync aggregate pub sync_aggregate: SyncAggregate, /// Slot of the sync aggregated signature @@ -86,13 +88,20 @@ impl LightClientOptimisticUpdate { sync_aggregate, signature_slot, }), - ForkName::Deneb | ForkName::Electra => Self::Deneb(LightClientOptimisticUpdateDeneb { + ForkName::Deneb => Self::Deneb(LightClientOptimisticUpdateDeneb { attested_header: LightClientHeaderDeneb::block_to_light_client_header( attested_block, )?, sync_aggregate, signature_slot, }), + ForkName::Electra => Self::Electra(LightClientOptimisticUpdateElectra { + attested_header: LightClientHeaderElectra::block_to_light_client_header( + attested_block, + )?, + sync_aggregate, + signature_slot, + }), ForkName::Base => return Err(Error::AltairForkNotActive), }; @@ -107,6 +116,7 @@ impl LightClientOptimisticUpdate { Self::Altair(_) => func(ForkName::Altair), Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), + Self::Electra(_) => func(ForkName::Electra), } } @@ -139,9 +149,12 @@ impl LightClientOptimisticUpdate { ForkName::Capella => { Self::Capella(LightClientOptimisticUpdateCapella::from_ssz_bytes(bytes)?) } - ForkName::Deneb | ForkName::Electra => { + ForkName::Deneb => { Self::Deneb(LightClientOptimisticUpdateDeneb::from_ssz_bytes(bytes)?) } + ForkName::Electra => { + Self::Electra(LightClientOptimisticUpdateElectra::from_ssz_bytes(bytes)?) + } ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientOptimisticUpdate decoding for {fork_name} not implemented" @@ -154,18 +167,16 @@ impl LightClientOptimisticUpdate { #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { - // TODO(electra): review electra changes - match fork_name { + let fixed_len = match fork_name { ForkName::Base => 0, - ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Altair | ForkName::Bellatrix => { as Encode>::ssz_fixed_len() - + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } - } + ForkName::Capella => as Encode>::ssz_fixed_len(), + ForkName::Deneb => as Encode>::ssz_fixed_len(), + ForkName::Electra => as Encode>::ssz_fixed_len(), + }; + fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } } diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index 002fbea2d3..76ad568988 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -1,4 +1,5 @@ use super::{EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee}; +use crate::light_client_header::LightClientHeaderElectra; use crate::{ beacon_state, test_utils::TestRandom, BeaconBlock, BeaconBlockHeader, BeaconState, ChainSpec, ForkName, ForkVersionDeserialize, LightClientHeaderAltair, LightClientHeaderCapella, @@ -76,7 +77,7 @@ impl From for Error { /// or to sync up to the last committee period, we need to have one ready for each ALTAIR period /// we go over, note: there is no need to keep all of the updates from [ALTAIR_PERIOD, CURRENT_PERIOD]. #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -111,6 +112,8 @@ pub struct LightClientUpdate { pub attested_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] pub attested_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] + pub attested_header: LightClientHeaderElectra, /// The `SyncCommittee` used in the next period. pub next_sync_committee: Arc>, /// Merkle proof for next sync committee @@ -122,6 +125,8 @@ pub struct LightClientUpdate { pub finalized_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "finalized_header_deneb"))] pub finalized_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "finalized_header_electra"))] + pub finalized_header: LightClientHeaderElectra, /// Merkle proof attesting finalized header. pub finality_branch: FixedVector, /// current sync aggreggate @@ -221,7 +226,7 @@ impl LightClientUpdate { signature_slot: block.slot(), }) } - ForkName::Deneb | ForkName::Electra => { + ForkName::Deneb => { let attested_header = LightClientHeaderDeneb::block_to_light_client_header(attested_block)?; let finalized_header = @@ -236,6 +241,23 @@ impl LightClientUpdate { signature_slot: block.slot(), }) } + ForkName::Electra => { + let attested_header = + LightClientHeaderElectra::block_to_light_client_header(attested_block)?; + let finalized_header = + LightClientHeaderElectra::block_to_light_client_header(finalized_block)?; + Self::Electra(LightClientUpdateElectra { + attested_header, + next_sync_committee: attested_state.next_sync_committee()?.clone(), + next_sync_committee_branch: FixedVector::new(next_sync_committee_branch)?, + finalized_header, + finality_branch: FixedVector::new(finality_branch)?, + sync_aggregate: sync_aggregate.clone(), + signature_slot: block.slot(), + }) + } // To add a new fork, just append the new fork variant on the latest fork. Forks that + // have a distinct execution header will need a new LightClientUdpate variant only + // if you need to test or support lightclient usages }; Ok(light_client_update) @@ -247,9 +269,8 @@ impl LightClientUpdate { Self::Altair(LightClientUpdateAltair::from_ssz_bytes(bytes)?) } ForkName::Capella => Self::Capella(LightClientUpdateCapella::from_ssz_bytes(bytes)?), - ForkName::Deneb | ForkName::Electra => { - Self::Deneb(LightClientUpdateDeneb::from_ssz_bytes(bytes)?) - } + ForkName::Deneb => Self::Deneb(LightClientUpdateDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => Self::Electra(LightClientUpdateElectra::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientUpdate decoding for {fork_name} not implemented" From 09141ec51ad32cbd823b7406b5430d76acddc2fd Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 21 Jun 2024 10:48:43 -0400 Subject: [PATCH 09/14] Update slasher/src/database.rs --- slasher/src/database.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 673e15d3b2..a81a3b0580 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -258,7 +258,7 @@ impl IndexedAttestationOnDisk { spec: &ChainSpec, ) -> Result, Error> { let fork_at_target_epoch = spec.fork_name_at_epoch(self.data.target.epoch); - if fork_at_target_epoch >= ForkName::Electra { + if fork_at_target_epoch.electra_enabled() { let attesting_indices = VariableList::new(self.attesting_indices)?; Ok(IndexedAttestation::Electra(IndexedAttestationElectra { attesting_indices, From 8fc533368c6f94e3ff56df795f7db18444215a69 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 21 Jun 2024 10:57:35 -0400 Subject: [PATCH 10/14] fix imports --- slasher/src/database.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/slasher/src/database.rs b/slasher/src/database.rs index a81a3b0580..801abe9283 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -19,9 +19,9 @@ use std::marker::PhantomData; use std::sync::Arc; use tree_hash::TreeHash; use types::{ - AggregateSignature, AttestationData, ChainSpec, Epoch, EthSpec, ForkName, Hash256, - IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, ProposerSlashing, - SignedBeaconBlockHeader, Slot, VariableList, + AggregateSignature, AttestationData, ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, + IndexedAttestationBase, IndexedAttestationElectra, ProposerSlashing, SignedBeaconBlockHeader, + Slot, VariableList, }; /// Current database schema version, to check compatibility of on-disk DB with software. @@ -819,7 +819,7 @@ impl SlasherDB { #[cfg(test)] mod test { use super::*; - use types::{Checkpoint, MainnetEthSpec, Unsigned}; + use types::{Checkpoint, ForkName, MainnetEthSpec, Unsigned}; type E = MainnetEthSpec; From 68fd7a7881d84d8ac536d8542b8be55a65e9fb8d Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 21 Jun 2024 13:09:56 -0400 Subject: [PATCH 11/14] Update beacon_node/beacon_chain/src/attestation_verification.rs --- beacon_node/beacon_chain/src/attestation_verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index c7f7bb58da..45db6f23a3 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -1396,7 +1396,7 @@ pub fn obtain_indexed_attestation_and_committees_per_slot( /// `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 isn't found in the `shuffling_cache`, we will read a state +/// 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 From d137881614d94324194a26b718623714eef66cb9 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Fri, 21 Jun 2024 13:13:07 -0400 Subject: [PATCH 12/14] Update beacon_node/beacon_chain/src/attestation_verification.rs --- beacon_node/beacon_chain/src/attestation_verification.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index 45db6f23a3..06fba937d8 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -1392,7 +1392,7 @@ pub fn obtain_indexed_attestation_and_committees_per_slot( /// 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 attestations slot can be complex. It might involve reading straight from the +/// 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. /// From 758b58c9e97931b34ee8046cfea64780659ef788 Mon Sep 17 00:00:00 2001 From: chonghe <44791194+chong-he@users.noreply.github.com> Date: Mon, 24 Jun 2024 07:52:15 +0800 Subject: [PATCH 13/14] Update slasher DB size and Lighthouse book (#5934) * Update book * Fix * mdlint * Revise * Update slasher doc * Revise max db size * change blob to file * Add checkpoint-blobs * Thanks Jimmy for the command * Update schema docs --- book/src/checkpoint-sync.md | 15 ++++++++++-- book/src/database-migrations.md | 1 + book/src/faq.md | 42 ++++++++++++++++++++++++++------- book/src/slasher.md | 4 ++-- book/src/slashing-protection.md | 4 ++-- slasher/src/config.rs | 2 +- 6 files changed, 52 insertions(+), 16 deletions(-) diff --git a/book/src/checkpoint-sync.md b/book/src/checkpoint-sync.md index 63d96874c3..2bf028acfe 100644 --- a/book/src/checkpoint-sync.md +++ b/book/src/checkpoint-sync.md @@ -146,8 +146,19 @@ For more information on historic state storage see the To manually specify a checkpoint use the following two flags: -* `--checkpoint-state`: accepts an SSZ-encoded `BeaconState` blob -* `--checkpoint-block`: accepts an SSZ-encoded `SignedBeaconBlock` blob +* `--checkpoint-state`: accepts an SSZ-encoded `BeaconState` file +* `--checkpoint-block`: accepts an SSZ-encoded `SignedBeaconBlock` file +* `--checkpoint-blobs`: accepts an SSZ-encoded `Blobs` file + +The command is as following: + +```bash +curl -H "Accept: application/octet-stream" "http://localhost:5052/eth/v2/debug/beacon/states/$SLOT" > state.ssz +curl -H "Accept: application/octet-stream" "http://localhost:5052/eth/v2/beacon/blocks/$SLOT" > block.ssz +curl -H "Accept: application/octet-stream" "http://localhost:5052/eth/v1/beacon/blob_sidecars/$SLOT" > blobs.ssz +``` + +where `$SLOT` is the slot number. It can be specified as `head` or `finalized` as well. _Both_ the state and block must be provided and the state **must** match the block. The state may be from the same slot as the block (unadvanced), or advanced to an epoch boundary, diff --git a/book/src/database-migrations.md b/book/src/database-migrations.md index a81acd7794..611c61cb9c 100644 --- a/book/src/database-migrations.md +++ b/book/src/database-migrations.md @@ -16,6 +16,7 @@ validator client or the slasher**. | Lighthouse version | Release date | Schema version | Downgrade available? | |--------------------|--------------|----------------|----------------------| +| v5.2.0 | Jun 2024 | v19 | yes before Deneb | | v5.1.0 | Mar 2024 | v19 | yes before Deneb | | v5.0.0 | Feb 2024 | v19 | yes before Deneb | | v4.6.0 | Dec 2023 | v19 | yes before Deneb | diff --git a/book/src/faq.md b/book/src/faq.md index c7fdb6b32f..2de7841343 100644 --- a/book/src/faq.md +++ b/book/src/faq.md @@ -15,6 +15,7 @@ - [My beacon node logs `WARN Error signalling fork choice waiter`, what should I do?](#bn-fork-choice) - [My beacon node logs `ERRO Aggregate attestation queue full`, what should I do?](#bn-queue-full) - [My beacon node logs `WARN Failed to finalize deposit cache`, what should I do?](#bn-deposit-cache) +- [My beacon node logs `WARN Could not verify blob sidecar for gossip`, what does it mean?](#bn-blob) ## [Validator](#validator-1) @@ -214,6 +215,16 @@ This suggests that the computer resources are being overwhelmed. It could be due This is a known [bug](https://github.com/sigp/lighthouse/issues/3707) that will fix by itself. +### My beacon node logs `WARN Could not verify blob sidecar for gossip`, what does it mean? + +An example of the full log is shown below: + +```text +Jun 07 23:05:12.170 WARN Could not verify blob sidecar for gossip. Ignoring the blob sidecar, commitment: 0xaa97…6f54, index: 1, root: 0x93b8…c47c, slot: 9248017, error: PastFinalizedSlot { blob_slot: Slot(9248017), finalized_slot: Slot(9248032) }, module: network::network_beacon_processor::gossip_methods:720 +``` + +The `PastFinalizedSlot` indicates that the time at which the node received the blob has past the finalization period. This could be due to a peer sending an earlier blob. The log will be gone when Lighthouse eventually drops the peer. + ## Validator ### Why does it take so long for a validator to be activated? @@ -327,13 +338,24 @@ The first thing is to ensure both consensus and execution clients are synced wit You can see more information on the [Ethstaker KB](https://ethstaker.gitbook.io/ethstaker-knowledge-base/help/missed-attestations). -Another cause for missing attestations is delays during block processing. When this happens, the debug logs will show (debug logs can be found under `$datadir/beacon/logs`): +Another cause for missing attestations is the block arriving late, or there are delays during block processing. + +An example of the log: (debug logs can be found under `$datadir/beacon/logs`): ```text -DEBG Delayed head block set_as_head_delay: Some(93.579425ms), imported_delay: Some(1.460405278s), observed_delay: Some(2.540811921s), block_delay: 4.094796624s, slot: 6837344, proposer_index: 211108, block_root: 0x2c52231c0a5a117401f5231585de8aa5dd963bc7cbc00c544e681342eedd1700, service: beacon +Delayed head block, set_as_head_time_ms: 27, imported_time_ms: 168, attestable_delay_ms: 4209, available_delay_ms: 4186, execution_time_ms: 201, blob_delay_ms: 3815, observed_delay_ms: 3984, total_delay_ms: 4381, slot: 1886014, proposer_index: 733, block_root: 0xa7390baac88d50f1cbb5ad81691915f6402385a12521a670bbbd4cd5f8bf3934, service: beacon, module: beacon_chain::canonical_head:1441 ``` -The fields to look for are `imported_delay > 1s` and `observed_delay < 3s`. The `imported_delay` is how long the node took to process the block. The `imported_delay` of larger than 1 second suggests that there is slowness in processing the block. It could be due to high CPU usage, high I/O disk usage or the clients are doing some background maintenance processes. The `observed_delay` is determined mostly by the proposer and partly by your networking setup (e.g., how long it took for the node to receive the block). The `observed_delay` of less than 3 seconds means that the block is not arriving late from the block proposer. Combining the above, this implies that the validator should have been able to attest to the block, but failed due to slowness in the node processing the block. +The field to look for is `attestable_delay`, which defines the time when a block is ready for the validator to attest. If the `attestable_delay` is greater than 4s which has past the window of attestation, the attestation wil fail. In the above example, the delay is mostly caused by late block observed by the node, as shown in `observed_delay`. The `observed_delay` is determined mostly by the proposer and partly by your networking setup (e.g., how long it took for the node to receive the block). Ideally, `observed_delay` should be less than 3 seconds. In this example, the validator failed to attest the block due to the block arriving late. + +Another example of log: + +``` +DEBG Delayed head block, set_as_head_time_ms: 22, imported_time_ms: 312, attestable_delay_ms: 7052, available_delay_ms: 6874, execution_time_ms: 4694, blob_delay_ms: 2159, observed_delay_ms: 2179, total_delay_ms: 7209, slot: 1885922, proposer_index: 606896, block_root: 0x9966df24d24e722d7133068186f0caa098428696e9f441ac416d0aca70cc0a23, service: beacon, module: beacon_chain::canonical_head:1441 +/159.69.68.247/tcp/9000, service: libp2p, module: lighthouse_network::service:1811 +``` + +In this example, we see that the `execution_time_ms` is 4694ms. The `execution_time_ms` is how long the node took to process the block. The `execution_time_ms` of larger than 1 second suggests that there is slowness in processing the block. If the `execution_time_ms` is high, it could be due to high CPU usage, high I/O disk usage or the clients are doing some background maintenance processes. ### Sometimes I miss the attestation head vote, resulting in penalty. Is this normal? @@ -514,21 +536,23 @@ If you would still like to subscribe to all subnets, you can use the flag `subsc ### How to know how many of my peers are connected via QUIC? -With `--metrics` enabled in the beacon node, you can find the number of peers connected via QUIC using: +With `--metrics` enabled in the beacon node, the [Grafana Network dashboard](https://github.com/sigp/lighthouse-metrics/blob/master/dashboards/Network.json) displays the connected by transport, which will show the number of peers connected via QUIC. + +Alternatively, you can find the number of peers connected via QUIC manually using: ```bash - curl -s "http://localhost:5054/metrics" | grep libp2p_quic_peers + curl -s "http://localhost:5054/metrics" | grep 'transport="quic"' ``` A response example is: ```text -# HELP libp2p_quic_peers Count of libp2p peers currently connected via QUIC -# TYPE libp2p_quic_peers gauge -libp2p_quic_peers 4 +libp2p_peers_multi{direction="inbound",transport="quic"} 27 +libp2p_peers_multi{direction="none",transport="quic"} 0 +libp2p_peers_multi{direction="outbound",transport="quic"} 9 ``` -which shows that there are 4 peers connected via QUIC. +which shows that there are a total of 36 peers connected via QUIC. ## Miscellaneous diff --git a/book/src/slasher.md b/book/src/slasher.md index 5098fe6eda..3310f6c9ef 100644 --- a/book/src/slasher.md +++ b/book/src/slasher.md @@ -114,13 +114,13 @@ changed after initialization. * Flag: `--slasher-max-db-size GIGABYTES` * Argument: maximum size of the database in gigabytes -* Default: 256 GB +* Default: 512 GB Both database backends LMDB and MDBX place a hard limit on the size of the database file. You can use the `--slasher-max-db-size` flag to set this limit. It can be adjusted after initialization if the limit is reached. -By default the limit is set to accommodate the default history length and around 600K validators (with about 30% headroom) but +By default the limit is set to accommodate the default history length and around 1 million validators but you can set it lower if running with a reduced history length. The space required scales approximately linearly in validator count and history length, i.e. if you halve either you can halve the space required. diff --git a/book/src/slashing-protection.md b/book/src/slashing-protection.md index 88e2bb955c..2d580f1c31 100644 --- a/book/src/slashing-protection.md +++ b/book/src/slashing-protection.md @@ -75,7 +75,7 @@ Once you have the slashing protection database from your existing client, you ca using this command: ```bash -lighthouse account validator slashing-protection import +lighthouse account validator slashing-protection import filename.json ``` When importing an interchange file, you still need to import the validator keystores themselves @@ -86,7 +86,7 @@ separately, using the instructions for [import validator keys](./mainnet-validat You can export Lighthouse's database for use with another client with this command: ``` -lighthouse account validator slashing-protection export +lighthouse account validator slashing-protection export filename.json ``` The validator client needs to be stopped in order to export, to guarantee that the data exported is diff --git a/slasher/src/config.rs b/slasher/src/config.rs index 4fd74343e7..54161f6ce9 100644 --- a/slasher/src/config.rs +++ b/slasher/src/config.rs @@ -11,7 +11,7 @@ pub const DEFAULT_VALIDATOR_CHUNK_SIZE: usize = 256; pub const DEFAULT_HISTORY_LENGTH: usize = 4096; pub const DEFAULT_UPDATE_PERIOD: u64 = 12; pub const DEFAULT_SLOT_OFFSET: f64 = 10.5; -pub const DEFAULT_MAX_DB_SIZE: usize = 256 * 1024; // 256 GiB +pub const DEFAULT_MAX_DB_SIZE: usize = 512 * 1024; // 512 GiB pub const DEFAULT_ATTESTATION_ROOT_CACHE_SIZE: NonZeroUsize = new_non_zero_usize(100_000); pub const DEFAULT_BROADCAST: bool = false; From c52c598f6922059900d60723bb541add6865a87e Mon Sep 17 00:00:00 2001 From: ethDreamer <37123614+ethDreamer@users.noreply.github.com> Date: Mon, 24 Jun 2024 16:08:07 -0500 Subject: [PATCH 14/14] Electra: Remaining Consensus Data Structures (#5712) * Attestation superstruct changes for EIP 7549 (#5644) * update * experiment * superstruct changes * revert * superstruct changes * fix tests * indexed attestation * indexed attestation superstruct * updated TODOs * `superstruct` the `AttesterSlashing` (#5636) * `superstruct` Attester Fork Variants * Push a little further * Deal with Encode / Decode of AttesterSlashing * not so sure about this.. * Stop Encode/Decode Bounds from Propagating Out * Tons of Changes.. * More Conversions to AttestationRef * Add AsReference trait (#15) * Add AsReference trait * Fix some snafus * Got it Compiling! :D * Got Tests Building * Get beacon chain tests compiling --------- Co-authored-by: Michael Sproul * Merge remote-tracking branch 'upstream/unstable' into electra_attestation_changes * Make EF Tests Fork-Agnostic (#5713) * Finish EF Test Fork Agnostic (#5714) * Superstruct `AggregateAndProof` (#5715) * Upgrade `superstruct` to `0.8.0` * superstruct `AggregateAndProof` * Merge remote-tracking branch 'sigp/unstable' into electra_attestation_changes * cargo fmt * Merge pull request #5726 from realbigsean/electra_attestation_changes Merge unstable into Electra attestation changes * EIP7549 `get_attestation_indices` (#5657) * get attesting indices electra impl * fmt * get tests to pass * fmt * fix some beacon chain tests * fmt * fix slasher test * fmt got me again * fix more tests * fix tests * Some small changes (#5739) * cargo fmt (#5740) * Sketch op pool changes * fix get attesting indices (#5742) * fix get attesting indices * better errors * fix compile * only get committee index once * Ef test fixes (#5753) * attestation related ef test fixes * delete commented out stuff * Fix Aggregation Pool for Electra (#5754) * Fix Aggregation Pool for Electra * Remove Outdated Interface * fix ssz (#5755) * Get `electra_op_pool` up to date (#5756) * fix get attesting indices (#5742) * fix get attesting indices * better errors * fix compile * only get committee index once * Ef test fixes (#5753) * attestation related ef test fixes * delete commented out stuff * Fix Aggregation Pool for Electra (#5754) * Fix Aggregation Pool for Electra * Remove Outdated Interface * fix ssz (#5755) --------- Co-authored-by: realbigsean * Revert "Get `electra_op_pool` up to date (#5756)" (#5757) This reverts commit ab9e58aa3d0e6fe2175a4996a5de710e81152896. * Merge branch 'electra_attestation_changes' of https://github.com/sigp/lighthouse into electra_op_pool * Compute on chain aggregate impl (#5752) * add compute_on_chain_agg impl to op pool changes * fmt * get op pool tests to pass * update the naive agg pool interface (#5760) * Fix bugs in cross-committee aggregation * Add comment to max cover optimisation * Fix assert * Merge pull request #5749 from sigp/electra_op_pool Optimise Electra op pool aggregation * update committee offset * Fix Electra Fork Choice Tests (#5764) * Subscribe to the correct subnets for electra attestations (#5782) * subscribe to the correct att subnets for electra * subscribe to the correct att subnets for electra * cargo fmt * fix slashing handling * Merge remote-tracking branch 'upstream/unstable' * Send unagg attestation based on fork * Publish all aggregates * just one more check bro plz.. * Merge pull request #5832 from ethDreamer/electra_attestation_changes_merge_unstable Merge `unstable` into `electra_attestation_changes` * Merge pull request #5835 from realbigsean/fix-validator-logic Fix validator logic * Merge pull request #5816 from realbigsean/electra-attestation-slashing-handling Electra slashing handling * Electra attestation changes rm decode impl (#5856) * Remove Crappy Decode impl for Attestation * Remove Inefficient Attestation Decode impl * Implement Schema Upgrade / Downgrade * Update beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs Co-authored-by: Michael Sproul --------- Co-authored-by: Michael Sproul * Fix failing attestation tests and misc electra attestation cleanup (#5810) * - get attestation related beacon chain tests to pass - observed attestations are now keyed off of data + committee index - rename op pool attestationref to compactattestationref - remove unwraps in agg pool and use options instead - cherry pick some changes from ef-tests-electra * cargo fmt * fix failing test * Revert dockerfile changes * make committee_index return option * function args shouldnt be a ref to attestation ref * fmt * fix dup imports --------- Co-authored-by: realbigsean * fix some todos (#5817) * Merge branch 'unstable' of https://github.com/sigp/lighthouse into electra_attestation_changes * add consolidations to merkle calc for inclusion proof * Remove Duplicate KZG Commitment Merkle Proof Code (#5874) * Remove Duplicate KZG Commitment Merkle Proof Code * s/tree_lists/fields/ * Merge branch 'unstable' of https://github.com/sigp/lighthouse into electra_attestation_changes * fix compile * Fix slasher tests (#5906) * Fix electra tests * Add electra attestations to double vote tests * Update superstruct to 0.8 * Merge remote-tracking branch 'origin/unstable' into electra_attestation_changes * Small cleanup in slasher tests * Clean up Electra observed aggregates (#5929) * Use consistent key in observed_attestations * Remove unwraps from observed aggregates * Merge branch 'unstable' of https://github.com/sigp/lighthouse into electra_attestation_changes * De-dup attestation constructor logic * Remove unwraps in Attestation construction * Dedup match_attestation_data * Remove outdated TODO * Use ForkName Ord in fork-choice tests * Use ForkName Ord in BeaconBlockBody * Make to_electra not fallible * Remove TestRandom impl for IndexedAttestation * Remove IndexedAttestation faulty Decode impl * Drop TestRandom impl * Add PendingAttestationInElectra * Indexed att on disk (#35) * indexed att on disk * fix lints * Update slasher/src/migrate.rs Co-authored-by: ethDreamer <37123614+ethDreamer@users.noreply.github.com> --------- Co-authored-by: Lion - dapplion <35266934+dapplion@users.noreply.github.com> Co-authored-by: ethDreamer <37123614+ethDreamer@users.noreply.github.com> * add electra fork enabled fn to ForkName impl (#36) * add electra fork enabled fn to ForkName impl * remove inadvertent file * Update common/eth2/src/types.rs Co-authored-by: ethDreamer <37123614+ethDreamer@users.noreply.github.com> * Dedup attestation constructor logic in attester cache * Use if let Ok for committee_bits * Dedup Attestation constructor code * Diff reduction in tests * Fix beacon_chain tests * Diff reduction * Use Ord for ForkName in pubsub * Resolve into_attestation_and_indices todo * Remove stale TODO * Fix beacon_chain tests * Test spec invariant * Use electra_enabled in pubsub * Remove get_indexed_attestation_from_signed_aggregate * Use ok_or instead of if let else * committees are sorted * remove dup method `get_indexed_attestation_from_committees` * Merge pull request #5940 from dapplion/electra_attestation_changes_lionreview Electra attestations #5712 review * update default persisted op pool deserialization * ensure aggregate and proof uses serde untagged on ref * Fork aware ssz static attestation tests * Electra attestation changes from Lions review (#5971) * dedup/cleanup and remove unneeded hashset use * remove irrelevant TODOs * Merge branch 'unstable' of https://github.com/sigp/lighthouse into electra_attestation_changes * Electra attestation changes sean review (#5972) * instantiate empty bitlist in unreachable code * clean up error conversion * fork enabled bool cleanup * remove a couple todos * return bools instead of options in `aggregate` and use the result * delete commented out code * use map macros in simple transformations * remove signers_disjoint_from * get ef tests compiling * get ef tests compiling * update intentionally excluded files * Avoid changing slasher schema for Electra * Delete slasher schema v4 * Fix clippy * Fix compilation of beacon_chain tests * Update database.rs * Add electra lightclient types * Update slasher/src/database.rs * fix imports * Merge pull request #5980 from dapplion/electra-lightclient Add electra lightclient types * Merge pull request #5975 from michaelsproul/electra-slasher-no-migration Avoid changing slasher schema for Electra * Update beacon_node/beacon_chain/src/attestation_verification.rs * Update beacon_node/beacon_chain/src/attestation_verification.rs --- Cargo.lock | 5 +- Cargo.toml | 2 +- .../beacon_chain/src/attestation_simulator.rs | 2 +- .../src/attestation_verification.rs | 386 ++++++++++----- .../src/attestation_verification/batch.rs | 9 +- .../beacon_chain/src/attester_cache.rs | 2 + .../beacon_chain/src/beacon_block_reward.rs | 2 +- beacon_node/beacon_chain/src/beacon_chain.rs | 183 ++++--- .../beacon_chain/src/bellatrix_readiness.rs | 4 +- beacon_node/beacon_chain/src/block_reward.rs | 9 +- .../beacon_chain/src/block_verification.rs | 2 +- .../beacon_chain/src/early_attester_cache.rs | 22 +- beacon_node/beacon_chain/src/errors.rs | 3 + beacon_node/beacon_chain/src/lib.rs | 2 +- .../src/naive_aggregation_pool.rs | 353 ++++++++++---- .../beacon_chain/src/observed_aggregates.rs | 166 +++++-- .../beacon_chain/src/observed_operations.rs | 15 +- beacon_node/beacon_chain/src/schema_change.rs | 9 + .../src/schema_change/migration_schema_v20.rs | 103 ++++ beacon_node/beacon_chain/src/test_utils.rs | 297 ++++++++---- .../beacon_chain/src/validator_monitor.rs | 49 +- .../tests/attestation_production.rs | 34 +- .../tests/attestation_verification.rs | 363 ++++++++++---- .../beacon_chain/tests/block_verification.rs | 178 +++++-- .../tests/payload_invalidation.rs | 24 +- beacon_node/beacon_chain/tests/store_tests.rs | 38 +- .../tests/sync_committee_verification.rs | 3 +- beacon_node/beacon_chain/tests/tests.rs | 2 +- beacon_node/client/src/notifier.rs | 2 +- beacon_node/execution_layer/src/lib.rs | 2 +- .../http_api/src/block_packing_efficiency.rs | 57 ++- beacon_node/http_api/src/lib.rs | 27 +- .../http_api/src/publish_attestations.rs | 4 +- beacon_node/http_api/tests/tests.rs | 53 ++- .../lighthouse_network/src/types/pubsub.rs | 98 +++- .../gossip_methods.rs | 78 ++- .../src/network_beacon_processor/mod.rs | 2 +- .../src/subnet_service/attestation_subnets.rs | 2 +- beacon_node/operation_pool/src/attestation.rs | 37 +- .../operation_pool/src/attestation_storage.rs | 298 ++++++++++-- .../operation_pool/src/attester_slashing.rs | 18 +- beacon_node/operation_pool/src/lib.rs | 161 +++++-- beacon_node/operation_pool/src/persistence.rs | 182 ++++--- beacon_node/src/lib.rs | 10 +- beacon_node/store/src/consensus_context.rs | 5 +- beacon_node/store/src/metadata.rs | 2 +- common/eth2/src/types.rs | 15 + consensus/fork_choice/src/fork_choice.rs | 59 ++- consensus/fork_choice/tests/tests.rs | 36 +- .../src/common/get_attesting_indices.rs | 174 ++++++- .../src/common/get_indexed_attestation.rs | 21 - consensus/state_processing/src/common/mod.rs | 6 +- .../state_processing/src/consensus_context.rs | 56 +-- consensus/state_processing/src/lib.rs | 2 +- .../block_signature_verifier.rs | 8 +- .../src/per_block_processing/errors.rs | 1 + .../is_valid_indexed_attestation.rs | 8 +- .../process_operations.rs | 58 ++- .../per_block_processing/signature_sets.rs | 53 +-- .../src/per_block_processing/tests.rs | 110 ++++- .../verify_attestation.rs | 16 +- .../verify_attester_slashing.rs | 21 +- .../base/validator_statuses.rs | 2 +- .../state_processing/src/upgrade/altair.rs | 4 +- .../state_processing/src/verify_operation.rs | 216 ++++++++- consensus/types/src/aggregate_and_proof.rs | 125 +++-- consensus/types/src/attestation.rs | 449 +++++++++++++++++- consensus/types/src/attester_slashing.rs | 173 ++++++- consensus/types/src/beacon_block.rs | 40 +- consensus/types/src/beacon_block_body.rs | 341 ++++++++----- consensus/types/src/beacon_state.rs | 8 + .../types/src/beacon_state/committee_cache.rs | 2 + consensus/types/src/chain_spec.rs | 2 +- consensus/types/src/eth_spec.rs | 10 + .../types/src/execution_payload_header.rs | 17 +- consensus/types/src/fork_name.rs | 8 + consensus/types/src/indexed_attestation.rs | 180 ++++++- consensus/types/src/lib.rs | 30 +- consensus/types/src/light_client_bootstrap.rs | 36 +- .../types/src/light_client_finality_update.rs | 99 ++-- consensus/types/src/light_client_header.rs | 54 ++- .../src/light_client_optimistic_update.rs | 37 +- consensus/types/src/light_client_update.rs | 31 +- .../types/src/signed_aggregate_and_proof.rs | 86 +++- consensus/types/src/signed_beacon_block.rs | 2 + consensus/types/src/subnet_id.rs | 14 +- .../types/src/sync_committee_contribution.rs | 14 +- consensus/types/src/validator.rs | 6 +- lcli/src/indexed_attestations.rs | 13 +- lcli/src/transition_blocks.rs | 2 +- slasher/Cargo.toml | 1 + slasher/src/array.rs | 20 +- slasher/src/attestation_queue.rs | 7 +- slasher/src/attester_record.rs | 18 +- slasher/src/config.rs | 3 +- slasher/src/database.rs | 146 +++++- slasher/src/error.rs | 7 + slasher/src/lib.rs | 39 +- slasher/src/slasher.rs | 13 +- slasher/src/test_utils.rs | 71 ++- slasher/tests/attester_slashings.rs | 235 ++++++--- slasher/tests/proposer_slashings.rs | 8 +- slasher/tests/random.rs | 6 +- slasher/tests/wrap_around.rs | 8 +- testing/ef_tests/check_all_files_accessed.py | 4 +- testing/ef_tests/src/cases/fork_choice.rs | 52 +- testing/ef_tests/src/cases/operations.rs | 25 +- testing/ef_tests/src/handler.rs | 8 + testing/ef_tests/src/type_name.rs | 11 +- testing/ef_tests/tests/tests.rs | 60 ++- testing/web3signer_tests/src/lib.rs | 20 +- validator_client/src/attestation_service.rs | 58 ++- validator_client/src/block_service.rs | 4 +- .../src/http_api/tests/keystores.rs | 10 +- validator_client/src/signing_method.rs | 2 +- .../src/signing_method/web3signer.rs | 2 +- validator_client/src/validator_store.rs | 27 +- watch/src/database/mod.rs | 2 +- 118 files changed, 5076 insertions(+), 1741 deletions(-) create mode 100644 beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs delete mode 100644 consensus/state_processing/src/common/get_indexed_attestation.rs diff --git a/Cargo.lock b/Cargo.lock index a1865289b0..90196ea5b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7653,6 +7653,7 @@ dependencies = [ "safe_arith", "serde", "slog", + "ssz_types", "strum", "tempfile", "tree_hash", @@ -8034,9 +8035,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "superstruct" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4e1f478a7728f8855d7e620e9a152cf8932c6614f86564c886f9b8141f3201" +checksum = "bf0f31f730ad9e579364950e10d6172b4a9bd04b447edf5988b066a860cc340e" dependencies = [ "darling", "itertools", diff --git a/Cargo.toml b/Cargo.toml index b942d1719e..d67f6edf1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -162,7 +162,7 @@ smallvec = "1.11.2" snap = "1" ssz_types = "0.6" strum = { version = "0.24", features = ["derive"] } -superstruct = "0.7" +superstruct = "0.8" syn = "1" sysinfo = "0.26" tempfile = "3" diff --git a/beacon_node/beacon_chain/src/attestation_simulator.rs b/beacon_node/beacon_chain/src/attestation_simulator.rs index 6453158458..c97c4490af 100644 --- a/beacon_node/beacon_chain/src/attestation_simulator.rs +++ b/beacon_node/beacon_chain/src/attestation_simulator.rs @@ -82,7 +82,7 @@ pub fn produce_unaggregated_attestation( // Store the unaggregated attestation in the validator monitor for later processing match chain.produce_unaggregated_attestation(current_slot, beacon_committee_index) { Ok(unaggregated_attestation) => { - let data = &unaggregated_attestation.data; + let data = unaggregated_attestation.data(); debug!( chain.log, diff --git a/beacon_node/beacon_chain/src/attestation_verification.rs b/beacon_node/beacon_chain/src/attestation_verification.rs index b7019d79b4..06fba937d8 100644 --- a/beacon_node/beacon_chain/src/attestation_verification.rs +++ b/beacon_node/beacon_chain/src/attestation_verification.rs @@ -35,17 +35,23 @@ mod batch; use crate::{ - beacon_chain::VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, metrics, - observed_aggregates::ObserveOutcome, observed_attesters::Error as ObservedAttestersError, + beacon_chain::VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT, + metrics, + observed_aggregates::{ObserveOutcome, ObservedAttestationKey}, + observed_attesters::Error as ObservedAttestersError, BeaconChain, BeaconChainError, BeaconChainTypes, }; use bls::verify_signature_sets; +use itertools::Itertools; use proto_array::Block as ProtoBlock; use slog::debug; use slot_clock::SlotClock; use state_processing::{ - common::get_indexed_attestation, - per_block_processing::errors::AttestationValidationError, + 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, @@ -55,8 +61,9 @@ use std::borrow::Cow; use strum::AsRefStr; use tree_hash::TreeHash; use types::{ - Attestation, BeaconCommittee, ChainSpec, CommitteeIndex, Epoch, EthSpec, ForkName, Hash256, - IndexedAttestation, SelectionProof, SignedAggregateAndProof, Slot, SubnetId, + Attestation, AttestationRef, BeaconCommittee, BeaconStateError::NoCommitteeFound, ChainSpec, + CommitteeIndex, Epoch, EthSpec, Hash256, IndexedAttestation, SelectionProof, + SignedAggregateAndProof, Slot, SubnetId, }; pub use batch::{batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations}; @@ -137,6 +144,12 @@ pub enum Error { /// /// 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 @@ -185,6 +198,12 @@ pub enum Error { /// /// The peer has sent an invalid message. NotExactlyOneAggregationBitSet(usize), + /// 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. /// @@ -248,7 +267,7 @@ pub enum Error { impl From for Error { fn from(e: BeaconChainError) -> Self { - Error::BeaconChainError(e) + Self::BeaconChainError(e) } } @@ -263,10 +282,11 @@ enum CheckAttestationSignature { /// `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, - attestation_data_root: Hash256, + observed_attestation_key_root: Hash256, } /// Wraps a `Attestation` that has been verified up until the point that an `IndexedAttestation` can @@ -274,7 +294,7 @@ struct IndexedAggregatedAttestation<'a, T: BeaconChainTypes> { /// /// These attestations have *not* undergone signature verification. struct IndexedUnaggregatedAttestation<'a, T: BeaconChainTypes> { - attestation: &'a Attestation, + attestation: AttestationRef<'a, T::EthSpec>, indexed_attestation: IndexedAttestation, subnet_id: SubnetId, validator_index: u64, @@ -295,7 +315,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { /// Wraps an `Attestation` that has been fully verified for propagation on the gossip network. pub struct VerifiedUnaggregatedAttestation<'a, T: BeaconChainTypes> { - attestation: &'a Attestation, + attestation: AttestationRef<'a, T::EthSpec>, indexed_attestation: IndexedAttestation, subnet_id: SubnetId, } @@ -322,20 +342,20 @@ impl<'a, T: BeaconChainTypes> Clone for IndexedUnaggregatedAttestation<'a, T> { /// 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) -> &Attestation; + fn attestation(&self) -> AttestationRef; 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(); - let attesting_indices = self.indexed_attestation().attesting_indices.clone().into(); + let attestation = self.attestation().clone_as_attestation(); + let attesting_indices = self.indexed_attestation().attesting_indices_to_vec(); (attestation, attesting_indices) } } impl<'a, T: BeaconChainTypes> VerifiedAttestation for VerifiedAggregatedAttestation<'a, T> { - fn attestation(&self) -> &Attestation { + fn attestation(&self) -> AttestationRef { self.attestation() } @@ -345,7 +365,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAttestation for VerifiedAggregatedAttes } impl<'a, T: BeaconChainTypes> VerifiedAttestation for VerifiedUnaggregatedAttestation<'a, T> { - fn attestation(&self) -> &Attestation { + fn attestation(&self) -> AttestationRef { self.attestation } @@ -357,7 +377,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAttestation for VerifiedUnaggregatedAtt /// 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(&'a Attestation, TErr), + SignatureNotChecked(AttestationRef<'a, T::EthSpec>, TErr), /// As for `SignatureNotChecked`, but we know the `IndexedAttestation`. SignatureNotCheckedIndexed(IndexedAttestation, TErr), /// The attestation's signature is invalid, so it will never be slashable. @@ -382,7 +402,7 @@ fn process_slash_info( let (indexed_attestation, check_signature, err) = match slash_info { SignatureNotChecked(attestation, err) => { if let Error::UnknownHeadBlock { .. } = err { - if attestation.data.beacon_block_root == attestation.data.target.root { + if attestation.data().beacon_block_root == attestation.data().target.root { return err; } } @@ -451,7 +471,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { signed_aggregate: &SignedAggregateAndProof, chain: &BeaconChain, ) -> Result { - let attestation = &signed_aggregate.message.aggregate; + let attestation = signed_aggregate.message().aggregate(); // Ensure attestation is within the last ATTESTATION_PROPAGATION_SLOT_RANGE slots (within a // MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance). @@ -460,30 +480,39 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { verify_propagation_slot_range(&chain.slot_clock, attestation, &chain.spec)?; // Check the attestation's epoch matches its target. - if attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()) - != attestation.data.target.epoch + 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, + slot: attestation.data().slot, + epoch: attestation.data().target.epoch, }); } - // Ensure the valid aggregated attestation has not already been seen locally. - let attestation_data = &attestation.data; - let attestation_data_root = attestation_data.tree_hash_root(); + 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, attestation_data_root) + .is_known_subset(attestation, observed_attestation_key_root) .map_err(|e| Error::BeaconChainError(e.into()))? { metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); - return Err(Error::AttestationSupersetKnown(attestation_data_root)); + return Err(Error::AttestationSupersetKnown( + observed_attestation_key_root, + )); } - let aggregator_index = signed_aggregate.message.aggregator_index; + let aggregator_index = signed_aggregate.message().aggregator_index(); // Ensure there has been no other observed aggregate for the given `aggregator_index`. // @@ -491,7 +520,7 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { match chain .observed_aggregators .read() - .validator_has_been_observed(attestation.data.target.epoch, aggregator_index as usize) + .validator_has_been_observed(attestation.data().target.epoch, aggregator_index as usize) { Ok(true) => Err(Error::AggregatorAlreadyKnown(aggregator_index)), Ok(false) => Ok(()), @@ -523,10 +552,10 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { verify_attestation_target_root::(&head_block, attestation)?; // Ensure that the attestation has participants. - if attestation.aggregation_bits.is_zero() { + if attestation.is_aggregation_bits_zero() { Err(Error::EmptyAggregationBitfield) } else { - Ok(attestation_data_root) + Ok(observed_attestation_key_root) } } @@ -536,23 +565,47 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { chain: &BeaconChain, ) -> Result> { use AttestationSlashInfo::*; - - let attestation = &signed_aggregate.message.aggregate; - let aggregator_index = signed_aggregate.message.aggregator_index; - let attestation_data_root = match Self::verify_early_checks(signed_aggregate, chain) { + 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)), + 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 = - |(committee, _): (BeaconCommittee, CommitteesPerSlot)| { - // Note: this clones the signature which is known to be a relatively slow operation. - // - // Future optimizations should remove this clone. - let selection_proof = - SelectionProof::from(signed_aggregate.message.selection_proof.clone()); + |(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; - if !selection_proof + 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(e.into()))? { @@ -564,23 +617,44 @@ impl<'a, T: BeaconChainTypes> IndexedAggregatedAttestation<'a, T> { return Err(Error::AggregatorNotInCommittee { aggregator_index }); } - get_indexed_attestation(committee.committee, attestation) - .map_err(|e| BeaconChainError::from(e).into()) + // 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 indexed_attestation = match map_attestation_committee( + 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)), + Err(e) => { + return Err(SignatureNotChecked( + signed_aggregate.message().aggregate(), + e, + )) + } }; - Ok(IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_data_root, + observed_attestation_key_root, }) } } @@ -589,11 +663,11 @@ 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, - attestation_data_root: Hash256, + observed_attestation_key_root: Hash256, chain: &BeaconChain, ) -> Result<(), Error> { - let attestation = &signed_aggregate.message.aggregate; - let aggregator_index = signed_aggregate.message.aggregator_index; + 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. // @@ -602,11 +676,13 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { if let ObserveOutcome::Subset = chain .observed_attestations .write() - .observe_item(attestation, Some(attestation_data_root)) + .observe_item(attestation, Some(observed_attestation_key_root)) .map_err(|e| Error::BeaconChainError(e.into()))? { metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_SUBSETS); - return Err(Error::AttestationSupersetKnown(attestation_data_root)); + return Err(Error::AttestationSupersetKnown( + observed_attestation_key_root, + )); } // Observe the aggregator so we don't process another aggregate from them. @@ -616,12 +692,12 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { if chain .observed_aggregators .write() - .observe_validator(attestation.data.target.epoch, aggregator_index as usize) + .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, + epoch: attestation.data().target.epoch, }); } @@ -666,7 +742,7 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { let IndexedAggregatedAttestation { signed_aggregate, indexed_attestation, - attestation_data_root, + observed_attestation_key_root, } = signed_aggregate; match check_signature { @@ -690,7 +766,9 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { CheckAttestationSignature::No => (), }; - if let Err(e) = Self::verify_late_checks(signed_aggregate, attestation_data_root, chain) { + if let Err(e) = + Self::verify_late_checks(signed_aggregate, observed_attestation_key_root, chain) + { return Err(SignatureValid(indexed_attestation, e)); } @@ -701,8 +779,8 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { } /// Returns the underlying `attestation` for the `signed_aggregate`. - pub fn attestation(&self) -> &Attestation { - &self.signed_aggregate.message.aggregate + pub fn attestation(&self) -> AttestationRef<'a, T::EthSpec> { + self.signed_aggregate.message().aggregate() } /// Returns the underlying `signed_aggregate`. @@ -714,16 +792,16 @@ impl<'a, T: BeaconChainTypes> VerifiedAggregatedAttestation<'a, T> { impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { /// Run the checks that happen before an indexed attestation is constructed. pub fn verify_early_checks( - attestation: &Attestation, + attestation: AttestationRef, chain: &BeaconChain, ) -> Result<(), Error> { - let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()); + 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 { + if attestation_epoch != attestation.data().target.epoch { return Err(Error::InvalidTargetEpoch { - slot: attestation.data.slot, - epoch: attestation.data.target.epoch, + slot: attestation.data().slot, + epoch: attestation.data().target.epoch, }); } @@ -735,11 +813,14 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { // Check to ensure that the attestation is "unaggregated". I.e., it has exactly one // aggregation bit set. - let num_aggregation_bits = attestation.aggregation_bits.num_set_bits(); + let num_aggregation_bits = attestation.num_set_aggregation_bits(); if num_aggregation_bits != 1 { return Err(Error::NotExactlyOneAggregationBitSet(num_aggregation_bits)); } + // [New in Electra:EIP7549] + verify_committee_index(attestation)?; + // Attestations must be for a known block. If the block is unknown, we simply drop the // attestation and do not delay consideration for later. // @@ -755,14 +836,14 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { /// Run the checks that apply to the indexed attestation before the signature is checked. pub fn verify_middle_checks( - attestation: &Attestation, + attestation: AttestationRef, indexed_attestation: &IndexedAttestation, committees_per_slot: u64, subnet_id: Option, chain: &BeaconChain, ) -> Result<(u64, SubnetId), Error> { - let expected_subnet_id = SubnetId::compute_subnet_for_attestation_data::( - &indexed_attestation.data, + let expected_subnet_id = SubnetId::compute_subnet_for_attestation::( + attestation, committees_per_slot, &chain.spec, ) @@ -779,8 +860,7 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { }; let validator_index = *indexed_attestation - .attesting_indices - .first() + .attesting_indices_first() .ok_or(Error::NotExactlyOneAggregationBitSet(0))?; /* @@ -790,12 +870,12 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { if chain .observed_gossip_attesters .read() - .validator_has_been_observed(attestation.data.target.epoch, validator_index as usize) + .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, + epoch: attestation.data().target.epoch, }); } @@ -812,7 +892,7 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { subnet_id: Option, chain: &BeaconChain, ) -> Result { - Self::verify_slashable(attestation, subnet_id, chain) + Self::verify_slashable(attestation.to_ref(), subnet_id, chain) .map(|verified_unaggregated| { if let Some(slasher) = chain.slasher.as_ref() { slasher.accept_attestation(verified_unaggregated.indexed_attestation.clone()); @@ -824,7 +904,7 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { /// Verify the attestation, producing extra information about whether it might be slashable. pub fn verify_slashable( - attestation: &'a Attestation, + attestation: AttestationRef<'a, T::EthSpec>, subnet_id: Option, chain: &BeaconChain, ) -> Result> { @@ -873,7 +953,7 @@ impl<'a, T: BeaconChainTypes> IndexedUnaggregatedAttestation<'a, T> { impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { /// Run the checks that apply after the signature has been checked. fn verify_late_checks( - attestation: &Attestation, + attestation: AttestationRef, validator_index: u64, chain: &BeaconChain, ) -> Result<(), Error> { @@ -886,12 +966,12 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { if chain .observed_gossip_attesters .write() - .observe_validator(attestation.data.target.epoch, validator_index as usize) + .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, + epoch: attestation.data().target.epoch, }); } Ok(()) @@ -967,7 +1047,7 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { } /// Returns the wrapped `attestation`. - pub fn attestation(&self) -> &Attestation { + pub fn attestation(&self) -> AttestationRef { self.attestation } @@ -997,34 +1077,34 @@ impl<'a, T: BeaconChainTypes> VerifiedUnaggregatedAttestation<'a, T> { /// already finalized. fn verify_head_block_is_known( chain: &BeaconChain, - attestation: &Attestation, + attestation: AttestationRef, max_skip_slots: Option, ) -> Result { let block_opt = chain .canonical_head .fork_choice_read_lock() - .get_block(&attestation.data.beacon_block_root) + .get_block(&attestation.data().beacon_block_root) .or_else(|| { chain .early_attester_cache - .get_proto_block(attestation.data.beacon_block_root) + .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 { - if attestation.data.slot > block.slot + max_skip_slots { + if attestation.data().slot > block.slot + max_skip_slots { return Err(Error::TooManySkippedSlots { head_block_slot: block.slot, - attestation_slot: attestation.data.slot, + attestation_slot: attestation.data().slot, }); } } Ok(block) - } else if chain.is_pre_finalization_block(attestation.data.beacon_block_root)? { + } else if chain.is_pre_finalization_block(attestation.data().beacon_block_root)? { Err(Error::HeadBlockFinalized { - beacon_block_root: attestation.data.beacon_block_root, + beacon_block_root: attestation.data().beacon_block_root, }) } else { // The block is either: @@ -1034,7 +1114,7 @@ fn verify_head_block_is_known( // 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, + beacon_block_root: attestation.data().beacon_block_root, }) } } @@ -1045,10 +1125,10 @@ fn verify_head_block_is_known( /// Accounts for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`. pub fn verify_propagation_slot_range( slot_clock: &S, - attestation: &Attestation, + attestation: AttestationRef, spec: &ChainSpec, ) -> Result<(), Error> { - let attestation_slot = attestation.data.slot; + let attestation_slot = attestation.data().slot; let latest_permissible_slot = slot_clock .now_with_future_tolerance(spec.maximum_gossip_clock_disparity()) .ok_or(BeaconChainError::UnableToReadSlot)?; @@ -1067,14 +1147,13 @@ pub fn verify_propagation_slot_range( let current_fork = spec.fork_name_at_slot::(slot_clock.now().ok_or(BeaconChainError::UnableToReadSlot)?); - let earliest_permissible_slot = match current_fork { - ForkName::Base | ForkName::Altair | ForkName::Bellatrix | ForkName::Capella => { - one_epoch_prior - } - // EIP-7045 - ForkName::Deneb | ForkName::Electra => one_epoch_prior + let earliest_permissible_slot = if !current_fork.deneb_enabled() { + one_epoch_prior + // EIP-7045 + } else { + one_epoch_prior .epoch(E::slots_per_epoch()) - .start_slot(E::slots_per_epoch()), + .start_slot(E::slots_per_epoch()) }; if attestation_slot < earliest_permissible_slot { @@ -1102,18 +1181,17 @@ pub fn verify_attestation_signature( let fork = chain .spec - .fork_at_epoch(indexed_attestation.data.target.epoch); + .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.signature(), indexed_attestation, &fork, chain.genesis_validators_root, &chain.spec, ) .map_err(BeaconChainError::SignatureSetError)?; - metrics::stop_timer(signature_setup_timer); let _signature_verification_timer = @@ -1130,11 +1208,11 @@ pub fn verify_attestation_signature( /// `attestation.data.beacon_block_root`. pub fn verify_attestation_target_root( head_block: &ProtoBlock, - attestation: &Attestation, + attestation: AttestationRef, ) -> 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()); + 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. // @@ -1147,7 +1225,7 @@ pub fn verify_attestation_target_root( // Reference: // https://github.com/ethereum/eth2.0-specs/pull/2001#issuecomment-699246659 return Err(Error::InvalidTargetRoot { - attestation: attestation.data.target.root, + 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, @@ -1166,9 +1244,9 @@ pub fn verify_attestation_target_root( }; // Reject any attestation with an invalid target root. - if target_root != attestation.data.target.root { + if target_root != attestation.data().target.root { return Err(Error::InvalidTargetRoot { - attestation: attestation.data.target.root, + attestation: attestation.data().target.root, expected: Some(target_root), }); } @@ -1199,14 +1277,14 @@ pub fn verify_signed_aggregate_signatures( .try_read_for(VALIDATOR_PUBKEY_CACHE_LOCK_TIMEOUT) .ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout)?; - let aggregator_index = signed_aggregate.message.aggregator_index; + 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); + .fork_at_epoch(indexed_attestation.data().target.epoch); let signature_sets = vec![ signed_aggregate_selection_proof_signature_set( @@ -1227,7 +1305,7 @@ pub fn verify_signed_aggregate_signatures( .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.signature(), indexed_attestation, &fork, chain.genesis_validators_root, @@ -1239,6 +1317,28 @@ pub fn verify_signed_aggregate_signatures( 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(()) +} + /// Assists in readability. type CommitteesPerSlot = u64; @@ -1246,35 +1346,71 @@ type CommitteesPerSlot = u64; /// public keys cached in the `chain`. pub fn obtain_indexed_attestation_and_committees_per_slot( chain: &BeaconChain, - attestation: &Attestation, + attestation: AttestationRef, ) -> Result<(IndexedAttestation, CommitteesPerSlot), Error> { - map_attestation_committee(chain, attestation, |(committee, committees_per_slot)| { - get_indexed_attestation(committee.committee, attestation) - .map(|attestation| (attestation, committees_per_slot)) - .map_err(Error::Invalid) + 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 committee for -/// an attestation can be complex. It might involve reading straight from the +/// 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 committee for `attestation` isn't found in the `shuffling_cache`, we will read a state +/// 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`. -fn map_attestation_committee( +/// +/// Committees are sorted by ascending index order 0..committees_per_slot +fn map_attestation_committees( chain: &BeaconChain, - attestation: &Attestation, + attestation: AttestationRef, map_fn: F, ) -> Result where T: BeaconChainTypes, - F: Fn((BeaconCommittee, CommitteesPerSlot)) -> Result, + F: Fn((Vec, CommitteesPerSlot)) -> Result, { - let attestation_epoch = attestation.data.slot.epoch(T::EthSpec::slots_per_epoch()); - let target = &attestation.data.target; + 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. // @@ -1297,12 +1433,12 @@ where let committees_per_slot = committee_cache.committees_per_slot(); Ok(committee_cache - .get_beacon_committee(attestation.data.slot, attestation.data.index) - .map(|committee| map_fn((committee, committees_per_slot))) - .unwrap_or_else(|| { + .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.data.index, + slot: attestation.data().slot, + index: attestation.committee_index().unwrap_or(0), }) })) }) diff --git a/beacon_node/beacon_chain/src/attestation_verification/batch.rs b/beacon_node/beacon_chain/src/attestation_verification/batch.rs index 6aec2bef68..07fad1bd4a 100644 --- a/beacon_node/beacon_chain/src/attestation_verification/batch.rs +++ b/beacon_node/beacon_chain/src/attestation_verification/batch.rs @@ -66,14 +66,13 @@ where .ok_or(BeaconChainError::ValidatorPubkeyCacheLockTimeout)?; let mut signature_sets = Vec::with_capacity(num_indexed * 3); - // Iterate, flattening to get only the `Ok` values. for indexed in indexing_results.iter().flatten() { let signed_aggregate = &indexed.signed_aggregate; let indexed_attestation = &indexed.indexed_attestation; let fork = chain .spec - .fork_at_epoch(indexed_attestation.data.target.epoch); + .fork_at_epoch(indexed_attestation.data().target.epoch); signature_sets.push( signed_aggregate_selection_proof_signature_set( @@ -98,7 +97,7 @@ where signature_sets.push( indexed_attestation_signature_set_from_pubkeys( |validator_index| pubkey_cache.get(validator_index).map(Cow::Borrowed), - &indexed_attestation.signature, + indexed_attestation.signature(), indexed_attestation, &fork, chain.genesis_validators_root, @@ -182,11 +181,11 @@ where let indexed_attestation = &partially_verified.indexed_attestation; let fork = chain .spec - .fork_at_epoch(indexed_attestation.data.target.epoch); + .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.signature(), indexed_attestation, &fork, chain.genesis_validators_root, diff --git a/beacon_node/beacon_chain/src/attester_cache.rs b/beacon_node/beacon_chain/src/attester_cache.rs index 2e07cd32ed..b5012e8e4e 100644 --- a/beacon_node/beacon_chain/src/attester_cache.rs +++ b/beacon_node/beacon_chain/src/attester_cache.rs @@ -15,6 +15,7 @@ use state_processing::state_advance::{partial_state_advance, Error as StateAdvan use std::collections::HashMap; use std::ops::Range; use types::{ + attestation::Error as AttestationError, beacon_state::{ compute_committee_index_in_epoch, compute_committee_range_in_epoch, epoch_committee_count, }, @@ -59,6 +60,7 @@ pub enum Error { InverseRange { range: Range, }, + AttestationError(AttestationError), } impl From for Error { diff --git a/beacon_node/beacon_chain/src/beacon_block_reward.rs b/beacon_node/beacon_chain/src/beacon_block_reward.rs index 5b70215d22..33567001e3 100644 --- a/beacon_node/beacon_chain/src/beacon_block_reward.rs +++ b/beacon_node/beacon_chain/src/beacon_block_reward.rs @@ -202,7 +202,7 @@ impl BeaconChain { let mut previous_epoch_participation = state.previous_epoch_participation()?.clone(); for attestation in block.body().attestations() { - let data = &attestation.data; + let data = attestation.data(); let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); // [Modified in Deneb:EIP7045] let participation_flag_indices = get_attestation_participation_flag_indices( diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 77e1bc095e..f1d9ce791e 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -85,7 +85,9 @@ use futures::channel::mpsc::Sender; use itertools::process_results; use itertools::Itertools; use kzg::Kzg; -use operation_pool::{AttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella}; +use operation_pool::{ + CompactAttestationRef, OperationPool, PersistedOperationPool, ReceivedPreCapella, +}; use parking_lot::{Mutex, RwLock}; use proto_array::{DoNotReOrg, ProposerHeadError}; use safe_arith::SafeArith; @@ -1643,14 +1645,49 @@ impl BeaconChain { Ok((duties, dependent_root, execution_status)) } + pub fn get_aggregated_attestation( + &self, + attestation: AttestationRef, + ) -> Result>, Error> { + match attestation { + AttestationRef::Base(att) => self.get_aggregated_attestation_base(&att.data), + AttestationRef::Electra(att) => self.get_aggregated_attestation_electra( + att.data.slot, + &att.data.tree_hash_root(), + att.committee_index() + .ok_or(Error::AttestationCommitteeIndexNotSet)?, + ), + } + } + /// Returns an aggregated `Attestation`, if any, that has a matching `attestation.data`. /// /// The attestation will be obtained from `self.naive_aggregation_pool`. - pub fn get_aggregated_attestation( + pub fn get_aggregated_attestation_base( &self, data: &AttestationData, ) -> Result>, Error> { - if let Some(attestation) = self.naive_aggregation_pool.read().get(data) { + let attestation_key = crate::naive_aggregation_pool::AttestationKey::new_base(data); + if let Some(attestation) = self.naive_aggregation_pool.read().get(&attestation_key) { + self.filter_optimistic_attestation(attestation) + .map(Option::Some) + } else { + Ok(None) + } + } + + pub fn get_aggregated_attestation_electra( + &self, + slot: Slot, + attestation_data_root: &Hash256, + committee_index: CommitteeIndex, + ) -> Result>, Error> { + let attestation_key = crate::naive_aggregation_pool::AttestationKey::new_electra( + slot, + *attestation_data_root, + committee_index, + ); + if let Some(attestation) = self.naive_aggregation_pool.read().get(&attestation_key) { self.filter_optimistic_attestation(attestation) .map(Option::Some) } else { @@ -1662,16 +1699,21 @@ impl BeaconChain { /// `attestation.data.tree_hash_root()`. /// /// The attestation will be obtained from `self.naive_aggregation_pool`. - pub fn get_aggregated_attestation_by_slot_and_root( + /// + /// NOTE: This function will *only* work with pre-electra attestations and it only + /// exists to support the pre-electra validator API method. + pub fn get_pre_electra_aggregated_attestation_by_slot_and_root( &self, slot: Slot, attestation_data_root: &Hash256, ) -> Result>, Error> { - if let Some(attestation) = self - .naive_aggregation_pool - .read() - .get_by_slot_and_root(slot, attestation_data_root) - { + let attestation_key = + crate::naive_aggregation_pool::AttestationKey::new_base_from_slot_and_root( + slot, + *attestation_data_root, + ); + + if let Some(attestation) = self.naive_aggregation_pool.read().get(&attestation_key) { self.filter_optimistic_attestation(attestation) .map(Option::Some) } else { @@ -1685,7 +1727,7 @@ impl BeaconChain { &self, attestation: Attestation, ) -> Result, Error> { - let beacon_block_root = attestation.data.beacon_block_root; + let beacon_block_root = attestation.data().beacon_block_root; match self .canonical_head .fork_choice_read_lock() @@ -1951,17 +1993,15 @@ impl BeaconChain { }; drop(cache_timer); - Ok(Attestation { - aggregation_bits: BitList::with_capacity(committee_len)?, - data: AttestationData { - slot: request_slot, - index: request_index, - beacon_block_root, - source: justified_checkpoint, - target, - }, - signature: AggregateSignature::empty(), - }) + Ok(Attestation::::empty_for_signing( + request_index, + committee_len, + request_slot, + beacon_block_root, + justified_checkpoint, + target, + &self.spec, + )?) } /// Performs the same validation as `Self::verify_unaggregated_attestation_for_gossip`, but for @@ -1999,8 +2039,9 @@ impl BeaconChain { // This method is called for API and gossip attestations, so this covers all unaggregated attestation events if let Some(event_handler) = self.event_handler.as_ref() { if event_handler.has_attestation_subscribers() { - event_handler - .register(EventKind::Attestation(Box::new(v.attestation().clone()))); + event_handler.register(EventKind::Attestation(Box::new( + v.attestation().clone_as_attestation(), + ))); } } metrics::inc_counter(&metrics::UNAGGREGATED_ATTESTATION_PROCESSING_SUCCESSES); @@ -2036,8 +2077,9 @@ impl BeaconChain { // This method is called for API and gossip attestations, so this covers all aggregated attestation events if let Some(event_handler) = self.event_handler.as_ref() { if event_handler.has_attestation_subscribers() { - event_handler - .register(EventKind::Attestation(Box::new(v.attestation().clone()))); + event_handler.register(EventKind::Attestation(Box::new( + v.attestation().clone_as_attestation(), + ))); } } metrics::inc_counter(&metrics::AGGREGATED_ATTESTATION_PROCESSING_SUCCESSES); @@ -2146,7 +2188,7 @@ impl BeaconChain { .fork_choice_write_lock() .on_attestation( self.slot()?, - verified.indexed_attestation(), + verified.indexed_attestation().to_ref(), AttestationFromBlock::False, ) .map_err(Into::into) @@ -2173,8 +2215,8 @@ impl BeaconChain { self.log, "Stored unaggregated attestation"; "outcome" => ?outcome, - "index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), + "index" => attestation.committee_index(), + "slot" => attestation.data().slot.as_u64(), ), Err(NaiveAggregationError::SlotTooLow { slot, @@ -2192,8 +2234,8 @@ impl BeaconChain { self.log, "Failed to store unaggregated attestation"; "error" => ?e, - "index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), + "index" => attestation.committee_index(), + "slot" => attestation.data().slot.as_u64(), ); return Err(Error::from(e).into()); } @@ -2317,7 +2359,7 @@ impl BeaconChain { pub fn filter_op_pool_attestation( &self, filter_cache: &mut HashMap<(Hash256, Epoch), bool>, - att: &AttestationRef, + att: &CompactAttestationRef, state: &BeaconState, ) -> bool { *filter_cache @@ -2508,7 +2550,7 @@ impl BeaconChain { // Add to fork choice. self.canonical_head .fork_choice_write_lock() - .on_attester_slashing(attester_slashing.as_inner()); + .on_attester_slashing(attester_slashing.as_inner().to_ref()); if let Some(event_handler) = self.event_handler.as_ref() { if event_handler.has_attester_slashing_subscribers() { @@ -3786,7 +3828,7 @@ impl BeaconChain { self.log, "Failed to get indexed attestation"; "purpose" => "validator monitor", - "attestation_slot" => attestation.data.slot, + "attestation_slot" => attestation.data().slot, "error" => ?e, ); continue; @@ -3842,7 +3884,7 @@ impl BeaconChain { self.log, "Failed to register observed attestation"; "error" => ?e, - "epoch" => a.data.target.epoch + "epoch" => a.data().target.epoch ); } } @@ -3854,7 +3896,7 @@ impl BeaconChain { self.log, "Failed to get indexed attestation"; "purpose" => "observation", - "attestation_slot" => a.data.slot, + "attestation_slot" => a.data().slot, "error" => ?e, ); continue; @@ -3863,15 +3905,15 @@ impl BeaconChain { let mut observed_block_attesters = self.observed_block_attesters.write(); - for &validator_index in &indexed_attestation.attesting_indices { + for &validator_index in indexed_attestation.attesting_indices_iter() { if let Err(e) = observed_block_attesters - .observe_validator(a.data.target.epoch, validator_index as usize) + .observe_validator(a.data().target.epoch, validator_index as usize) { debug!( self.log, "Failed to register observed block attester"; "error" => ?e, - "epoch" => a.data.target.epoch, + "epoch" => a.data().target.epoch, "validator_index" => validator_index, ) } @@ -3895,13 +3937,13 @@ impl BeaconChain { self.log, "Failed to get indexed attestation"; "purpose" => "slasher", - "attestation_slot" => attestation.data.slot, + "attestation_slot" => attestation.data().slot, "error" => ?e, ); continue; } }; - slasher.accept_attestation(indexed_attestation.clone()); + slasher.accept_attestation(indexed_attestation.clone_as_indexed_attestation()); } } } @@ -3920,7 +3962,7 @@ impl BeaconChain { if block.slot() + 2 * T::EthSpec::slots_per_epoch() >= current_slot { metrics::observe( &metrics::OPERATIONS_PER_BLOCK_ATTESTATION, - block.body().attestations().len() as f64, + block.body().attestations_len() as f64, ); if let Ok(sync_aggregate) = block.body().sync_aggregate() { @@ -4934,7 +4976,8 @@ impl BeaconChain { metrics::start_timer(&metrics::BLOCK_PRODUCTION_UNAGGREGATED_TIMES); for attestation in self.naive_aggregation_pool.read().iter() { let import = |attestation: &Attestation| { - let attesting_indices = get_attesting_indices_from_state(&state, attestation)?; + let attesting_indices = + get_attesting_indices_from_state(&state, attestation.to_ref())?; self.op_pool .insert_attestation(attestation.clone(), attesting_indices) }; @@ -4957,11 +5000,11 @@ impl BeaconChain { initialize_epoch_cache(&mut state, &self.spec)?; let mut prev_filter_cache = HashMap::new(); - let prev_attestation_filter = |att: &AttestationRef| { + let prev_attestation_filter = |att: &CompactAttestationRef| { self.filter_op_pool_attestation(&mut prev_filter_cache, att, &state) }; let mut curr_filter_cache = HashMap::new(); - let curr_attestation_filter = |att: &AttestationRef| { + let curr_attestation_filter = |att: &CompactAttestationRef| { self.filter_op_pool_attestation(&mut curr_filter_cache, att, &state) }; @@ -4984,7 +5027,7 @@ impl BeaconChain { attestations.retain(|att| { verify_attestation_for_block_inclusion( &state, - att, + att.to_ref(), &mut tmp_ctxt, VerifySignatures::True, &self.spec, @@ -5115,6 +5158,28 @@ impl BeaconChain { bls_to_execution_changes, } = partial_beacon_block; + let (attester_slashings_base, attester_slashings_electra) = + attester_slashings.into_iter().fold( + (Vec::new(), Vec::new()), + |(mut base, mut electra), slashing| { + match slashing { + AttesterSlashing::Base(slashing) => base.push(slashing), + AttesterSlashing::Electra(slashing) => electra.push(slashing), + } + (base, electra) + }, + ); + let (attestations_base, attestations_electra) = attestations.into_iter().fold( + (Vec::new(), Vec::new()), + |(mut base, mut electra), attestation| { + match attestation { + Attestation::Base(attestation) => base.push(attestation), + Attestation::Electra(attestation) => electra.push(attestation), + } + (base, electra) + }, + ); + let (inner_block, maybe_blobs_and_proofs, execution_payload_value) = match &state { BeaconState::Base(_) => ( BeaconBlock::Base(BeaconBlockBase { @@ -5127,8 +5192,8 @@ impl BeaconChain { eth1_data, graffiti, proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings.into(), - attestations: attestations.into(), + attester_slashings: attester_slashings_base.into(), + attestations: attestations_base.into(), deposits: deposits.into(), voluntary_exits: voluntary_exits.into(), _phantom: PhantomData, @@ -5148,8 +5213,8 @@ impl BeaconChain { eth1_data, graffiti, proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings.into(), - attestations: attestations.into(), + attester_slashings: attester_slashings_base.into(), + attestations: attestations_base.into(), deposits: deposits.into(), voluntary_exits: voluntary_exits.into(), sync_aggregate: sync_aggregate @@ -5175,8 +5240,8 @@ impl BeaconChain { eth1_data, graffiti, proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings.into(), - attestations: attestations.into(), + attester_slashings: attester_slashings_base.into(), + attestations: attestations_base.into(), deposits: deposits.into(), voluntary_exits: voluntary_exits.into(), sync_aggregate: sync_aggregate @@ -5207,8 +5272,8 @@ impl BeaconChain { eth1_data, graffiti, proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings.into(), - attestations: attestations.into(), + attester_slashings: attester_slashings_base.into(), + attestations: attestations_base.into(), deposits: deposits.into(), voluntary_exits: voluntary_exits.into(), sync_aggregate: sync_aggregate @@ -5241,8 +5306,8 @@ impl BeaconChain { eth1_data, graffiti, proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings.into(), - attestations: attestations.into(), + attester_slashings: attester_slashings_base.into(), + attestations: attestations_base.into(), deposits: deposits.into(), voluntary_exits: voluntary_exits.into(), sync_aggregate: sync_aggregate @@ -5279,8 +5344,8 @@ impl BeaconChain { eth1_data, graffiti, proposer_slashings: proposer_slashings.into(), - attester_slashings: attester_slashings.into(), - attestations: attestations.into(), + attester_slashings: attester_slashings_electra.into(), + attestations: attestations_electra.into(), deposits: deposits.into(), voluntary_exits: voluntary_exits.into(), sync_aggregate: sync_aggregate @@ -5291,6 +5356,8 @@ impl BeaconChain { bls_to_execution_changes: bls_to_execution_changes.into(), blob_kzg_commitments: kzg_commitments .ok_or(BlockProductionError::InvalidPayloadFork)?, + // TODO(electra): finish consolidations when they're more spec'd out + consolidations: Vec::new().into(), }, }), maybe_blobs_and_proofs, @@ -5396,7 +5463,7 @@ impl BeaconChain { self.log, "Produced beacon block"; "parent" => ?block.parent_root(), - "attestations" => block.body().attestations().len(), + "attestations" => block.body().attestations_len(), "slot" => block.slot() ); diff --git a/beacon_node/beacon_chain/src/bellatrix_readiness.rs b/beacon_node/beacon_chain/src/bellatrix_readiness.rs index bf9e848126..60b1abaf09 100644 --- a/beacon_node/beacon_chain/src/bellatrix_readiness.rs +++ b/beacon_node/beacon_chain/src/bellatrix_readiness.rs @@ -244,8 +244,8 @@ impl BeaconChain { }); } - if let Some(&expected) = expected_withdrawals_root { - if let Some(&got) = got_withdrawals_root { + if let Some(expected) = expected_withdrawals_root { + if let Some(got) = got_withdrawals_root { if got != expected { return Ok(GenesisExecutionPayloadStatus::WithdrawalsRootMismatch { got, diff --git a/beacon_node/beacon_chain/src/block_reward.rs b/beacon_node/beacon_chain/src/block_reward.rs index fd0cfc7e9b..69eecc89b8 100644 --- a/beacon_node/beacon_chain/src/block_reward.rs +++ b/beacon_node/beacon_chain/src/block_reward.rs @@ -27,10 +27,12 @@ impl BeaconChain { let split_attestations = block .body() .attestations() - .iter() .map(|att| { let attesting_indices = get_attesting_indices_from_state(state, att)?; - Ok(SplitAttestation::new(att.clone(), attesting_indices)) + Ok(SplitAttestation::new( + att.clone_as_attestation(), + attesting_indices, + )) }) .collect::, BeaconChainError>>()?; @@ -86,8 +88,7 @@ impl BeaconChain { block .body() .attestations() - .iter() - .map(|a| a.data.clone()) + .map(|a| a.data().clone()) .collect() } else { vec![] diff --git a/beacon_node/beacon_chain/src/block_verification.rs b/beacon_node/beacon_chain/src/block_verification.rs index f4f6526a56..b24921e317 100644 --- a/beacon_node/beacon_chain/src/block_verification.rs +++ b/beacon_node/beacon_chain/src/block_verification.rs @@ -1625,7 +1625,7 @@ impl ExecutionPendingBlock { } // Register each attestation in the block with fork choice. - for (i, attestation) in block.message().body().attestations().iter().enumerate() { + for (i, attestation) in block.message().body().attestations().enumerate() { let _fork_choice_attestation_timer = metrics::start_timer(&metrics::FORK_CHOICE_PROCESS_ATTESTATION_TIMES); diff --git a/beacon_node/beacon_chain/src/early_attester_cache.rs b/beacon_node/beacon_chain/src/early_attester_cache.rs index 79d732f51b..dda699cc6c 100644 --- a/beacon_node/beacon_chain/src/early_attester_cache.rs +++ b/beacon_node/beacon_chain/src/early_attester_cache.rs @@ -122,18 +122,16 @@ impl EarlyAttesterCache { item.committee_lengths .get_committee_length::(request_slot, request_index, spec)?; - let attestation = Attestation { - aggregation_bits: BitList::with_capacity(committee_len) - .map_err(BeaconStateError::from)?, - data: AttestationData { - slot: request_slot, - index: request_index, - beacon_block_root: item.beacon_block_root, - source: item.source, - target: item.target, - }, - signature: AggregateSignature::empty(), - }; + let attestation = Attestation::empty_for_signing( + request_index, + committee_len, + request_slot, + item.beacon_block_root, + item.source, + item.target, + spec, + ) + .map_err(Error::AttestationError)?; metrics::inc_counter(&metrics::BEACON_EARLY_ATTESTER_CACHE_HITS); diff --git a/beacon_node/beacon_chain/src/errors.rs b/beacon_node/beacon_chain/src/errors.rs index 340f1f9f79..3d61d4f32d 100644 --- a/beacon_node/beacon_chain/src/errors.rs +++ b/beacon_node/beacon_chain/src/errors.rs @@ -226,6 +226,8 @@ pub enum BeaconChainError { LightClientError(LightClientError), UnsupportedFork, MilhouseError(MilhouseError), + AttestationError(AttestationError), + AttestationCommitteeIndexNotSet, } easy_from_to!(SlotProcessingError, BeaconChainError); @@ -256,6 +258,7 @@ easy_from_to!(AvailabilityCheckError, BeaconChainError); easy_from_to!(EpochCacheError, BeaconChainError); easy_from_to!(LightClientError, BeaconChainError); easy_from_to!(MilhouseError, BeaconChainError); +easy_from_to!(AttestationError, BeaconChainError); #[derive(Debug)] pub enum BlockProductionError { diff --git a/beacon_node/beacon_chain/src/lib.rs b/beacon_node/beacon_chain/src/lib.rs index f419429e09..466ab0b67e 100644 --- a/beacon_node/beacon_chain/src/lib.rs +++ b/beacon_node/beacon_chain/src/lib.rs @@ -39,7 +39,7 @@ mod light_client_server_cache; pub mod metrics; pub mod migrate; mod naive_aggregation_pool; -mod observed_aggregates; +pub mod observed_aggregates; mod observed_attesters; mod observed_blob_sidecars; pub mod observed_block_producers; diff --git a/beacon_node/beacon_chain/src/naive_aggregation_pool.rs b/beacon_node/beacon_chain/src/naive_aggregation_pool.rs index 7eeb9bb56f..211aecfe63 100644 --- a/beacon_node/beacon_chain/src/naive_aggregation_pool.rs +++ b/beacon_node/beacon_chain/src/naive_aggregation_pool.rs @@ -1,14 +1,124 @@ use crate::metrics; +use crate::observed_aggregates::AsReference; +use itertools::Itertools; +use smallvec::SmallVec; use std::collections::HashMap; -use tree_hash::TreeHash; +use tree_hash::{MerkleHasher, TreeHash, TreeHashType}; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; use types::slot_data::SlotData; use types::sync_committee_contribution::SyncContributionData; -use types::{Attestation, AttestationData, EthSpec, Hash256, Slot, SyncCommitteeContribution}; +use types::{ + Attestation, AttestationData, AttestationRef, CommitteeIndex, EthSpec, Hash256, Slot, + SyncCommitteeContribution, +}; -type AttestationDataRoot = Hash256; +type AttestationKeyRoot = Hash256; type SyncDataRoot = Hash256; +/// Post-Electra, we need a new key for Attestations that includes the committee index +#[derive(Debug, Clone, PartialEq)] +pub struct AttestationKey { + data_root: Hash256, + committee_index: Option, + slot: Slot, +} + +// A custom implementation of `TreeHash` such that: +// AttestationKey(data, None).tree_hash_root() == data.tree_hash_root() +// AttestationKey(data, Some(index)).tree_hash_root() == (data, index).tree_hash_root() +// This is necessary because pre-Electra, the validator will ask for the tree_hash_root() +// of the `AttestationData` +impl TreeHash for AttestationKey { + fn tree_hash_type() -> TreeHashType { + TreeHashType::Container + } + + fn tree_hash_packed_encoding(&self) -> SmallVec<[u8; 32]> { + unreachable!("AttestationKey should never be packed.") + } + + fn tree_hash_packing_factor() -> usize { + unreachable!("AttestationKey should never be packed.") + } + + fn tree_hash_root(&self) -> Hash256 { + match self.committee_index { + None => self.data_root, // Return just the data root if no committee index is present + Some(index) => { + // Combine the hash of the data with the hash of the index + let mut hasher = MerkleHasher::with_leaves(2); + hasher + .write(self.data_root.as_bytes()) + .expect("should write data hash"); + hasher + .write(&index.to_le_bytes()) + .expect("should write index"); + hasher.finish().expect("should give tree hash") + } + } + } +} + +impl AttestationKey { + pub fn from_attestation_ref(attestation: AttestationRef) -> Result { + let slot = attestation.data().slot; + match attestation { + AttestationRef::Base(att) => Ok(Self { + data_root: att.data.tree_hash_root(), + committee_index: None, + slot, + }), + AttestationRef::Electra(att) => { + let committee_index = att + .committee_bits + .iter() + .enumerate() + .filter_map(|(i, bit)| if bit { Some(i) } else { None }) + .at_most_one() + .map_err(|_| Error::MoreThanOneCommitteeBitSet)? + .ok_or(Error::NoCommitteeBitSet)?; + + Ok(Self { + data_root: att.data.tree_hash_root(), + committee_index: Some(committee_index as u64), + slot, + }) + } + } + } + + pub fn new_base(data: &AttestationData) -> Self { + let slot = data.slot; + Self { + data_root: data.tree_hash_root(), + committee_index: None, + slot, + } + } + + pub fn new_electra(slot: Slot, data_root: Hash256, committee_index: CommitteeIndex) -> Self { + Self { + data_root, + committee_index: Some(committee_index), + slot, + } + } + + pub fn new_base_from_slot_and_root(slot: Slot, data_root: Hash256) -> Self { + Self { + data_root, + committee_index: None, + slot, + } + } +} + +impl SlotData for AttestationKey { + fn get_slot(&self) -> Slot { + self.slot + } +} + /// The number of slots that will be stored in the pool. /// /// For example, if `SLOTS_RETAINED == 3` and the pool is pruned at slot `6`, then all items @@ -46,6 +156,10 @@ pub enum Error { /// The given `aggregation_bits` field had more than one signature. The number of /// signatures found is included. MoreThanOneAggregationBitSet(usize), + /// The electra attestation has more than one committee bit set + MoreThanOneCommitteeBitSet, + /// The electra attestation has NO committee bit set + NoCommitteeBitSet, /// We have reached the maximum number of unique items that can be stored in a /// slot. This is a DoS protection function. ReachedMaxItemsPerSlot(usize), @@ -59,12 +173,15 @@ pub enum Error { /// Implemented for items in the `NaiveAggregationPool`. Requires that items implement `SlotData`, /// which means they have an associated slot. This handles aggregation of items that are inserted. -pub trait AggregateMap { +pub trait AggregateMap +where + for<'a> ::Reference<'a>: SlotData, +{ /// `Key` should be a hash of `Data`. type Key; /// The item stored in the map - type Value: Clone + SlotData; + type Value: Clone + SlotData + AsReference; /// The unique fields of `Value`, hashed to create `Key`. type Data: SlotData; @@ -73,7 +190,10 @@ pub trait AggregateMap { fn new(initial_capacity: usize) -> Self; /// Insert a `Value` into `Self`, returning a result. - fn insert(&mut self, value: &Self::Value) -> Result; + fn insert( + &mut self, + value: ::Reference<'_>, + ) -> Result; /// Get a `Value` from `Self` based on `Data`. fn get(&self, data: &Self::Data) -> Option; @@ -81,9 +201,6 @@ pub trait AggregateMap { /// Get a reference to the inner `HashMap`. fn get_map(&self) -> &HashMap; - /// Get a `Value` from `Self` based on `Key`, which is a hash of `Data`. - fn get_by_root(&self, root: &Self::Key) -> Option<&Self::Value>; - /// The number of items store in `Self`. fn len(&self) -> usize; @@ -103,13 +220,13 @@ pub trait AggregateMap { /// A collection of `Attestation` objects, keyed by their `attestation.data`. Enforces that all /// `attestation` are from the same slot. pub struct AggregatedAttestationMap { - map: HashMap>, + map: HashMap>, } impl AggregateMap for AggregatedAttestationMap { - type Key = AttestationDataRoot; + type Key = AttestationKeyRoot; type Value = Attestation; - type Data = AttestationData; + type Data = AttestationKey; /// Create an empty collection with the given `initial_capacity`. fn new(initial_capacity: usize) -> Self { @@ -121,48 +238,45 @@ impl AggregateMap for AggregatedAttestationMap { /// Insert an attestation into `self`, aggregating it into the pool. /// /// The given attestation (`a`) must only have one signature. - fn insert(&mut self, a: &Self::Value) -> Result { + fn insert(&mut self, a: AttestationRef) -> Result { let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_CORE_INSERT); - let set_bits = a - .aggregation_bits + let aggregation_bit = *a + .set_aggregation_bits() .iter() - .enumerate() - .filter(|(_i, bit)| *bit) - .map(|(i, _bit)| i) - .collect::>(); - - let committee_index = set_bits - .first() - .copied() + .at_most_one() + .map_err(|iter| Error::MoreThanOneAggregationBitSet(iter.count()))? .ok_or(Error::NoAggregationBitsSet)?; - if set_bits.len() > 1 { - return Err(Error::MoreThanOneAggregationBitSet(set_bits.len())); - } + let attestation_key = AttestationKey::from_attestation_ref(a)?; + let attestation_key_root = attestation_key.tree_hash_root(); - let attestation_data_root = a.data.tree_hash_root(); - - if let Some(existing_attestation) = self.map.get_mut(&attestation_data_root) { + if let Some(existing_attestation) = self.map.get_mut(&attestation_key_root) { if existing_attestation - .aggregation_bits - .get(committee_index) + .get_aggregation_bit(aggregation_bit) .map_err(|_| Error::InconsistentBitfieldLengths)? { - Ok(InsertOutcome::SignatureAlreadyKnown { committee_index }) + Ok(InsertOutcome::SignatureAlreadyKnown { + committee_index: aggregation_bit, + }) } else { let _timer = metrics::start_timer(&metrics::ATTESTATION_PROCESSING_AGG_POOL_AGGREGATION); existing_attestation.aggregate(a); - Ok(InsertOutcome::SignatureAggregated { committee_index }) + Ok(InsertOutcome::SignatureAggregated { + committee_index: aggregation_bit, + }) } } else { if self.map.len() >= MAX_ATTESTATIONS_PER_SLOT { return Err(Error::ReachedMaxItemsPerSlot(MAX_ATTESTATIONS_PER_SLOT)); } - self.map.insert(attestation_data_root, a.clone()); - Ok(InsertOutcome::NewItemInserted { committee_index }) + self.map + .insert(attestation_key_root, a.clone_as_attestation()); + Ok(InsertOutcome::NewItemInserted { + committee_index: aggregation_bit, + }) } } @@ -177,11 +291,6 @@ impl AggregateMap for AggregatedAttestationMap { &self.map } - /// Returns an aggregated `Attestation` with the given `root`, if any. - fn get_by_root(&self, root: &Self::Key) -> Option<&Self::Value> { - self.map.get(root) - } - fn len(&self) -> usize { self.map.len() } @@ -288,11 +397,6 @@ impl AggregateMap for SyncContributionAggregateMap { &self.map } - /// Returns an aggregated `SyncCommitteeContribution` with the given `root`, if any. - fn get_by_root(&self, root: &SyncDataRoot) -> Option<&SyncCommitteeContribution> { - self.map.get(root) - } - fn len(&self) -> usize { self.map.len() } @@ -336,12 +440,20 @@ impl AggregateMap for SyncContributionAggregateMap { /// `current_slot - SLOTS_RETAINED` will be removed and any future item with a slot lower /// than that will also be refused. Pruning is done automatically based upon the items it /// receives and it can be triggered manually. -pub struct NaiveAggregationPool { +pub struct NaiveAggregationPool +where + T: AggregateMap, + for<'a> ::Reference<'a>: SlotData, +{ lowest_permissible_slot: Slot, maps: HashMap, } -impl Default for NaiveAggregationPool { +impl Default for NaiveAggregationPool +where + T: AggregateMap, + for<'a> ::Reference<'a>: SlotData, +{ fn default() -> Self { Self { lowest_permissible_slot: Slot::new(0), @@ -350,7 +462,11 @@ impl Default for NaiveAggregationPool { } } -impl NaiveAggregationPool { +impl NaiveAggregationPool +where + T: AggregateMap, + for<'a> ::Reference<'a>: SlotData, +{ /// Insert an item into `self`, aggregating it into the pool. /// /// The given item must only have one signature and have an @@ -358,7 +474,10 @@ impl NaiveAggregationPool { /// /// The pool may be pruned if the given item has a slot higher than any /// previously seen. - pub fn insert(&mut self, item: &T::Value) -> Result { + pub fn insert( + &mut self, + item: ::Reference<'_>, + ) -> Result { let _timer = T::start_insert_timer(); let slot = item.get_slot(); let lowest_permissible_slot = self.lowest_permissible_slot; @@ -412,13 +531,6 @@ impl NaiveAggregationPool { .and_then(|map| map.get(data)) } - /// Returns an aggregated `T::Value` with the given `slot` and `root`, if any. - pub fn get_by_slot_and_root(&self, slot: Slot, root: &T::Key) -> Option { - self.maps - .get(&slot) - .and_then(|map| map.get_by_root(root).cloned()) - } - /// Iterate all items in all slots of `self`. pub fn iter(&self) -> impl Iterator { self.maps.values().flat_map(|map| map.get_map().values()) @@ -467,18 +579,30 @@ mod tests { use super::*; use ssz_types::BitList; use store::BitVector; + use tree_hash::TreeHash; use types::{ test_utils::{generate_deterministic_keypair, test_random_instance}, - Fork, Hash256, SyncCommitteeMessage, + Attestation, AttestationBase, AttestationElectra, Fork, Hash256, SyncCommitteeMessage, }; type E = types::MainnetEthSpec; - fn get_attestation(slot: Slot) -> Attestation { - let mut a: Attestation = test_random_instance(); + fn get_attestation_base(slot: Slot) -> Attestation { + let mut a: AttestationBase = test_random_instance(); a.data.slot = slot; a.aggregation_bits = BitList::with_capacity(4).expect("should create bitlist"); - a + Attestation::Base(a) + } + + fn get_attestation_electra(slot: Slot) -> Attestation { + let mut a: AttestationElectra = test_random_instance(); + a.data.slot = slot; + a.aggregation_bits = BitList::with_capacity(4).expect("should create bitlist"); + a.committee_bits = BitVector::new(); + a.committee_bits + .set(0, true) + .expect("should set committee bit"); + Attestation::Electra(a) } fn get_sync_contribution(slot: Slot) -> SyncCommitteeContribution { @@ -521,9 +645,16 @@ mod tests { } fn unset_attestation_bit(a: &mut Attestation, i: usize) { - a.aggregation_bits - .set(i, false) - .expect("should unset aggregation bit") + match a { + Attestation::Base(ref mut att) => att + .aggregation_bits + .set(i, false) + .expect("should unset aggregation bit"), + Attestation::Electra(ref mut att) => att + .aggregation_bits + .set(i, false) + .expect("should unset aggregation bit"), + } } fn unset_sync_contribution_bit(a: &mut SyncCommitteeContribution, i: usize) { @@ -533,19 +664,19 @@ mod tests { } fn mutate_attestation_block_root(a: &mut Attestation, block_root: Hash256) { - a.data.beacon_block_root = block_root + a.data_mut().beacon_block_root = block_root } fn mutate_attestation_slot(a: &mut Attestation, slot: Slot) { - a.data.slot = slot + a.data_mut().slot = slot } fn attestation_block_root_comparator(a: &Attestation, block_root: Hash256) -> bool { - a.data.beacon_block_root == block_root + a.data().beacon_block_root == block_root } - fn key_from_attestation(a: &Attestation) -> AttestationData { - a.data.clone() + fn key_from_attestation(a: &Attestation) -> AttestationKey { + AttestationKey::from_attestation_ref(a.to_ref()).expect("should create attestation key") } fn mutate_sync_contribution_block_root( @@ -570,6 +701,45 @@ mod tests { SyncContributionData::from_contribution(a) } + #[test] + fn attestation_key_tree_hash_tests() { + let attestation_base = get_attestation_base(Slot::new(42)); + // for a base attestation, the tree_hash_root() of the key should be the same as the tree_hash_root() of the data + let attestation_key_base = AttestationKey::from_attestation_ref(attestation_base.to_ref()) + .expect("should create attestation key"); + assert_eq!( + attestation_key_base.tree_hash_root(), + attestation_base.data().tree_hash_root() + ); + let mut attestation_electra = get_attestation_electra(Slot::new(42)); + // for an electra attestation, the tree_hash_root() of the key should be different from the tree_hash_root() of the data + let attestation_key_electra = + AttestationKey::from_attestation_ref(attestation_electra.to_ref()) + .expect("should create attestation key"); + assert_ne!( + attestation_key_electra.tree_hash_root(), + attestation_electra.data().tree_hash_root() + ); + // for an electra attestation, the tree_hash_root() of the key should be dependent on which committee bit is set + let committe_bits = attestation_electra + .committee_bits_mut() + .expect("should get committee bits"); + committe_bits + .set(0, false) + .expect("should set committee bit"); + committe_bits + .set(1, true) + .expect("should set committee bit"); + let new_attestation_key_electra = + AttestationKey::from_attestation_ref(attestation_electra.to_ref()) + .expect("should create attestation key"); + // this new key should have a different tree_hash_root() than the previous key + assert_ne!( + attestation_key_electra.tree_hash_root(), + new_attestation_key_electra.tree_hash_root() + ); + } + macro_rules! test_suite { ( $mod_name: ident, @@ -592,10 +762,10 @@ mod tests { let mut a = $get_method_name(Slot::new(0)); let mut pool: NaiveAggregationPool<$map_type> = - NaiveAggregationPool::default(); + NaiveAggregationPool::<$map_type>::default(); assert_eq!( - pool.insert(&a), + pool.insert(a.as_reference()), Err(Error::NoAggregationBitsSet), "should not accept item without any signatures" ); @@ -603,12 +773,12 @@ mod tests { $sign_method_name(&mut a, 0, Hash256::random()); assert_eq!( - pool.insert(&a), + pool.insert(a.as_reference()), Ok(InsertOutcome::NewItemInserted { committee_index: 0 }), "should accept new item" ); assert_eq!( - pool.insert(&a), + pool.insert(a.as_reference()), Ok(InsertOutcome::SignatureAlreadyKnown { committee_index: 0 }), "should acknowledge duplicate signature" ); @@ -621,7 +791,7 @@ mod tests { $sign_method_name(&mut a, 1, Hash256::random()); assert_eq!( - pool.insert(&a), + pool.insert(a.as_reference()), Err(Error::MoreThanOneAggregationBitSet(2)), "should not accept item with multiple signatures" ); @@ -637,15 +807,15 @@ mod tests { $sign_method_name(&mut a_1, 1, genesis_validators_root); let mut pool: NaiveAggregationPool<$map_type> = - NaiveAggregationPool::default(); + NaiveAggregationPool::<$map_type>::default(); assert_eq!( - pool.insert(&a_0), + pool.insert(a_0.as_reference()), Ok(InsertOutcome::NewItemInserted { committee_index: 0 }), "should accept a_0" ); assert_eq!( - pool.insert(&a_1), + pool.insert(a_1.as_reference()), Ok(InsertOutcome::SignatureAggregated { committee_index: 1 }), "should accept a_1" ); @@ -655,7 +825,7 @@ mod tests { .expect("should not error while getting attestation"); let mut a_01 = a_0.clone(); - a_01.aggregate(&a_1); + a_01.aggregate(a_1.as_reference()); assert_eq!(retrieved, a_01, "retrieved item should be aggregated"); @@ -671,7 +841,7 @@ mod tests { $block_root_mutator(&mut a_different, different_root); assert_eq!( - pool.insert(&a_different), + pool.insert(a_different.as_reference()), Ok(InsertOutcome::NewItemInserted { committee_index: 2 }), "should accept a_different" ); @@ -690,7 +860,7 @@ mod tests { $sign_method_name(&mut base, 0, Hash256::random()); let mut pool: NaiveAggregationPool<$map_type> = - NaiveAggregationPool::default(); + NaiveAggregationPool::<$map_type>::default(); for i in 0..SLOTS_RETAINED * 2 { let slot = Slot::from(i); @@ -698,7 +868,7 @@ mod tests { $slot_mutator(&mut a, slot); assert_eq!( - pool.insert(&a), + pool.insert(a.as_reference()), Ok(InsertOutcome::NewItemInserted { committee_index: 0 }), "should accept new item" ); @@ -739,7 +909,7 @@ mod tests { $sign_method_name(&mut base, 0, Hash256::random()); let mut pool: NaiveAggregationPool<$map_type> = - NaiveAggregationPool::default(); + NaiveAggregationPool::<$map_type>::default(); for i in 0..=$item_limit { let mut a = base.clone(); @@ -747,13 +917,13 @@ mod tests { if i < $item_limit { assert_eq!( - pool.insert(&a), + pool.insert(a.as_reference()), Ok(InsertOutcome::NewItemInserted { committee_index: 0 }), "should accept item below limit" ); } else { assert_eq!( - pool.insert(&a), + pool.insert(a.as_reference()), Err(Error::ReachedMaxItemsPerSlot($item_limit)), "should not accept item above limit" ); @@ -765,8 +935,21 @@ mod tests { } test_suite! { - attestation_tests, - get_attestation, + attestation_tests_base, + get_attestation_base, + sign_attestation, + unset_attestation_bit, + mutate_attestation_block_root, + mutate_attestation_slot, + attestation_block_root_comparator, + key_from_attestation, + AggregatedAttestationMap, + MAX_ATTESTATIONS_PER_SLOT + } + + test_suite! { + attestation_tests_electra, + get_attestation_electra, sign_attestation, unset_attestation_bit, mutate_attestation_block_root, diff --git a/beacon_node/beacon_chain/src/observed_aggregates.rs b/beacon_node/beacon_chain/src/observed_aggregates.rs index ab00aefcd3..00476bfe7a 100644 --- a/beacon_node/beacon_chain/src/observed_aggregates.rs +++ b/beacon_node/beacon_chain/src/observed_aggregates.rs @@ -6,22 +6,33 @@ use ssz_types::{BitList, BitVector}; use std::collections::HashMap; use std::marker::PhantomData; use tree_hash::TreeHash; +use tree_hash_derive::TreeHash; use types::consts::altair::{ SYNC_COMMITTEE_SUBNET_COUNT, TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE, }; use types::slot_data::SlotData; -use types::{Attestation, EthSpec, Hash256, Slot, SyncCommitteeContribution}; +use types::{ + Attestation, AttestationData, AttestationRef, EthSpec, Hash256, Slot, SyncCommitteeContribution, +}; pub type ObservedSyncContributions = ObservedAggregates< SyncCommitteeContribution, E, BitVector<::SyncSubcommitteeSize>, >; -pub type ObservedAggregateAttestations = ObservedAggregates< - Attestation, - E, - BitList<::MaxValidatorsPerCommittee>, ->; +pub type ObservedAggregateAttestations = + ObservedAggregates, E, BitList<::MaxValidatorsPerSlot>>; + +/// Attestation data augmented with committee index +/// +/// This is hashed and used to key the map of observed aggregate attestations. This is important +/// post-Electra where the attestation data committee index is 0 and we want to avoid accidentally +/// comparing aggregation bits for *different* committees. +#[derive(TreeHash)] +pub struct ObservedAttestationKey { + pub committee_index: u64, + pub attestation_data: AttestationData, +} /// A trait use to associate capacity constants with the type being stored in `ObservedAggregates`. pub trait Consts { @@ -95,35 +106,60 @@ pub trait SubsetItem { /// Returns the item that gets stored in `ObservedAggregates` for later subset /// comparison with incoming aggregates. - fn get_item(&self) -> Self::Item; + fn get_item(&self) -> Result; /// Returns a unique value that keys the object to the item that is being stored /// in `ObservedAggregates`. - fn root(&self) -> Hash256; + fn root(&self) -> Result; } -impl SubsetItem for Attestation { - type Item = BitList; +impl<'a, E: EthSpec> SubsetItem for AttestationRef<'a, E> { + type Item = BitList; fn is_subset(&self, other: &Self::Item) -> bool { - self.aggregation_bits.is_subset(other) + match self { + Self::Base(att) => { + if let Ok(extended_aggregation_bits) = att.extend_aggregation_bits() { + return extended_aggregation_bits.is_subset(other); + } + false + } + Self::Electra(att) => att.aggregation_bits.is_subset(other), + } } fn is_superset(&self, other: &Self::Item) -> bool { - other.is_subset(&self.aggregation_bits) + match self { + Self::Base(att) => { + if let Ok(extended_aggregation_bits) = att.extend_aggregation_bits() { + return other.is_subset(&extended_aggregation_bits); + } + false + } + Self::Electra(att) => other.is_subset(&att.aggregation_bits), + } } /// Returns the sync contribution aggregation bits. - fn get_item(&self) -> Self::Item { - self.aggregation_bits.clone() + fn get_item(&self) -> Result { + match self { + Self::Base(att) => att + .extend_aggregation_bits() + .map_err(|_| Error::GetItemError), + Self::Electra(att) => Ok(att.aggregation_bits.clone()), + } } - /// Returns the hash tree root of the attestation data. - fn root(&self) -> Hash256 { - self.data.tree_hash_root() + /// Returns the hash tree root of the attestation data augmented with the committee index. + fn root(&self) -> Result { + Ok(ObservedAttestationKey { + committee_index: self.committee_index().ok_or(Error::RootError)?, + attestation_data: self.data().clone(), + } + .tree_hash_root()) } } -impl SubsetItem for SyncCommitteeContribution { +impl<'a, E: EthSpec> SubsetItem for &'a SyncCommitteeContribution { type Item = BitVector; fn is_subset(&self, other: &Self::Item) -> bool { self.aggregation_bits.is_subset(other) @@ -134,19 +170,19 @@ impl SubsetItem for SyncCommitteeContribution { } /// Returns the sync contribution aggregation bits. - fn get_item(&self) -> Self::Item { - self.aggregation_bits.clone() + fn get_item(&self) -> Result { + Ok(self.aggregation_bits.clone()) } /// Returns the hash tree root of the root, slot and subcommittee index /// of the sync contribution. - fn root(&self) -> Hash256 { - SyncCommitteeData { + fn root(&self) -> Result { + Ok(SyncCommitteeData { root: self.beacon_block_root, slot: self.slot, subcommittee_index: self.subcommittee_index, } - .tree_hash_root() + .tree_hash_root()) } } @@ -173,6 +209,8 @@ pub enum Error { expected: Slot, attestation: Slot, }, + GetItemError, + RootError, } /// A `HashMap` that contains entries related to some `Slot`. @@ -196,7 +234,7 @@ impl SlotHashSet { /// Store the items in self so future observations recognise its existence. pub fn observe_item>( &mut self, - item: &S, + item: S, root: Hash256, ) -> Result { if item.get_slot() != self.slot { @@ -215,7 +253,7 @@ impl SlotHashSet { // If true, we replace the new item with its existing subset. This allows us // to hold fewer items in the list. } else if item.is_superset(existing) { - *existing = item.get_item(); + *existing = item.get_item()?; return Ok(ObserveOutcome::New); } } @@ -233,7 +271,7 @@ impl SlotHashSet { return Err(Error::ReachedMaxObservationsPerSlot(self.max_capacity)); } - let item = item.get_item(); + let item = item.get_item()?; self.map.entry(root).or_default().push(item); Ok(ObserveOutcome::New) } @@ -242,7 +280,7 @@ impl SlotHashSet { /// the given root and slot. pub fn is_known_subset>( &self, - item: &S, + item: S, root: Hash256, ) -> Result { if item.get_slot() != self.slot { @@ -264,16 +302,43 @@ impl SlotHashSet { } } +/// Trait for observable items that can be observed from their reference type. +/// +/// This is used to make observations for `Attestation`s from `AttestationRef`s. +pub trait AsReference { + type Reference<'a> + where + Self: 'a; + + fn as_reference(&self) -> Self::Reference<'_>; +} + +impl AsReference for Attestation { + type Reference<'a> = AttestationRef<'a, E>; + + fn as_reference(&self) -> AttestationRef<'_, E> { + self.to_ref() + } +} + +impl AsReference for SyncCommitteeContribution { + type Reference<'a> = &'a Self; + + fn as_reference(&self) -> &Self { + self + } +} + /// Stores the roots of objects for some number of `Slots`, so we can determine if /// these have previously been seen on the network. -pub struct ObservedAggregates { +pub struct ObservedAggregates { lowest_permissible_slot: Slot, sets: Vec>, _phantom_spec: PhantomData, _phantom_tree_hash: PhantomData, } -impl Default for ObservedAggregates { +impl Default for ObservedAggregates { fn default() -> Self { Self { lowest_permissible_slot: Slot::new(0), @@ -284,17 +349,22 @@ impl Default for ObservedAggregates, E: EthSpec, I> ObservedAggregates { +impl ObservedAggregates +where + T: Consts + AsReference, + E: EthSpec, + for<'a> T::Reference<'a>: SubsetItem + SlotData, +{ /// Store `item` in `self` keyed at `root`. /// /// `root` must equal `item.root::()`. pub fn observe_item( &mut self, - item: &T, + item: T::Reference<'_>, root_opt: Option, ) -> Result { let index = self.get_set_index(item.get_slot())?; - let root = root_opt.unwrap_or_else(|| item.root()); + let root = root_opt.map_or_else(|| item.root(), Ok)?; self.sets .get_mut(index) @@ -307,7 +377,11 @@ impl, E: EthSpec, I> ObservedAggrega /// /// `root` must equal `item.root::()`. #[allow(clippy::wrong_self_convention)] - pub fn is_known_subset(&mut self, item: &T, root: Hash256) -> Result { + pub fn is_known_subset( + &mut self, + item: T::Reference<'_>, + root: Hash256, + ) -> Result { let index = self.get_set_index(item.get_slot())?; self.sets @@ -399,14 +473,15 @@ impl, E: EthSpec, I> ObservedAggrega #[cfg(not(debug_assertions))] mod tests { use super::*; - use types::{test_utils::test_random_instance, Hash256}; + use types::{test_utils::test_random_instance, AttestationBase, Hash256}; type E = types::MainnetEthSpec; fn get_attestation(slot: Slot, beacon_block_root: u64) -> Attestation { - let mut a: Attestation = test_random_instance(); - a.data.slot = slot; - a.data.beacon_block_root = Hash256::from_low_u64_be(beacon_block_root); + let a: AttestationBase = test_random_instance(); + let mut a = Attestation::Base(a); + a.data_mut().slot = slot; + a.data_mut().beacon_block_root = Hash256::from_low_u64_be(beacon_block_root); a } @@ -432,12 +507,15 @@ mod tests { for a in &items { assert_eq!( - store.is_known_subset(a, a.root()), + store.is_known_subset( + a.as_reference(), + a.as_reference().root().unwrap() + ), Ok(false), "should indicate an unknown attestation is unknown" ); assert_eq!( - store.observe_item(a, None), + store.observe_item(a.as_reference(), None), Ok(ObserveOutcome::New), "should observe new attestation" ); @@ -445,12 +523,18 @@ mod tests { for a in &items { assert_eq!( - store.is_known_subset(a, a.root()), + store.is_known_subset( + a.as_reference(), + a.as_reference().root().unwrap() + ), Ok(true), "should indicate a known attestation is known" ); assert_eq!( - store.observe_item(a, Some(a.root())), + store.observe_item( + a.as_reference(), + Some(a.as_reference().root().unwrap()) + ), Ok(ObserveOutcome::Subset), "should acknowledge an existing attestation" ); diff --git a/beacon_node/beacon_chain/src/observed_operations.rs b/beacon_node/beacon_chain/src/observed_operations.rs index 04861fbe31..969d03a11b 100644 --- a/beacon_node/beacon_chain/src/observed_operations.rs +++ b/beacon_node/beacon_chain/src/observed_operations.rs @@ -1,7 +1,6 @@ use derivative::Derivative; use smallvec::{smallvec, SmallVec}; -use ssz::{Decode, Encode}; -use state_processing::{SigVerifiedOp, VerifyOperation, VerifyOperationAt}; +use state_processing::{SigVerifiedOp, TransformPersist, VerifyOperation, VerifyOperationAt}; use std::collections::HashSet; use std::marker::PhantomData; use types::{ @@ -34,7 +33,7 @@ pub struct ObservedOperations, E: EthSpec> { /// Was the observed operation new and valid for further processing, or a useless duplicate? #[derive(Debug, PartialEq, Eq, Clone)] -pub enum ObservationOutcome { +pub enum ObservationOutcome { New(SigVerifiedOp), AlreadyKnown, } @@ -62,15 +61,13 @@ impl ObservableOperation for ProposerSlashing { impl ObservableOperation for AttesterSlashing { fn observed_validators(&self) -> SmallVec<[u64; SMALL_VEC_SIZE]> { let attestation_1_indices = self - .attestation_1 - .attesting_indices - .iter() + .attestation_1() + .attesting_indices_iter() .copied() .collect::>(); let attestation_2_indices = self - .attestation_2 - .attesting_indices - .iter() + .attestation_2() + .attesting_indices_iter() .copied() .collect::>(); attestation_1_indices diff --git a/beacon_node/beacon_chain/src/schema_change.rs b/beacon_node/beacon_chain/src/schema_change.rs index 63eb72c43a..06d189a8c0 100644 --- a/beacon_node/beacon_chain/src/schema_change.rs +++ b/beacon_node/beacon_chain/src/schema_change.rs @@ -2,6 +2,7 @@ mod migration_schema_v17; mod migration_schema_v18; mod migration_schema_v19; +mod migration_schema_v20; use crate::beacon_chain::BeaconChainTypes; use crate::types::ChainSpec; @@ -78,6 +79,14 @@ pub fn migrate_schema( let ops = migration_schema_v19::downgrade_from_v19::(db.clone(), log)?; db.store_schema_version_atomically(to, ops) } + (SchemaVersion(19), SchemaVersion(20)) => { + let ops = migration_schema_v20::upgrade_to_v20::(db.clone(), log)?; + db.store_schema_version_atomically(to, ops) + } + (SchemaVersion(20), SchemaVersion(19)) => { + let ops = migration_schema_v20::downgrade_from_v20::(db.clone(), log)?; + db.store_schema_version_atomically(to, ops) + } // Anything else is an error. (_, _) => Err(HotColdDBError::UnsupportedSchemaVersion { target_version: to, diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs new file mode 100644 index 0000000000..737fcd0a93 --- /dev/null +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v20.rs @@ -0,0 +1,103 @@ +use crate::beacon_chain::{BeaconChainTypes, OP_POOL_DB_KEY}; +use operation_pool::{ + PersistedOperationPool, PersistedOperationPoolV15, PersistedOperationPoolV20, +}; +use slog::{debug, info, Logger}; +use std::sync::Arc; +use store::{Error, HotColdDB, KeyValueStoreOp, StoreItem}; +use types::Attestation; + +pub fn upgrade_to_v20( + db: Arc>, + log: Logger, +) -> Result, Error> { + // Load a V15 op pool and transform it to V20. + let Some(PersistedOperationPoolV15:: { + attestations_v15, + sync_contributions, + attester_slashings_v15, + proposer_slashings, + voluntary_exits, + bls_to_execution_changes, + capella_bls_change_broadcast_indices, + }) = db.get_item(&OP_POOL_DB_KEY)? + else { + debug!(log, "Nothing to do, no operation pool stored"); + return Ok(vec![]); + }; + + let attestations = attestations_v15 + .into_iter() + .map(|(attestation, indices)| (Attestation::Base(attestation).into(), indices)) + .collect(); + + let attester_slashings = attester_slashings_v15 + .into_iter() + .map(|slashing| slashing.into()) + .collect(); + + let v20 = PersistedOperationPool::V20(PersistedOperationPoolV20 { + attestations, + sync_contributions, + attester_slashings, + proposer_slashings, + voluntary_exits, + bls_to_execution_changes, + capella_bls_change_broadcast_indices, + }); + Ok(vec![v20.as_kv_store_op(OP_POOL_DB_KEY)]) +} + +pub fn downgrade_from_v20( + db: Arc>, + log: Logger, +) -> Result, Error> { + // Load a V20 op pool and transform it to V15. + let Some(PersistedOperationPoolV20:: { + attestations, + sync_contributions, + attester_slashings, + proposer_slashings, + voluntary_exits, + bls_to_execution_changes, + capella_bls_change_broadcast_indices, + }) = db.get_item(&OP_POOL_DB_KEY)? + else { + debug!(log, "Nothing to do, no operation pool stored"); + return Ok(vec![]); + }; + + let attestations_v15 = attestations + .into_iter() + .filter_map(|(attestation, indices)| { + if let Attestation::Base(attestation) = attestation.into() { + Some((attestation, indices)) + } else { + info!(log, "Dropping attestation during downgrade"; "reason" => "not a base attestation"); + None + } + }) + .collect(); + + let attester_slashings_v15 = attester_slashings + .into_iter() + .filter_map(|slashing| match slashing.try_into() { + Ok(slashing) => Some(slashing), + Err(_) => { + info!(log, "Dropping attester slashing during downgrade"; "reason" => "not a base attester slashing"); + None + } + }) + .collect(); + + let v15 = PersistedOperationPool::V15(PersistedOperationPoolV15 { + attestations_v15, + sync_contributions, + attester_slashings_v15, + proposer_slashings, + voluntary_exits, + bls_to_execution_changes, + capella_bls_change_broadcast_indices, + }); + Ok(vec![v15.as_kv_store_op(OP_POOL_DB_KEY)]) +} diff --git a/beacon_node/beacon_chain/src/test_utils.rs b/beacon_node/beacon_chain/src/test_utils.rs index 465aa70782..bd98f19af6 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -58,6 +58,7 @@ use store::{config::StoreConfig, HotColdDB, ItemStore, LevelDB, MemoryStore}; use task_executor::TaskExecutor; use task_executor::{test_utils::TestRuntime, ShutdownReason}; use tree_hash::TreeHash; +use types::indexed_attestation::IndexedAttestationBase; use types::payload::BlockProductionVersion; pub use types::test_utils::generate_deterministic_keypairs; use types::test_utils::TestRandom; @@ -1031,20 +1032,18 @@ where *state.get_block_root(target_slot)? }; - Ok(Attestation { - aggregation_bits: BitList::with_capacity(committee_len)?, - data: AttestationData { - slot, - index, - beacon_block_root, - source: state.current_justified_checkpoint(), - target: Checkpoint { - epoch, - root: target_root, - }, + Ok(Attestation::empty_for_signing( + index, + committee_len, + slot, + beacon_block_root, + state.current_justified_checkpoint(), + Checkpoint { + epoch, + root: target_root, }, - signature: AggregateSignature::empty(), - }) + &self.spec, + )?) } /// A list of attestations for each committee for the given slot. @@ -1119,17 +1118,24 @@ where ) .unwrap(); - attestation.aggregation_bits.set(i, true).unwrap(); + match attestation { + Attestation::Base(ref mut att) => { + att.aggregation_bits.set(i, true).unwrap() + } + Attestation::Electra(ref mut att) => { + att.aggregation_bits.set(i, true).unwrap() + } + } - attestation.signature = { + *attestation.signature_mut() = { let domain = self.spec.get_domain( - attestation.data.target.epoch, + attestation.data().target.epoch, Domain::BeaconAttester, &fork, state.genesis_validators_root(), ); - let message = attestation.data.signing_root(domain); + let message = attestation.data().signing_root(domain); let mut agg_sig = AggregateSignature::infinity(); @@ -1140,8 +1146,8 @@ where agg_sig }; - let subnet_id = SubnetId::compute_subnet_for_attestation_data::( - &attestation.data, + let subnet_id = SubnetId::compute_subnet_for_attestation::( + attestation.to_ref(), committee_count, &self.chain.spec, ) @@ -1315,7 +1321,10 @@ where // If there are any attestations in this committee, create an aggregate. if let Some((attestation, _)) = committee_attestations.first() { let bc = state - .get_beacon_committee(attestation.data.slot, attestation.data.index) + .get_beacon_committee( + attestation.data().slot, + attestation.committee_index().unwrap(), + ) .unwrap(); // Find an aggregator if one exists. Return `None` if there are no @@ -1342,25 +1351,35 @@ where }) .copied()?; + let fork_name = self.spec.fork_name_at_slot::(slot); + + let aggregate = if fork_name.electra_enabled() { + self.chain.get_aggregated_attestation_electra( + slot, + &attestation.data().tree_hash_root(), + bc.index, + ) + } else { + self.chain + .get_aggregated_attestation_base(attestation.data()) + } + .unwrap() + .unwrap_or_else(|| { + committee_attestations.iter().skip(1).fold( + attestation.clone(), + |mut agg, (att, _)| { + agg.aggregate(att.to_ref()); + agg + }, + ) + }); + // If the chain is able to produce an aggregate, use that. Otherwise, build an // aggregate locally. - let aggregate = self - .chain - .get_aggregated_attestation(&attestation.data) - .unwrap() - .unwrap_or_else(|| { - committee_attestations.iter().skip(1).fold( - attestation.clone(), - |mut agg, (att, _)| { - agg.aggregate(att); - agg - }, - ) - }); let signed_aggregate = SignedAggregateAndProof::from_aggregate( aggregator_index as u64, - aggregate, + aggregate.to_ref(), None, &self.validator_keypairs[aggregator_index].sk, &fork, @@ -1486,50 +1505,89 @@ where ) -> AttesterSlashing { let fork = self.chain.canonical_head.cached_head().head_fork(); - let mut attestation_1 = IndexedAttestation { - attesting_indices: VariableList::new(validator_indices).unwrap(), - data: AttestationData { - slot: Slot::new(0), - index: 0, - beacon_block_root: Hash256::zero(), - target: Checkpoint { - root: Hash256::zero(), - epoch: target1.unwrap_or(fork.epoch), - }, - source: Checkpoint { - root: Hash256::zero(), - epoch: source1.unwrap_or(Epoch::new(0)), - }, + let fork_name = self.spec.fork_name_at_slot::(Slot::new(0)); + + let data = AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + target: Checkpoint { + root: Hash256::zero(), + epoch: target1.unwrap_or(fork.epoch), }, - signature: AggregateSignature::infinity(), + source: Checkpoint { + root: Hash256::zero(), + epoch: source1.unwrap_or(Epoch::new(0)), + }, + }; + let mut attestation_1 = if fork_name.electra_enabled() { + IndexedAttestation::Electra(IndexedAttestationElectra { + attesting_indices: VariableList::new(validator_indices).unwrap(), + data, + signature: AggregateSignature::infinity(), + }) + } else { + IndexedAttestation::Base(IndexedAttestationBase { + attesting_indices: VariableList::new(validator_indices).unwrap(), + data, + signature: AggregateSignature::infinity(), + }) }; let mut attestation_2 = attestation_1.clone(); - attestation_2.data.index += 1; - attestation_2.data.source.epoch = source2.unwrap_or(Epoch::new(0)); - attestation_2.data.target.epoch = target2.unwrap_or(fork.epoch); + attestation_2.data_mut().index += 1; + attestation_2.data_mut().source.epoch = source2.unwrap_or(Epoch::new(0)); + attestation_2.data_mut().target.epoch = target2.unwrap_or(fork.epoch); for attestation in &mut [&mut attestation_1, &mut attestation_2] { - for &i in &attestation.attesting_indices { - let sk = &self.validator_keypairs[i as usize].sk; + match attestation { + IndexedAttestation::Base(attestation) => { + for i in attestation.attesting_indices.iter() { + let sk = &self.validator_keypairs[*i as usize].sk; - let genesis_validators_root = self.chain.genesis_validators_root; + let genesis_validators_root = self.chain.genesis_validators_root; - let domain = self.chain.spec.get_domain( - attestation.data.target.epoch, - Domain::BeaconAttester, - &fork, - genesis_validators_root, - ); - let message = attestation.data.signing_root(domain); + let domain = self.chain.spec.get_domain( + attestation.data.target.epoch, + Domain::BeaconAttester, + &fork, + genesis_validators_root, + ); + let message = attestation.data.signing_root(domain); - attestation.signature.add_assign(&sk.sign(message)); + attestation.signature.add_assign(&sk.sign(message)); + } + } + IndexedAttestation::Electra(attestation) => { + for i in attestation.attesting_indices.iter() { + let sk = &self.validator_keypairs[*i as usize].sk; + + let genesis_validators_root = self.chain.genesis_validators_root; + + let domain = self.chain.spec.get_domain( + attestation.data.target.epoch, + Domain::BeaconAttester, + &fork, + genesis_validators_root, + ); + let message = attestation.data.signing_root(domain); + + attestation.signature.add_assign(&sk.sign(message)); + } + } } } - AttesterSlashing { - attestation_1, - attestation_2, + if fork_name.electra_enabled() { + AttesterSlashing::Electra(AttesterSlashingElectra { + attestation_1: attestation_1.as_electra().unwrap().clone(), + attestation_2: attestation_2.as_electra().unwrap().clone(), + }) + } else { + AttesterSlashing::Base(AttesterSlashingBase { + attestation_1: attestation_1.as_base().unwrap().clone(), + attestation_2: attestation_2.as_base().unwrap().clone(), + }) } } @@ -1538,6 +1596,8 @@ where validator_indices_1: Vec, validator_indices_2: Vec, ) -> AttesterSlashing { + let fork_name = self.spec.fork_name_at_slot::(Slot::new(0)); + let data = AttestationData { slot: Slot::new(0), index: 0, @@ -1552,42 +1612,94 @@ where }, }; - let mut attestation_1 = IndexedAttestation { - attesting_indices: VariableList::new(validator_indices_1).unwrap(), - data: data.clone(), - signature: AggregateSignature::infinity(), + let (mut attestation_1, mut attestation_2) = if fork_name.electra_enabled() { + let attestation_1 = IndexedAttestationElectra { + attesting_indices: VariableList::new(validator_indices_1).unwrap(), + data: data.clone(), + signature: AggregateSignature::infinity(), + }; + + let attestation_2 = IndexedAttestationElectra { + attesting_indices: VariableList::new(validator_indices_2).unwrap(), + data, + signature: AggregateSignature::infinity(), + }; + + ( + IndexedAttestation::Electra(attestation_1), + IndexedAttestation::Electra(attestation_2), + ) + } else { + let attestation_1 = IndexedAttestationBase { + attesting_indices: VariableList::new(validator_indices_1).unwrap(), + data: data.clone(), + signature: AggregateSignature::infinity(), + }; + + let attestation_2 = IndexedAttestationBase { + attesting_indices: VariableList::new(validator_indices_2).unwrap(), + data, + signature: AggregateSignature::infinity(), + }; + + ( + IndexedAttestation::Base(attestation_1), + IndexedAttestation::Base(attestation_2), + ) }; - let mut attestation_2 = IndexedAttestation { - attesting_indices: VariableList::new(validator_indices_2).unwrap(), - data, - signature: AggregateSignature::infinity(), - }; - - attestation_2.data.index += 1; + attestation_2.data_mut().index += 1; let fork = self.chain.canonical_head.cached_head().head_fork(); for attestation in &mut [&mut attestation_1, &mut attestation_2] { - for &i in &attestation.attesting_indices { - let sk = &self.validator_keypairs[i as usize].sk; + match attestation { + IndexedAttestation::Base(attestation) => { + for i in attestation.attesting_indices.iter() { + let sk = &self.validator_keypairs[*i as usize].sk; - let genesis_validators_root = self.chain.genesis_validators_root; + let genesis_validators_root = self.chain.genesis_validators_root; - let domain = self.chain.spec.get_domain( - attestation.data.target.epoch, - Domain::BeaconAttester, - &fork, - genesis_validators_root, - ); - let message = attestation.data.signing_root(domain); + let domain = self.chain.spec.get_domain( + attestation.data.target.epoch, + Domain::BeaconAttester, + &fork, + genesis_validators_root, + ); + let message = attestation.data.signing_root(domain); - attestation.signature.add_assign(&sk.sign(message)); + attestation.signature.add_assign(&sk.sign(message)); + } + } + IndexedAttestation::Electra(attestation) => { + for i in attestation.attesting_indices.iter() { + let sk = &self.validator_keypairs[*i as usize].sk; + + let genesis_validators_root = self.chain.genesis_validators_root; + + let domain = self.chain.spec.get_domain( + attestation.data.target.epoch, + Domain::BeaconAttester, + &fork, + genesis_validators_root, + ); + let message = attestation.data.signing_root(domain); + + attestation.signature.add_assign(&sk.sign(message)); + } + } } } - AttesterSlashing { - attestation_1, - attestation_2, + if fork_name.electra_enabled() { + AttesterSlashing::Electra(AttesterSlashingElectra { + attestation_1: attestation_1.as_electra().unwrap().clone(), + attestation_2: attestation_2.as_electra().unwrap().clone(), + }) + } else { + AttesterSlashing::Base(AttesterSlashingBase { + attestation_1: attestation_1.as_base().unwrap().clone(), + attestation_2: attestation_2.as_base().unwrap().clone(), + }) } } @@ -2380,6 +2492,7 @@ where AttestationStrategy::AllValidators => self.get_all_validators(), AttestationStrategy::SomeValidators(vals) => vals, }; + let state_root = state.update_tree_hash_cache().unwrap(); let (_, _, last_produced_block_hash, _) = self .add_attested_blocks_at_slots_with_sync( @@ -2506,6 +2619,7 @@ pub fn generate_rand_block_and_blobs( rng: &mut impl Rng, ) -> (SignedBeaconBlock>, Vec>) { let inner = map_fork_name!(fork_name, BeaconBlock, <_>::random_for_test(rng)); + let mut block = SignedBeaconBlock::from_block(inner, types::Signature::random_for_test(rng)); let mut blob_sidecars = vec![]; @@ -2542,7 +2656,6 @@ pub fn generate_rand_block_and_blobs( }; let (bundle, transactions) = execution_layer::test_utils::generate_blobs::(num_blobs).unwrap(); - payload.execution_payload.transactions = <_>::default(); for tx in Vec::from(transactions) { payload.execution_payload.transactions.push(tx).unwrap(); diff --git a/beacon_node/beacon_chain/src/validator_monitor.rs b/beacon_node/beacon_chain/src/validator_monitor.rs index f6cd5e857e..4ce5383819 100644 --- a/beacon_node/beacon_chain/src/validator_monitor.rs +++ b/beacon_node/beacon_chain/src/validator_monitor.rs @@ -25,9 +25,10 @@ use types::consts::altair::{ TIMELY_HEAD_FLAG_INDEX, TIMELY_SOURCE_FLAG_INDEX, TIMELY_TARGET_FLAG_INDEX, }; use types::{ - Attestation, AttestationData, AttesterSlashing, BeaconBlockRef, BeaconState, BeaconStateError, - ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, ProposerSlashing, PublicKeyBytes, - SignedAggregateAndProof, SignedContributionAndProof, Slot, SyncCommitteeMessage, VoluntaryExit, + Attestation, AttestationData, AttesterSlashingRef, BeaconBlockRef, BeaconState, + BeaconStateError, ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, + IndexedAttestationRef, ProposerSlashing, PublicKeyBytes, SignedAggregateAndProof, + SignedContributionAndProof, Slot, SyncCommitteeMessage, VoluntaryExit, }; /// Used for Prometheus labels. @@ -469,7 +470,7 @@ impl ValidatorMonitor { unaggregated_attestations.remove(&oldest_slot); } } - let slot = attestation.data.slot; + let slot = attestation.data().slot; self.unaggregated_attestations.insert(slot, attestation); } @@ -730,12 +731,12 @@ impl ValidatorMonitor { // that qualifies the committee index for reward is included let inclusion_delay = spec.min_attestation_inclusion_delay; - let data = &unaggregated_attestation.data; + let data = unaggregated_attestation.data(); // Get the reward indices for the unaggregated attestation or log an error match get_attestation_participation_flag_indices( state, - &unaggregated_attestation.data, + unaggregated_attestation.data(), inclusion_delay, spec, ) { @@ -1233,7 +1234,7 @@ impl ValidatorMonitor { indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { - let data = &indexed_attestation.data; + let data = indexed_attestation.data(); let epoch = data.slot.epoch(E::slots_per_epoch()); let delay = get_message_delay_ms( seen_timestamp, @@ -1242,7 +1243,7 @@ impl ValidatorMonitor { slot_clock, ); - indexed_attestation.attesting_indices.iter().for_each(|i| { + indexed_attestation.attesting_indices_iter().for_each(|i| { if let Some(validator) = self.get_validator(*i) { let id = &validator.id; @@ -1321,7 +1322,7 @@ impl ValidatorMonitor { indexed_attestation: &IndexedAttestation, slot_clock: &S, ) { - let data = &indexed_attestation.data; + let data = indexed_attestation.data(); let epoch = data.slot.epoch(E::slots_per_epoch()); let delay = get_message_delay_ms( seen_timestamp, @@ -1330,7 +1331,7 @@ impl ValidatorMonitor { slot_clock, ); - let aggregator_index = signed_aggregate_and_proof.message.aggregator_index; + let aggregator_index = signed_aggregate_and_proof.message().aggregator_index(); if let Some(validator) = self.get_validator(aggregator_index) { let id = &validator.id; @@ -1365,7 +1366,7 @@ impl ValidatorMonitor { }); } - indexed_attestation.attesting_indices.iter().for_each(|i| { + indexed_attestation.attesting_indices_iter().for_each(|i| { if let Some(validator) = self.get_validator(*i) { let id = &validator.id; @@ -1410,11 +1411,11 @@ impl ValidatorMonitor { /// Note: Blocks that get orphaned will skew the inclusion distance calculation. pub fn register_attestation_in_block( &self, - indexed_attestation: &IndexedAttestation, + indexed_attestation: IndexedAttestationRef<'_, E>, parent_slot: Slot, spec: &ChainSpec, ) { - let data = &indexed_attestation.data; + let data = indexed_attestation.data(); // Best effort inclusion distance which ignores skip slots between the parent // and the current block. Skipped slots between the attestation slot and the parent // slot are still counted for simplicity's sake. @@ -1423,7 +1424,7 @@ impl ValidatorMonitor { let delay = inclusion_distance - spec.min_attestation_inclusion_delay; let epoch = data.slot.epoch(E::slots_per_epoch()); - indexed_attestation.attesting_indices.iter().for_each(|i| { + indexed_attestation.attesting_indices_iter().for_each(|i| { if let Some(validator) = self.get_validator(*i) { let id = &validator.id; @@ -1783,33 +1784,31 @@ impl ValidatorMonitor { } /// Register an attester slashing from the gossip network. - pub fn register_gossip_attester_slashing(&self, slashing: &AttesterSlashing) { + pub fn register_gossip_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("gossip", slashing) } /// Register an attester slashing from the HTTP API. - pub fn register_api_attester_slashing(&self, slashing: &AttesterSlashing) { + pub fn register_api_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("api", slashing) } /// Register an attester slashing included in a *valid* `BeaconBlock`. - pub fn register_block_attester_slashing(&self, slashing: &AttesterSlashing) { + pub fn register_block_attester_slashing(&self, slashing: AttesterSlashingRef<'_, E>) { self.register_attester_slashing("block", slashing) } - fn register_attester_slashing(&self, src: &str, slashing: &AttesterSlashing) { - let data = &slashing.attestation_1.data; + fn register_attester_slashing(&self, src: &str, slashing: AttesterSlashingRef<'_, E>) { + let data = slashing.attestation_1().data(); let attestation_1_indices: HashSet = slashing - .attestation_1 - .attesting_indices - .iter() + .attestation_1() + .attesting_indices_iter() .copied() .collect(); slashing - .attestation_2 - .attesting_indices - .iter() + .attestation_2() + .attesting_indices_iter() .filter(|index| attestation_1_indices.contains(index)) .filter_map(|index| self.get_validator(*index)) .for_each(|validator| { diff --git a/beacon_node/beacon_chain/tests/attestation_production.rs b/beacon_node/beacon_chain/tests/attestation_production.rs index ff83b25320..697e449dc6 100644 --- a/beacon_node/beacon_chain/tests/attestation_production.rs +++ b/beacon_node/beacon_chain/tests/attestation_production.rs @@ -8,7 +8,9 @@ use beacon_chain::{metrics, StateSkipConfig, WhenSlotSkipped}; use lazy_static::lazy_static; use std::sync::Arc; use tree_hash::TreeHash; -use types::{AggregateSignature, EthSpec, Keypair, MainnetEthSpec, RelativeEpoch, Slot}; +use types::{ + AggregateSignature, Attestation, EthSpec, Keypair, MainnetEthSpec, RelativeEpoch, Slot, +}; pub const VALIDATOR_COUNT: usize = 16; @@ -188,20 +190,22 @@ async fn produces_attestations() { .produce_unaggregated_attestation(slot, index) .expect("should produce attestation"); - let data = &attestation.data; + let (aggregation_bits_len, aggregation_bits_zero) = match &attestation { + Attestation::Base(att) => { + (att.aggregation_bits.len(), att.aggregation_bits.is_zero()) + } + Attestation::Electra(att) => { + (att.aggregation_bits.len(), att.aggregation_bits.is_zero()) + } + }; + assert_eq!(aggregation_bits_len, committee_len, "bad committee len"); + assert!(aggregation_bits_zero, "some committee bits are set"); + + let data = attestation.data(); assert_eq!( - attestation.aggregation_bits.len(), - committee_len, - "bad committee len" - ); - assert!( - attestation.aggregation_bits.is_zero(), - "some committee bits are set" - ); - assert_eq!( - attestation.signature, - AggregateSignature::empty(), + attestation.signature(), + &AggregateSignature::infinity(), "bad signature" ); assert_eq!(data.index, index, "bad index"); @@ -329,10 +333,10 @@ async fn early_attester_cache_old_request() { .produce_unaggregated_attestation(attest_slot, 0) .unwrap(); - assert_eq!(attestation.data.slot, attest_slot); + assert_eq!(attestation.data().slot, attest_slot); let attested_block = harness .chain - .get_blinded_block(&attestation.data.beacon_block_root) + .get_blinded_block(&attestation.data().beacon_block_root) .unwrap() .unwrap(); assert_eq!(attested_block.slot(), attest_slot); diff --git a/beacon_node/beacon_chain/tests/attestation_verification.rs b/beacon_node/beacon_chain/tests/attestation_verification.rs index 1463d1c5c1..19efe10c6d 100644 --- a/beacon_node/beacon_chain/tests/attestation_verification.rs +++ b/beacon_node/beacon_chain/tests/attestation_verification.rs @@ -3,6 +3,7 @@ use beacon_chain::attestation_verification::{ batch_verify_aggregated_attestations, batch_verify_unaggregated_attestations, Error, }; +use beacon_chain::observed_aggregates::ObservedAttestationKey; use beacon_chain::test_utils::{MakeAttestationOptions, HARNESS_GENESIS_TIME}; use beacon_chain::{ attestation_verification::Error as AttnError, @@ -14,14 +15,17 @@ use beacon_chain::{ use genesis::{interop_genesis_state, DEFAULT_ETH1_BLOCK_HASH}; use int_to_bytes::int_to_bytes32; use lazy_static::lazy_static; +use ssz_types::BitVector; use state_processing::{ per_block_processing::errors::AttestationValidationError, per_slot_processing, }; use tree_hash::TreeHash; use types::{ + signed_aggregate_and_proof::SignedAggregateAndProofRefMut, test_utils::generate_deterministic_keypair, Address, AggregateSignature, Attestation, - BeaconStateError, BitList, ChainSpec, Epoch, EthSpec, ForkName, Hash256, Keypair, - MainnetEthSpec, SecretKey, SelectionProof, SignedAggregateAndProof, Slot, SubnetId, Unsigned, + AttestationRef, AttestationRefMut, BeaconStateError, BitList, ChainSpec, Epoch, EthSpec, + ForkName, Hash256, Keypair, MainnetEthSpec, SecretKey, SelectionProof, SignedAggregateAndProof, + Slot, SubnetId, Unsigned, }; pub type E = MainnetEthSpec; @@ -125,7 +129,12 @@ fn get_valid_unaggregated_attestation( let validator_committee_index = 0; let validator_index = *head .beacon_state - .get_beacon_committee(current_slot, valid_attestation.data.index) + .get_beacon_committee( + current_slot, + valid_attestation + .committee_index() + .expect("should get committee index"), + ) .expect("should get committees") .committee .get(validator_committee_index) @@ -143,8 +152,8 @@ fn get_valid_unaggregated_attestation( ) .expect("should sign attestation"); - let subnet_id = SubnetId::compute_subnet_for_attestation_data::( - &valid_attestation.data, + let subnet_id = SubnetId::compute_subnet_for_attestation::( + valid_attestation.to_ref(), head.beacon_state .get_committee_count_at_slot(current_slot) .expect("should get committee count"), @@ -170,7 +179,12 @@ fn get_valid_aggregated_attestation( let current_slot = chain.slot().expect("should get slot"); let committee = state - .get_beacon_committee(current_slot, aggregate.data.index) + .get_beacon_committee( + current_slot, + aggregate + .committee_index() + .expect("should get committee index"), + ) .expect("should get committees"); let committee_len = committee.committee.len(); @@ -181,7 +195,7 @@ fn get_valid_aggregated_attestation( let aggregator_sk = generate_deterministic_keypair(val_index).sk; let proof = SelectionProof::new::( - aggregate.data.slot, + aggregate.data().slot, &aggregator_sk, &state.fork(), chain.genesis_validators_root, @@ -198,7 +212,7 @@ fn get_valid_aggregated_attestation( let signed_aggregate = SignedAggregateAndProof::from_aggregate( aggregator_index as u64, - aggregate, + aggregate.to_ref(), None, &aggregator_sk, &state.fork(), @@ -213,14 +227,19 @@ fn get_valid_aggregated_attestation( /// attestation. fn get_non_aggregator( chain: &BeaconChain, - aggregate: &Attestation, + aggregate: AttestationRef, ) -> (usize, SecretKey) { let head = chain.head_snapshot(); let state = &head.beacon_state; let current_slot = chain.slot().expect("should get slot"); let committee = state - .get_beacon_committee(current_slot, aggregate.data.index) + .get_beacon_committee( + current_slot, + aggregate + .committee_index() + .expect("should get committee index"), + ) .expect("should get committees"); let committee_len = committee.committee.len(); @@ -231,7 +250,7 @@ fn get_non_aggregator( let aggregator_sk = generate_deterministic_keypair(val_index).sk; let proof = SelectionProof::new::( - aggregate.data.slot, + aggregate.data().slot, &aggregator_sk, &state.fork(), chain.genesis_validators_root, @@ -301,15 +320,19 @@ impl GossipTester { get_valid_aggregated_attestation(&harness.chain, valid_attestation.clone()); let mut invalid_attestation = valid_attestation.clone(); - invalid_attestation.data.beacon_block_root = Hash256::repeat_byte(13); + invalid_attestation.data_mut().beacon_block_root = Hash256::repeat_byte(13); let (mut invalid_aggregate, _, _) = get_valid_aggregated_attestation(&harness.chain, invalid_attestation.clone()); - invalid_aggregate.message.aggregator_index = invalid_aggregate - .message - .aggregator_index - .checked_sub(1) - .unwrap(); + + match invalid_aggregate.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregator_index = att.message.aggregator_index.checked_sub(1).unwrap(); + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregator_index = att.message.aggregator_index.checked_sub(1).unwrap(); + } + } Self { harness, @@ -361,7 +384,10 @@ impl GossipTester { } pub fn non_aggregator(&self) -> (usize, SecretKey) { - get_non_aggregator(&self.harness.chain, &self.valid_aggregate.message.aggregate) + get_non_aggregator( + &self.harness.chain, + self.valid_aggregate.message().aggregate(), + ) } pub fn import_valid_aggregate(self) -> Self { @@ -418,6 +444,7 @@ impl GossipTester { vec![&self.invalid_aggregate, &aggregate].into_iter(), ) .unwrap(); + assert_eq!(results.len(), 2); let batch_err = results.pop().unwrap().err().expect(&format!( "{} should error during batch_verify_aggregated_attestations_for_gossip", @@ -490,7 +517,14 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "aggregate from future slot", - |tester, a| a.message.aggregate.data.slot = tester.slot() + 1, + |tester, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.slot = tester.slot() + 1 + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.slot = tester.slot() + 1 + } + }, |tester, err| { assert!(matches!( err, @@ -504,8 +538,18 @@ async fn aggregated_gossip_verification() { "aggregate from past slot", |tester, a| { let too_early_slot = tester.earliest_valid_attestation_slot() - 1; - a.message.aggregate.data.slot = too_early_slot; - a.message.aggregate.data.target.epoch = too_early_slot.epoch(E::slots_per_epoch()); + match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.slot = too_early_slot; + att.message.aggregate.data.target.epoch = + too_early_slot.epoch(E::slots_per_epoch()); + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.slot = too_early_slot; + att.message.aggregate.data.target.epoch = + too_early_slot.epoch(E::slots_per_epoch()); + } + } }, |tester, err| { let valid_early_slot = tester.earliest_valid_attestation_slot(); @@ -529,7 +573,14 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "attestation with invalid target epoch", - |_, a| a.message.aggregate.data.target.epoch += 1, + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.target.epoch += 1 + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.target.epoch += 1 + } + }, |_, err| assert!(matches!(err, AttnError::InvalidTargetEpoch { .. })), ) /* @@ -538,7 +589,14 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "attestation with invalid target root", - |_, a| a.message.aggregate.data.target.root = Hash256::repeat_byte(42), + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.target.root = Hash256::repeat_byte(42) + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.target.root = Hash256::repeat_byte(42) + } + }, |_, err| assert!(matches!(err, AttnError::InvalidTargetRoot { .. })), ) /* @@ -548,7 +606,14 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "aggregate with unknown head block", - |_, a| a.message.aggregate.data.beacon_block_root = Hash256::repeat_byte(42), + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.beacon_block_root = Hash256::repeat_byte(42) + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.beacon_block_root = Hash256::repeat_byte(42) + } + }, |_, err| { assert!(matches!( err, @@ -566,11 +631,19 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "aggregate with no participants", - |_, a| { - let aggregation_bits = &mut a.message.aggregate.aggregation_bits; - aggregation_bits.difference_inplace(&aggregation_bits.clone()); - assert!(aggregation_bits.is_zero()); - a.message.aggregate.signature = AggregateSignature::infinity(); + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + let aggregation_bits = &mut att.message.aggregate.aggregation_bits; + aggregation_bits.difference_inplace(&aggregation_bits.clone()); + assert!(aggregation_bits.is_zero()); + att.message.aggregate.signature = AggregateSignature::infinity() + } + SignedAggregateAndProofRefMut::Electra(att) => { + let aggregation_bits = &mut att.message.aggregate.aggregation_bits; + aggregation_bits.difference_inplace(&aggregation_bits.clone()); + assert!(aggregation_bits.is_zero()); + att.message.aggregate.signature = AggregateSignature::infinity() + } }, |_, err| assert!(matches!(err, AttnError::EmptyAggregationBitfield)), ) @@ -581,7 +654,14 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "aggregate with bad signature", - |tester, a| a.signature = tester.aggregator_sk.sign(Hash256::repeat_byte(42)), + |tester, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.signature = tester.aggregator_sk.sign(Hash256::repeat_byte(42)) + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.signature = tester.aggregator_sk.sign(Hash256::repeat_byte(42)) + } + }, |_, err| assert!(matches!(err, AttnError::InvalidSignature)), ) /* @@ -598,7 +678,7 @@ async fn aggregated_gossip_verification() { .chain .head_snapshot() .beacon_state - .get_beacon_committee(tester.slot(), a.message.aggregate.data.index) + .get_beacon_committee(tester.slot(), a.message().aggregate().committee_index().expect("should get committee index")) .expect("should get committees") .committee .len(); @@ -608,19 +688,38 @@ async fn aggregated_gossip_verification() { // // Could run for ever, but that seems _really_ improbable. let mut i: u64 = 0; - a.message.selection_proof = loop { - i += 1; - let proof: SelectionProof = tester - .aggregator_sk - .sign(Hash256::from_slice(&int_to_bytes32(i))) - .into(); - if proof - .is_aggregator(committee_len, &tester.harness.chain.spec) - .unwrap() - { - break proof.into(); + match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.selection_proof = loop { + i += 1; + let proof: SelectionProof = tester + .aggregator_sk + .sign(Hash256::from_slice(&int_to_bytes32(i))) + .into(); + if proof + .is_aggregator(committee_len, &tester.harness.chain.spec) + .unwrap() + { + break proof.into(); + } + }; } - }; + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.selection_proof = loop { + i += 1; + let proof: SelectionProof = tester + .aggregator_sk + .sign(Hash256::from_slice(&int_to_bytes32(i))) + .into(); + if proof + .is_aggregator(committee_len, &tester.harness.chain.spec) + .unwrap() + { + break proof.into(); + } + }; + } + } }, |_, err| assert!(matches!(err, AttnError::InvalidSignature)), ) @@ -634,7 +733,14 @@ async fn aggregated_gossip_verification() { |tester, a| { let mut agg_sig = AggregateSignature::infinity(); agg_sig.add_assign(&tester.aggregator_sk.sign(Hash256::repeat_byte(42))); - a.message.aggregate.signature = agg_sig; + match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.signature = agg_sig; + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.signature = agg_sig; + } + } }, |_, err| assert!(matches!(err, AttnError::InvalidSignature)), ) @@ -643,8 +749,15 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "aggregate with too-high aggregator index", - |_, a| { - a.message.aggregator_index = ::ValidatorRegistryLimit::to_u64() + 1 + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregator_index = + ::ValidatorRegistryLimit::to_u64() + 1 + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregator_index = + ::ValidatorRegistryLimit::to_u64() + 1 + } }, |_, err| { assert!(matches!( @@ -663,7 +776,14 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "aggregate with unknown aggregator index", - |_, a| a.message.aggregator_index = VALIDATOR_COUNT as u64, + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregator_index = VALIDATOR_COUNT as u64 + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregator_index = VALIDATOR_COUNT as u64 + } + }, |_, err| { assert!(matches!( err, @@ -671,7 +791,7 @@ async fn aggregated_gossip_verification() { // // AttnError::AggregatorPubkeyUnknown(unknown_validator) // - // However the following error is triggered first: + // However, the following error is triggered first: AttnError::AggregatorNotInCommittee { aggregator_index } @@ -683,7 +803,7 @@ async fn aggregated_gossip_verification() { * The following test ensures: * * aggregate_and_proof.selection_proof selects the validator as an aggregator for the slot -- - * i.e. is_aggregator(state, aggregate.data.slot, aggregate.data.index, + * i.e. is_aggregator(state, aggregate.data.slot, aggregate.committee_index(), * aggregate_and_proof.selection_proof) returns True. */ .inspect_aggregate_err( @@ -693,7 +813,7 @@ async fn aggregated_gossip_verification() { let (index, sk) = tester.non_aggregator(); *a = SignedAggregateAndProof::from_aggregate( index as u64, - tester.valid_aggregate.message.aggregate.clone(), + tester.valid_aggregate.message().aggregate().clone(), None, &sk, &chain.canonical_head.cached_head().head_fork(), @@ -703,6 +823,7 @@ async fn aggregated_gossip_verification() { }, |tester, err| { let (val_index, _) = tester.non_aggregator(); + assert!(matches!( err, AttnError::InvalidSelectionProof { @@ -729,7 +850,12 @@ async fn aggregated_gossip_verification() { assert!(matches!( err, AttnError::AttestationSupersetKnown(hash) - if hash == tester.valid_aggregate.message.aggregate.data.tree_hash_root() + if hash == ObservedAttestationKey { + committee_index: tester.valid_aggregate.message().aggregate() + .committee_index() + .expect("should get committee index"), + attestation_data: tester.valid_aggregate.message().aggregate().data().clone(), + }.tree_hash_root() )) }, ) @@ -741,7 +867,14 @@ async fn aggregated_gossip_verification() { */ .inspect_aggregate_err( "aggregate from aggregator that has already been seen", - |_, a| a.message.aggregate.data.beacon_block_root = Hash256::repeat_byte(42), + |_, a| match a.to_mut() { + SignedAggregateAndProofRefMut::Base(att) => { + att.message.aggregate.data.beacon_block_root = Hash256::repeat_byte(42) + } + SignedAggregateAndProofRefMut::Electra(att) => { + att.message.aggregate.data.beacon_block_root = Hash256::repeat_byte(42) + } + }, |tester, err| { assert!(matches!( err, @@ -766,13 +899,29 @@ async fn unaggregated_gossip_verification() { .inspect_unaggregate_err( "attestation with invalid committee index", |tester, a, _| { - a.data.index = tester - .harness - .chain - .head_snapshot() - .beacon_state - .get_committee_count_at_slot(a.data.slot) - .unwrap() + match a.to_mut() { + AttestationRefMut::Base(attn) => { + attn.data.index = tester + .harness + .chain + .head_snapshot() + .beacon_state + .get_committee_count_at_slot(attn.data.slot) + .unwrap(); + } + AttestationRefMut::Electra(attn) => { + let committee_index = tester + .harness + .chain + .head_snapshot() + .beacon_state + .get_committee_count_at_slot(attn.data.slot) + .unwrap(); + // overwrite the existing committee bits before setting + attn.committee_bits = BitVector::default(); + attn.committee_bits.set(committee_index as usize, true).unwrap(); + } + } }, |_, err| assert!(matches!(err, AttnError::NoCommitteeForSlotAndIndex { .. })), ) @@ -806,7 +955,7 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation from future slot", - |tester, a, _| a.data.slot = tester.slot() + 1, + |tester, a, _| a.data_mut().slot = tester.slot() + 1, |tester, err| { assert!(matches!( err, @@ -822,8 +971,8 @@ async fn unaggregated_gossip_verification() { "attestation from past slot", |tester, a, _| { let too_early_slot = tester.earliest_valid_attestation_slot() - 1; - a.data.slot = too_early_slot; - a.data.target.epoch = too_early_slot.epoch(E::slots_per_epoch()); + a.data_mut().slot = too_early_slot; + a.data_mut().target.epoch = too_early_slot.epoch(E::slots_per_epoch()); }, |tester, err| { let valid_early_slot = tester.earliest_valid_attestation_slot(); @@ -847,7 +996,7 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation with invalid target epoch", - |_, a, _| a.data.target.epoch += 1, + |_, a, _| a.data_mut().target.epoch += 1, |_, err| { assert!(matches!( err, @@ -863,15 +1012,29 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation without any aggregation bits set", - |tester, a, _| { - a.aggregation_bits - .set(tester.attester_committee_index, false) - .expect("should unset aggregation bit"); - assert_eq!( - a.aggregation_bits.num_set_bits(), - 0, - "test requires no set bits" - ); + |tester, mut a, _| { + match &mut a { + Attestation::Base(ref mut att) => { + att.aggregation_bits + .set(tester.attester_committee_index, false) + .expect("should unset aggregation bit"); + assert_eq!( + att.aggregation_bits.num_set_bits(), + 0, + "test requires no set bits" + ); + } + Attestation::Electra(ref mut att) => { + att.aggregation_bits + .set(tester.attester_committee_index, false) + .expect("should unset aggregation bit"); + assert_eq!( + att.aggregation_bits.num_set_bits(), + 0, + "test requires no set bits" + ); + } + } }, |_, err| { assert!(matches!( @@ -882,10 +1045,19 @@ async fn unaggregated_gossip_verification() { ) .inspect_unaggregate_err( "attestation with two aggregation bits set", - |tester, a, _| { - a.aggregation_bits - .set(tester.attester_committee_index + 1, true) - .expect("should set second aggregation bit"); + |tester, mut a, _| { + match &mut a { + Attestation::Base(ref mut att) => { + att.aggregation_bits + .set(tester.attester_committee_index + 1, true) + .expect("should set second aggregation bit"); + } + Attestation::Electra(ref mut att) => { + att.aggregation_bits + .set(tester.attester_committee_index + 1, true) + .expect("should set second aggregation bit"); + } + } }, |_, err| { assert!(matches!( @@ -903,11 +1075,22 @@ async fn unaggregated_gossip_verification() { */ .inspect_unaggregate_err( "attestation with invalid bitfield", - |_, a, _| { - let bits = a.aggregation_bits.iter().collect::>(); - a.aggregation_bits = BitList::with_capacity(bits.len() + 1).unwrap(); - for (i, bit) in bits.into_iter().enumerate() { - a.aggregation_bits.set(i, bit).unwrap(); + |_, mut a, _| { + match &mut a { + Attestation::Base(ref mut att) => { + let bits = att.aggregation_bits.iter().collect::>(); + att.aggregation_bits = BitList::with_capacity(bits.len() + 1).unwrap(); + for (i, bit) in bits.into_iter().enumerate() { + att.aggregation_bits.set(i, bit).unwrap(); + } + } + Attestation::Electra(ref mut att) => { + let bits = att.aggregation_bits.iter().collect::>(); + att.aggregation_bits = BitList::with_capacity(bits.len() + 1).unwrap(); + for (i, bit) in bits.into_iter().enumerate() { + att.aggregation_bits.set(i, bit).unwrap(); + } + } } }, |_, err| { @@ -927,7 +1110,7 @@ async fn unaggregated_gossip_verification() { .inspect_unaggregate_err( "attestation with unknown head block", |_, a, _| { - a.data.beacon_block_root = Hash256::repeat_byte(42); + a.data_mut().beacon_block_root = Hash256::repeat_byte(42); }, |_, err| { assert!(matches!( @@ -949,7 +1132,7 @@ async fn unaggregated_gossip_verification() { .inspect_unaggregate_err( "attestation with invalid target root", |_, a, _| { - a.data.target.root = Hash256::repeat_byte(42); + a.data_mut().target.root = Hash256::repeat_byte(42); }, |_, err| { assert!(matches!( @@ -968,7 +1151,7 @@ async fn unaggregated_gossip_verification() { |tester, a, _| { let mut agg_sig = AggregateSignature::infinity(); agg_sig.add_assign(&tester.attester_sk.sign(Hash256::repeat_byte(42))); - a.signature = agg_sig; + *a.signature_mut() = agg_sig; }, |_, err| { assert!(matches!( @@ -1055,7 +1238,7 @@ async fn attestation_that_skips_epochs() { .cloned() .expect("should have at least one attestation in committee"); - let block_root = attestation.data.beacon_block_root; + let block_root = attestation.data().beacon_block_root; let block_slot = harness .chain .store @@ -1066,7 +1249,7 @@ async fn attestation_that_skips_epochs() { .slot(); assert!( - attestation.data.slot - block_slot > E::slots_per_epoch() * 2, + attestation.data().slot - block_slot > E::slots_per_epoch() * 2, "the attestation must skip more than two epochs" ); @@ -1228,7 +1411,7 @@ async fn attestation_to_finalized_block() { .first() .cloned() .expect("should have at least one attestation in committee"); - assert_eq!(attestation.data.beacon_block_root, earlier_block_root); + assert_eq!(attestation.data().beacon_block_root, earlier_block_root); // Attestation should be rejected for attesting to a pre-finalization block. let res = harness @@ -1281,8 +1464,8 @@ async fn verify_aggregate_for_gossip_doppelganger_detection() { .verify_aggregated_attestation_for_gossip(&valid_aggregate) .expect("should verify aggregate attestation"); - let epoch = valid_aggregate.message.aggregate.data.target.epoch; - let index = valid_aggregate.message.aggregator_index as usize; + let epoch = valid_aggregate.message().aggregate().data().target.epoch; + let index = valid_aggregate.message().aggregator_index() as usize; assert!(harness.chain.validator_seen_at_epoch(index, epoch)); // Check the correct beacon cache is populated @@ -1338,7 +1521,7 @@ async fn verify_attestation_for_gossip_doppelganger_detection() { .verify_unaggregated_attestation_for_gossip(&valid_attestation, Some(subnet_id)) .expect("should verify attestation"); - let epoch = valid_attestation.data.target.epoch; + let epoch = valid_attestation.data().target.epoch; assert!(harness.chain.validator_seen_at_epoch(index, epoch)); // Check the correct beacon cache is populated diff --git a/beacon_node/beacon_chain/tests/block_verification.rs b/beacon_node/beacon_chain/tests/block_verification.rs index 9c196b12e1..d9c9a3b6a7 100644 --- a/beacon_node/beacon_chain/tests/block_verification.rs +++ b/beacon_node/beacon_chain/tests/block_verification.rs @@ -2,7 +2,9 @@ use beacon_chain::block_verification_types::{AsBlock, ExecutedBlock, RpcBlock}; use beacon_chain::{ - test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType}, + test_utils::{ + test_spec, AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType, + }, AvailabilityProcessingStatus, BeaconChain, BeaconChainTypes, ExecutionPendingBlock, }; use beacon_chain::{ @@ -13,7 +15,7 @@ use lazy_static::lazy_static; use logging::test_logger; use slasher::{Config as SlasherConfig, Slasher}; use state_processing::{ - common::get_indexed_attestation, + common::{attesting_indices_base, attesting_indices_electra}, per_block_processing::{per_block_processing, BlockSignatureStrategy}, per_slot_processing, BlockProcessingError, ConsensusContext, VerifyBlockRoot, }; @@ -666,37 +668,95 @@ async fn invalid_signature_attester_slashing() { for &block_index in BLOCK_INDICES { let harness = get_invalid_sigs_harness(&chain_segment).await; let mut snapshots = chain_segment.clone(); - let indexed_attestation = IndexedAttestation { - attesting_indices: vec![0].into(), - data: AttestationData { - slot: Slot::new(0), - index: 0, - beacon_block_root: Hash256::zero(), - source: Checkpoint { - epoch: Epoch::new(0), - root: Hash256::zero(), + let fork_name = harness.chain.spec.fork_name_at_slot::(Slot::new(0)); + + let attester_slashing = if fork_name.electra_enabled() { + let indexed_attestation = IndexedAttestationElectra { + attesting_indices: vec![0].into(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + target: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, }, - target: Checkpoint { - epoch: Epoch::new(0), - root: Hash256::zero(), + signature: junk_aggregate_signature(), + }; + let attester_slashing = AttesterSlashingElectra { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + + AttesterSlashing::Electra(attester_slashing) + } else { + let indexed_attestation = IndexedAttestationBase { + attesting_indices: vec![0].into(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, + target: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::zero(), + }, }, - }, - signature: junk_aggregate_signature(), - }; - let attester_slashing = AttesterSlashing { - attestation_1: indexed_attestation.clone(), - attestation_2: indexed_attestation, + signature: junk_aggregate_signature(), + }; + let attester_slashing = AttesterSlashingBase { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + + AttesterSlashing::Base(attester_slashing) }; + let (mut block, signature) = snapshots[block_index] .beacon_block .as_ref() .clone() .deconstruct(); - block - .body_mut() - .attester_slashings_mut() - .push(attester_slashing) - .expect("should update attester slashing"); + match &mut block.body_mut() { + BeaconBlockBodyRefMut::Base(ref mut blk) => { + blk.attester_slashings + .push(attester_slashing.as_base().unwrap().clone()) + .expect("should update attester slashing"); + } + BeaconBlockBodyRefMut::Altair(ref mut blk) => { + blk.attester_slashings + .push(attester_slashing.as_base().unwrap().clone()) + .expect("should update attester slashing"); + } + BeaconBlockBodyRefMut::Bellatrix(ref mut blk) => { + blk.attester_slashings + .push(attester_slashing.as_base().unwrap().clone()) + .expect("should update attester slashing"); + } + BeaconBlockBodyRefMut::Capella(ref mut blk) => { + blk.attester_slashings + .push(attester_slashing.as_base().unwrap().clone()) + .expect("should update attester slashing"); + } + BeaconBlockBodyRefMut::Deneb(ref mut blk) => { + blk.attester_slashings + .push(attester_slashing.as_base().unwrap().clone()) + .expect("should update attester slashing"); + } + BeaconBlockBodyRefMut::Electra(ref mut blk) => { + blk.attester_slashings + .push(attester_slashing.as_electra().unwrap().clone()) + .expect("should update attester slashing"); + } + } snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); @@ -726,8 +786,34 @@ async fn invalid_signature_attestation() { .as_ref() .clone() .deconstruct(); - if let Some(attestation) = block.body_mut().attestations_mut().get_mut(0) { - attestation.signature = junk_aggregate_signature(); + match &mut block.body_mut() { + BeaconBlockBodyRefMut::Base(ref mut blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), + BeaconBlockBodyRefMut::Altair(ref mut blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), + BeaconBlockBodyRefMut::Bellatrix(ref mut blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), + BeaconBlockBodyRefMut::Capella(ref mut blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), + BeaconBlockBodyRefMut::Deneb(ref mut blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), + BeaconBlockBodyRefMut::Electra(ref mut blk) => blk + .attestations + .get_mut(0) + .map(|att| att.signature = junk_aggregate_signature()), + }; + + if block.body().attestations_len() > 0 { snapshots[block_index].beacon_block = Arc::new(SignedBeaconBlock::from_block(block, signature)); update_parent_roots(&mut snapshots, &mut chain_segment_blobs); @@ -1126,8 +1212,14 @@ async fn block_gossip_verification() { #[tokio::test] async fn verify_block_for_gossip_slashing_detection() { let slasher_dir = tempdir().unwrap(); + let spec = Arc::new(test_spec::()); let slasher = Arc::new( - Slasher::open(SlasherConfig::new(slasher_dir.path().into()), test_logger()).unwrap(), + Slasher::open( + SlasherConfig::new(slasher_dir.path().into()), + spec, + test_logger(), + ) + .unwrap(), ); let inner_slasher = slasher.clone(); @@ -1191,9 +1283,13 @@ async fn verify_block_for_gossip_doppelganger_detection() { let state = harness.get_current_state(); let ((block, _), _) = harness.make_block(state.clone(), Slot::new(1)).await; - + let attestations = block + .message() + .body() + .attestations() + .map(|att| att.clone_as_attestation()) + .collect::>(); let verified_block = harness.chain.verify_block_for_gossip(block).await.unwrap(); - let attestations = verified_block.block.message().body().attestations().clone(); harness .chain .process_block( @@ -1207,13 +1303,23 @@ async fn verify_block_for_gossip_doppelganger_detection() { .unwrap(); for att in attestations.iter() { - let epoch = att.data.target.epoch; - let committee = state - .get_beacon_committee(att.data.slot, att.data.index) - .unwrap(); - let indexed_attestation = get_indexed_attestation(committee.committee, att).unwrap(); + let epoch = att.data().target.epoch; + let indexed_attestation = match att { + Attestation::Base(att) => { + let committee = state + .get_beacon_committee(att.data.slot, att.data.index) + .unwrap(); + attesting_indices_base::get_indexed_attestation(committee.committee, att).unwrap() + } + Attestation::Electra(att) => { + attesting_indices_electra::get_indexed_attestation_from_state(&state, att).unwrap() + } + }; - for &index in &indexed_attestation.attesting_indices { + for index in match indexed_attestation { + IndexedAttestation::Base(att) => att.attesting_indices.into_iter(), + IndexedAttestation::Electra(att) => att.attesting_indices.into_iter(), + } { let index = index as usize; assert!(harness.chain.validator_seen_at_epoch(index, epoch)); diff --git a/beacon_node/beacon_chain/tests/payload_invalidation.rs b/beacon_node/beacon_chain/tests/payload_invalidation.rs index 0c36d21f2e..4dc7d20e22 100644 --- a/beacon_node/beacon_chain/tests/payload_invalidation.rs +++ b/beacon_node/beacon_chain/tests/payload_invalidation.rs @@ -25,7 +25,6 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use task_executor::ShutdownReason; -use tree_hash::TreeHash; use types::*; const VALIDATOR_COUNT: usize = 32; @@ -1193,15 +1192,23 @@ async fn attesting_to_optimistic_head() { .produce_unaggregated_attestation(Slot::new(0), 0) .unwrap(); - attestation.aggregation_bits.set(0, true).unwrap(); - attestation.data.slot = slot; - attestation.data.beacon_block_root = root; + match &mut attestation { + Attestation::Base(ref mut att) => { + att.aggregation_bits.set(0, true).unwrap(); + } + Attestation::Electra(ref mut att) => { + att.aggregation_bits.set(0, true).unwrap(); + } + } + + attestation.data_mut().slot = slot; + attestation.data_mut().beacon_block_root = root; rig.harness .chain .naive_aggregation_pool .write() - .insert(&attestation) + .insert(attestation.to_ref()) .unwrap(); attestation @@ -1216,16 +1223,13 @@ async fn attesting_to_optimistic_head() { let get_aggregated = || { rig.harness .chain - .get_aggregated_attestation(&attestation.data) + .get_aggregated_attestation(attestation.to_ref()) }; let get_aggregated_by_slot_and_root = || { rig.harness .chain - .get_aggregated_attestation_by_slot_and_root( - attestation.data.slot, - &attestation.data.tree_hash_root(), - ) + .get_aggregated_attestation(attestation.to_ref()) }; /* diff --git a/beacon_node/beacon_chain/tests/store_tests.rs b/beacon_node/beacon_chain/tests/store_tests.rs index 5da92573f7..c2468102ee 100644 --- a/beacon_node/beacon_chain/tests/store_tests.rs +++ b/beacon_node/beacon_chain/tests/store_tests.rs @@ -606,7 +606,7 @@ async fn epoch_boundary_state_attestation_processing() { for (attestation, subnet_id) in late_attestations.into_iter().flatten() { // load_epoch_boundary_state is idempotent! - let block_root = attestation.data.beacon_block_root; + let block_root = attestation.data().beacon_block_root; let block = store .get_blinded_block(&block_root) .unwrap() @@ -629,7 +629,7 @@ async fn epoch_boundary_state_attestation_processing() { .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); let current_slot = harness.chain.slot().expect("should get slot"); - let expected_attestation_slot = attestation.data.slot; + let expected_attestation_slot = attestation.data().slot; // Extra -1 to handle gossip clock disparity. let expected_earliest_permissible_slot = current_slot - E::slots_per_epoch() - 1; @@ -1014,6 +1014,7 @@ async fn multiple_attestations_per_block() { .await; let head = harness.chain.head_snapshot(); + let committees_per_slot = head .beacon_state .get_committee_count_at_slot(head.beacon_state.slot()) @@ -1022,16 +1023,29 @@ async fn multiple_attestations_per_block() { for snapshot in harness.chain.chain_dump().unwrap() { let slot = snapshot.beacon_block.slot(); - assert_eq!( - snapshot - .beacon_block - .as_ref() - .message() - .body() - .attestations() - .len() as u64, - if slot <= 1 { 0 } else { committees_per_slot } - ); + let fork_name = harness.chain.spec.fork_name_at_slot::(slot); + + if fork_name.electra_enabled() { + assert_eq!( + snapshot + .beacon_block + .as_ref() + .message() + .body() + .attestations_len() as u64, + if slot <= 1 { 0 } else { 1 } + ); + } else { + assert_eq!( + snapshot + .beacon_block + .as_ref() + .message() + .body() + .attestations_len() as u64, + if slot <= 1 { 0 } else { committees_per_slot } + ); + } } } diff --git a/beacon_node/beacon_chain/tests/sync_committee_verification.rs b/beacon_node/beacon_chain/tests/sync_committee_verification.rs index 0e4745ff6b..242ed55847 100644 --- a/beacon_node/beacon_chain/tests/sync_committee_verification.rs +++ b/beacon_node/beacon_chain/tests/sync_committee_verification.rs @@ -318,7 +318,6 @@ async fn aggregated_gossip_verification() { * The contribution_and_proof.selection_proof is a valid signature of the `SyncAggregatorSelectionData` * derived from the contribution by the validator with index `contribution_and_proof.aggregator_index`. */ - assert_invalid!( "aggregate with bad selection proof signature", { @@ -354,7 +353,6 @@ async fn aggregated_gossip_verification() { * derived from the participation info in `aggregation_bits` for the subcommittee specified by * the `contribution.subcommittee_index`. */ - assert_invalid!( "aggregate with bad aggregate signature", { @@ -450,6 +448,7 @@ async fn aggregated_gossip_verification() { root: contribution.beacon_block_root, subcommittee_index: contribution.subcommittee_index, }; + assert_invalid!( "aggregate that has already been seen", valid_aggregate.clone(), diff --git a/beacon_node/beacon_chain/tests/tests.rs b/beacon_node/beacon_chain/tests/tests.rs index 2f496eecd7..2a0854e78f 100644 --- a/beacon_node/beacon_chain/tests/tests.rs +++ b/beacon_node/beacon_chain/tests/tests.rs @@ -574,7 +574,7 @@ async fn attestations_with_increasing_slots() { .verify_unaggregated_attestation_for_gossip(&attestation, Some(subnet_id)); let current_slot = harness.chain.slot().expect("should get slot"); - let expected_attestation_slot = attestation.data.slot; + let expected_attestation_slot = attestation.data().slot; let expected_earliest_permissible_slot = current_slot - MinimalEthSpec::slots_per_epoch() - 1; diff --git a/beacon_node/client/src/notifier.rs b/beacon_node/client/src/notifier.rs index 7c224671ff..632188014e 100644 --- a/beacon_node/client/src/notifier.rs +++ b/beacon_node/client/src/notifier.rs @@ -551,7 +551,7 @@ async fn electra_readiness_logging( .snapshot .beacon_state .fork_name_unchecked() - >= ForkName::Electra; + .electra_enabled(); let has_execution_layer = beacon_chain.execution_layer.is_some(); diff --git a/beacon_node/execution_layer/src/lib.rs b/beacon_node/execution_layer/src/lib.rs index 3a744e3e71..f78803a909 100644 --- a/beacon_node/execution_layer/src/lib.rs +++ b/beacon_node/execution_layer/src/lib.rs @@ -2162,7 +2162,7 @@ fn verify_builder_bid( .ok() .cloned() .map(|withdrawals| Withdrawals::::from(withdrawals).tree_hash_root()); - let payload_withdrawals_root = header.withdrawals_root().ok().copied(); + let payload_withdrawals_root = header.withdrawals_root().ok(); if header.parent_hash() != parent_hash { Err(Box::new(InvalidBuilderPayload::ParentHash { diff --git a/beacon_node/http_api/src/block_packing_efficiency.rs b/beacon_node/http_api/src/block_packing_efficiency.rs index f105fdf0a7..66c7187278 100644 --- a/beacon_node/http_api/src/block_packing_efficiency.rs +++ b/beacon_node/http_api/src/block_packing_efficiency.rs @@ -10,8 +10,8 @@ use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use std::sync::Arc; use types::{ - BeaconCommittee, BeaconState, BeaconStateError, BlindedPayload, ChainSpec, Epoch, EthSpec, - Hash256, OwnedBeaconCommittee, RelativeEpoch, SignedBeaconBlock, Slot, + AttestationRef, BeaconCommittee, BeaconState, BeaconStateError, BlindedPayload, ChainSpec, + Epoch, EthSpec, Hash256, OwnedBeaconCommittee, RelativeEpoch, SignedBeaconBlock, Slot, }; use warp_utils::reject::{beacon_chain_error, custom_bad_request, custom_server_error}; @@ -111,22 +111,45 @@ impl PackingEfficiencyHandler { let attestations = block_body.attestations(); let mut attestations_in_block = HashMap::new(); - for attestation in attestations.iter() { - for (position, voted) in attestation.aggregation_bits.iter().enumerate() { - if voted { - let unique_attestation = UniqueAttestation { - slot: attestation.data.slot, - committee_index: attestation.data.index, - committee_position: position, - }; - let inclusion_distance: u64 = block - .slot() - .as_u64() - .checked_sub(attestation.data.slot.as_u64()) - .ok_or(PackingEfficiencyError::InvalidAttestationError)?; + for attestation in attestations { + match attestation { + AttestationRef::Base(attn) => { + for (position, voted) in attn.aggregation_bits.iter().enumerate() { + if voted { + let unique_attestation = UniqueAttestation { + slot: attn.data.slot, + committee_index: attn.data.index, + committee_position: position, + }; + let inclusion_distance: u64 = block + .slot() + .as_u64() + .checked_sub(attn.data.slot.as_u64()) + .ok_or(PackingEfficiencyError::InvalidAttestationError)?; - self.available_attestations.remove(&unique_attestation); - attestations_in_block.insert(unique_attestation, inclusion_distance); + self.available_attestations.remove(&unique_attestation); + attestations_in_block.insert(unique_attestation, inclusion_distance); + } + } + } + AttestationRef::Electra(attn) => { + for (position, voted) in attn.aggregation_bits.iter().enumerate() { + if voted { + let unique_attestation = UniqueAttestation { + slot: attn.data.slot, + committee_index: attn.data.index, + committee_position: position, + }; + let inclusion_distance: u64 = block + .slot() + .as_u64() + .checked_sub(attn.data.slot.as_u64()) + .ok_or(PackingEfficiencyError::InvalidAttestationError)?; + + self.available_attestations.remove(&unique_attestation); + attestations_in_block.insert(unique_attestation, inclusion_distance); + } + } } } } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 02db6b6a05..7c8f64a722 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -1635,7 +1635,12 @@ pub fn serve( let (block, execution_optimistic, finalized) = block_id.blinded_block(&chain)?; Ok(api_types::GenericResponse::from( - block.message().body().attestations().clone(), + block + .message() + .body() + .attestations() + .map(|att| att.clone_as_attestation()) + .collect::>(), ) .add_execution_optimistic_finalized(execution_optimistic, finalized)) }) @@ -1799,7 +1804,7 @@ pub fn serve( .naive_aggregation_pool .read() .iter() - .filter(|&att| query_filter(&att.data)) + .filter(|&att| query_filter(att.data())) .cloned(), ); Ok(api_types::GenericResponse::from(attestations)) @@ -1833,7 +1838,7 @@ pub fn serve( chain .validator_monitor .read() - .register_api_attester_slashing(&slashing); + .register_api_attester_slashing(slashing.to_ref()); if let ObservationOutcome::New(slashing) = outcome { publish_pubsub_message( @@ -3162,7 +3167,7 @@ pub fn serve( chain .produce_unaggregated_attestation(query.slot, query.committee_index) - .map(|attestation| attestation.data) + .map(|attestation| attestation.data().clone()) .map(api_types::GenericResponse::from) .map_err(warp_utils::reject::beacon_chain_error) }) @@ -3186,7 +3191,7 @@ pub fn serve( task_spawner.blocking_json_task(Priority::P0, move || { not_synced_filter?; chain - .get_aggregated_attestation_by_slot_and_root( + .get_pre_electra_aggregated_attestation_by_slot_and_root( query.slot, &query.attestation_data_root, ) @@ -3363,9 +3368,9 @@ pub fn serve( "Failure verifying aggregate and proofs"; "error" => format!("{:?}", e), "request_index" => index, - "aggregator_index" => aggregate.message.aggregator_index, - "attestation_index" => aggregate.message.aggregate.data.index, - "attestation_slot" => aggregate.message.aggregate.data.slot, + "aggregator_index" => aggregate.message().aggregator_index(), + "attestation_index" => aggregate.message().aggregate().committee_index(), + "attestation_slot" => aggregate.message().aggregate().data().slot, ); failures.push(api_types::Failure::new(index, format!("Verification: {:?}", e))); } @@ -3384,9 +3389,9 @@ pub fn serve( "Failure applying verified aggregate attestation to fork choice"; "error" => format!("{:?}", e), "request_index" => index, - "aggregator_index" => verified_aggregate.aggregate().message.aggregator_index, - "attestation_index" => verified_aggregate.attestation().data.index, - "attestation_slot" => verified_aggregate.attestation().data.slot, + "aggregator_index" => verified_aggregate.aggregate().message().aggregator_index(), + "attestation_index" => verified_aggregate.attestation().committee_index(), + "attestation_slot" => verified_aggregate.attestation().data().slot, ); failures.push(api_types::Failure::new(index, format!("Fork choice: {:?}", e))); } diff --git a/beacon_node/http_api/src/publish_attestations.rs b/beacon_node/http_api/src/publish_attestations.rs index ed7f1ed17c..0065476532 100644 --- a/beacon_node/http_api/src/publish_attestations.rs +++ b/beacon_node/http_api/src/publish_attestations.rs @@ -87,7 +87,7 @@ fn verify_and_publish_attestation( .send(NetworkMessage::Publish { messages: vec![PubsubMessage::Attestation(Box::new(( attestation.subnet_id(), - attestation.attestation().clone(), + attestation.attestation().clone_as_attestation(), )))], }) .map_err(|_| Error::Publication)?; @@ -141,7 +141,7 @@ pub async fn publish_attestations( // move the `attestations` vec into the blocking task, so this small overhead is unavoidable. let attestation_metadata = attestations .iter() - .map(|att| (att.data.slot, att.data.index)) + .map(|att| (att.data().slot, att.committee_index())) .collect::>(); // Gossip validate and publish attestations that can be immediately processed. diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bf5dbe359e..86f2096224 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -35,8 +35,8 @@ use tokio::time::Duration; use tree_hash::TreeHash; use types::application_domain::ApplicationDomain; use types::{ - AggregateSignature, BitList, Domain, EthSpec, ExecutionBlockHash, Hash256, Keypair, - MainnetEthSpec, RelativeEpoch, SelectionProof, SignedRoot, Slot, + attestation::AttestationBase, AggregateSignature, BitList, Domain, EthSpec, ExecutionBlockHash, + Hash256, Keypair, MainnetEthSpec, RelativeEpoch, SelectionProof, SignedRoot, Slot, }; type E = MainnetEthSpec; @@ -1640,7 +1640,13 @@ impl ApiTester { let expected = block_id.full_block(&self.chain).await.ok().map( |(block, _execution_optimistic, _finalized)| { - block.message().body().attestations().clone().into() + block + .message() + .body() + .attestations() + .map(|att| att.clone_as_attestation()) + .collect::>() + .into() }, ); @@ -1676,7 +1682,7 @@ impl ApiTester { let mut attestations = Vec::new(); for attestation in &self.attestations { let mut invalid_attestation = attestation.clone(); - invalid_attestation.data.slot += 1; + invalid_attestation.data_mut().slot += 1; // add both to ensure we only fail on invalid attestations attestations.push(attestation.clone()); @@ -1800,7 +1806,14 @@ impl ApiTester { pub async fn test_post_beacon_pool_attester_slashings_invalid(mut self) -> Self { let mut slashing = self.attester_slashing.clone(); - slashing.attestation_1.data.slot += 1; + match &mut slashing { + AttesterSlashing::Base(ref mut slashing) => { + slashing.attestation_1.data.slot += 1; + } + AttesterSlashing::Electra(ref mut slashing) => { + slashing.attestation_1.data.slot += 1; + } + } self.client .post_beacon_pool_attester_slashings(&slashing) @@ -3175,7 +3188,8 @@ impl ApiTester { .chain .produce_unaggregated_attestation(slot, index) .unwrap() - .data; + .data() + .clone(); assert_eq!(result, expected); } @@ -3189,14 +3203,16 @@ impl ApiTester { .head_beacon_block() .message() .body() - .attestations()[0] - .clone(); + .attestations() + .next() + .unwrap() + .clone_as_attestation(); let result = self .client .get_validator_aggregate_attestation( - attestation.data.slot, - attestation.data.tree_hash_root(), + attestation.data().slot, + attestation.data().tree_hash_root(), ) .await .unwrap() @@ -3276,11 +3292,12 @@ impl ApiTester { .unwrap() .data; - let mut attestation = Attestation { + // TODO(electra) make fork-agnostic + let mut attestation = Attestation::Base(AttestationBase { aggregation_bits: BitList::with_capacity(duty.committee_length as usize).unwrap(), data: attestation_data, signature: AggregateSignature::infinity(), - }; + }); attestation .sign( @@ -3294,7 +3311,7 @@ impl ApiTester { SignedAggregateAndProof::from_aggregate( i as u64, - attestation, + attestation.to_ref(), Some(proof), &kp.sk, &fork, @@ -3318,8 +3335,14 @@ impl ApiTester { pub async fn test_get_validator_aggregate_and_proofs_invalid(mut self) -> Self { let mut aggregate = self.get_aggregate().await; - - aggregate.message.aggregate.data.slot += 1; + match &mut aggregate { + SignedAggregateAndProof::Base(ref mut aggregate) => { + aggregate.message.aggregate.data.slot += 1; + } + SignedAggregateAndProof::Electra(ref mut aggregate) => { + aggregate.message.aggregate.data.slot += 1; + } + } self.client .post_validator_aggregate_and_proof::(&[aggregate]) diff --git a/beacon_node/lighthouse_network/src/types/pubsub.rs b/beacon_node/lighthouse_network/src/types/pubsub.rs index ed63ad014c..b443ecd1b9 100644 --- a/beacon_node/lighthouse_network/src/types/pubsub.rs +++ b/beacon_node/lighthouse_network/src/types/pubsub.rs @@ -7,12 +7,14 @@ use ssz::{Decode, Encode}; use std::io::{Error, ErrorKind}; use std::sync::Arc; use types::{ - Attestation, AttesterSlashing, BlobSidecar, EthSpec, ForkContext, ForkName, + Attestation, AttestationBase, AttestationElectra, AttesterSlashing, AttesterSlashingBase, + AttesterSlashingElectra, BlobSidecar, EthSpec, ForkContext, ForkName, LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, - SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, - SignedBeaconBlockBellatrix, SignedBeaconBlockCapella, SignedBeaconBlockDeneb, - SignedBeaconBlockElectra, SignedBlsToExecutionChange, SignedContributionAndProof, - SignedVoluntaryExit, SubnetId, SyncCommitteeMessage, SyncSubnetId, + SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, + SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, + SignedBeaconBlockCapella, SignedBeaconBlockDeneb, SignedBeaconBlockElectra, + SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, SubnetId, + SyncCommitteeMessage, SyncSubnetId, }; #[derive(Debug, Clone, PartialEq)] @@ -154,15 +156,55 @@ impl PubsubMessage { // the ssz decoders match gossip_topic.kind() { GossipKind::BeaconAggregateAndProof => { - let agg_and_proof = SignedAggregateAndProof::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; + let signed_aggregate_and_proof = + match fork_context.from_context_bytes(gossip_topic.fork_digest) { + Some(&fork_name) => { + if fork_name.electra_enabled() { + SignedAggregateAndProof::Electra( + SignedAggregateAndProofElectra::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } else { + SignedAggregateAndProof::Base( + SignedAggregateAndProofBase::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } + } + None => { + return Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )) + } + }; Ok(PubsubMessage::AggregateAndProofAttestation(Box::new( - agg_and_proof, + signed_aggregate_and_proof, ))) } GossipKind::Attestation(subnet_id) => { let attestation = - Attestation::from_ssz_bytes(data).map_err(|e| format!("{:?}", e))?; + match fork_context.from_context_bytes(gossip_topic.fork_digest) { + Some(&fork_name) => { + if fork_name.electra_enabled() { + Attestation::Electra( + AttestationElectra::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } else { + Attestation::Base( + AttestationBase::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } + } + None => { + return Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )) + } + }; Ok(PubsubMessage::Attestation(Box::new(( *subnet_id, attestation, @@ -239,8 +281,28 @@ impl PubsubMessage { Ok(PubsubMessage::ProposerSlashing(Box::new(proposer_slashing))) } GossipKind::AttesterSlashing => { - let attester_slashing = AttesterSlashing::from_ssz_bytes(data) - .map_err(|e| format!("{:?}", e))?; + let attester_slashing = + match fork_context.from_context_bytes(gossip_topic.fork_digest) { + Some(&fork_name) => { + if fork_name.electra_enabled() { + AttesterSlashing::Electra( + AttesterSlashingElectra::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } else { + AttesterSlashing::Base( + AttesterSlashingBase::from_ssz_bytes(data) + .map_err(|e| format!("{:?}", e))?, + ) + } + } + None => { + return Err(format!( + "Unknown gossipsub fork digest: {:?}", + gossip_topic.fork_digest + )) + } + }; Ok(PubsubMessage::AttesterSlashing(Box::new(attester_slashing))) } GossipKind::SignedContributionAndProof => { @@ -342,15 +404,17 @@ impl std::fmt::Display for PubsubMessage { ), PubsubMessage::AggregateAndProofAttestation(att) => write!( f, - "Aggregate and Proof: slot: {}, index: {}, aggregator_index: {}", - att.message.aggregate.data.slot, - att.message.aggregate.data.index, - att.message.aggregator_index, + "Aggregate and Proof: slot: {}, index: {:?}, aggregator_index: {}", + att.message().aggregate().data().slot, + att.message().aggregate().committee_index(), + att.message().aggregator_index(), ), PubsubMessage::Attestation(data) => write!( f, - "Attestation: subnet_id: {}, attestation_slot: {}, attestation_index: {}", - *data.0, data.1.data.slot, data.1.data.index, + "Attestation: subnet_id: {}, attestation_slot: {}, attestation_index: {:?}", + *data.0, + data.1.data().slot, + data.1.committee_index(), ), PubsubMessage::VoluntaryExit(_data) => write!(f, "Voluntary Exit"), PubsubMessage::ProposerSlashing(_data) => write!(f, "Proposer Slashing"), diff --git a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs index f178fa56c3..8baa0f773d 100644 --- a/beacon_node/network/src/network_beacon_processor/gossip_methods.rs +++ b/beacon_node/network/src/network_beacon_processor/gossip_methods.rs @@ -31,9 +31,9 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use store::hot_cold_store::HotColdDBError; use tokio::sync::mpsc; use types::{ - beacon_block::BlockImportSource, Attestation, AttesterSlashing, BlobSidecar, EthSpec, Hash256, - IndexedAttestation, LightClientFinalityUpdate, LightClientOptimisticUpdate, ProposerSlashing, - SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, + beacon_block::BlockImportSource, Attestation, AttestationRef, AttesterSlashing, BlobSidecar, + EthSpec, Hash256, IndexedAttestation, LightClientFinalityUpdate, LightClientOptimisticUpdate, + ProposerSlashing, SignedAggregateAndProof, SignedBeaconBlock, SignedBlsToExecutionChange, SignedContributionAndProof, SignedVoluntaryExit, Slot, SubnetId, SyncCommitteeMessage, SyncSubnetId, }; @@ -62,8 +62,8 @@ struct VerifiedUnaggregate { /// This implementation allows `Self` to be imported to fork choice and other functions on the /// `BeaconChain`. impl VerifiedAttestation for VerifiedUnaggregate { - fn attestation(&self) -> &Attestation { - &self.attestation + fn attestation(&self) -> AttestationRef { + self.attestation.to_ref() } fn indexed_attestation(&self) -> &IndexedAttestation { @@ -72,7 +72,7 @@ impl VerifiedAttestation for VerifiedUnaggregate { fn into_attestation_and_indices(self) -> (Attestation, Vec) { let attestation = *self.attestation; - let attesting_indices = self.indexed_attestation.attesting_indices.into(); + let attesting_indices = self.indexed_attestation.attesting_indices_to_vec(); (attestation, attesting_indices) } } @@ -95,8 +95,8 @@ struct VerifiedAggregate { /// This implementation allows `Self` to be imported to fork choice and other functions on the /// `BeaconChain`. impl VerifiedAttestation for VerifiedAggregate { - fn attestation(&self) -> &Attestation { - &self.signed_aggregate.message.aggregate + fn attestation(&self) -> AttestationRef { + self.signed_aggregate.message().aggregate() } fn indexed_attestation(&self) -> &IndexedAttestation { @@ -105,8 +105,8 @@ impl VerifiedAttestation for VerifiedAggregate { /// Efficient clone-free implementation that moves out of the `Box`. fn into_attestation_and_indices(self) -> (Attestation, Vec) { - let attestation = self.signed_aggregate.message.aggregate; - let attesting_indices = self.indexed_attestation.attesting_indices.into(); + let attestation = self.signed_aggregate.into_attestation(); + let attesting_indices = self.indexed_attestation.attesting_indices_to_vec(); (attestation, attesting_indices) } } @@ -133,7 +133,7 @@ enum FailedAtt { impl FailedAtt { pub fn beacon_block_root(&self) -> &Hash256 { - &self.attestation().data.beacon_block_root + &self.attestation().data().beacon_block_root } pub fn kind(&self) -> &'static str { @@ -143,10 +143,10 @@ impl FailedAtt { } } - pub fn attestation(&self) -> &Attestation { + pub fn attestation(&self) -> AttestationRef { match self { - FailedAtt::Unaggregate { attestation, .. } => attestation, - FailedAtt::Aggregate { attestation, .. } => &attestation.message.aggregate, + FailedAtt::Unaggregate { attestation, .. } => attestation.to_ref(), + FailedAtt::Aggregate { attestation, .. } => attestation.message().aggregate(), } } } @@ -309,7 +309,7 @@ impl NetworkBeaconProcessor { match result { Ok(verified_attestation) => { let indexed_attestation = &verified_attestation.indexed_attestation; - let beacon_block_root = indexed_attestation.data.beacon_block_root; + let beacon_block_root = indexed_attestation.data().beacon_block_root; // Register the attestation with any monitored validators. self.chain @@ -412,7 +412,7 @@ impl NetworkBeaconProcessor { reprocess_tx: Option>, seen_timestamp: Duration, ) { - let beacon_block_root = aggregate.message.aggregate.data.beacon_block_root; + let beacon_block_root = aggregate.message().aggregate().data().beacon_block_root; let result = match self .chain @@ -1476,7 +1476,7 @@ impl NetworkBeaconProcessor { self.chain .validator_monitor .read() - .register_gossip_attester_slashing(slashing.as_inner()); + .register_gossip_attester_slashing(slashing.as_inner().to_ref()); self.chain.import_attester_slashing(slashing); debug!(self.log, "Successfully imported attester slashing"); @@ -2077,6 +2077,27 @@ impl NetworkBeaconProcessor { "attn_val_index_too_high", ); } + AttnError::CommitteeIndexNonZero(index) => { + /* + * The validator index is not set to zero after Electra. + * + * The peer has published an invalid consensus message. + */ + debug!( + self.log, + "Committee index non zero"; + "peer_id" => %peer_id, + "block" => ?beacon_block_root, + "type" => ?attestation_type, + "committee_index" => index, + ); + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "attn_comm_index_non_zero", + ); + } AttnError::UnknownHeadBlock { beacon_block_root } => { trace!( self.log, @@ -2232,6 +2253,19 @@ impl NetworkBeaconProcessor { "attn_too_many_agg_bits", ); } + AttnError::NotExactlyOneCommitteeBitSet(_) => { + /* + * The attestation doesn't have only one committee bit set. + * + * The peer has published an invalid consensus message. + */ + self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Reject); + self.gossip_penalize_peer( + peer_id, + PeerAction::LowToleranceError, + "attn_too_many_comm_bits", + ); + } AttnError::AttestsToFutureBlock { .. } => { /* * The beacon_block_root is from a higher slot than the attestation. @@ -2330,7 +2364,7 @@ impl NetworkBeaconProcessor { self.log, "Ignored attestation to finalized block"; "block_root" => ?beacon_block_root, - "attestation_slot" => failed_att.attestation().data.slot, + "attestation_slot" => failed_att.attestation().data().slot, ); self.propagate_validation_result(message_id, peer_id, MessageAcceptance::Ignore); @@ -2353,9 +2387,9 @@ impl NetworkBeaconProcessor { debug!( self.log, "Dropping attestation"; - "target_root" => ?failed_att.attestation().data.target.root, + "target_root" => ?failed_att.attestation().data().target.root, "beacon_block_root" => ?beacon_block_root, - "slot" => ?failed_att.attestation().data.slot, + "slot" => ?failed_att.attestation().data().slot, "type" => ?attestation_type, "error" => ?e, "peer_id" => % peer_id @@ -2374,7 +2408,7 @@ impl NetworkBeaconProcessor { self.log, "Unable to validate attestation"; "beacon_block_root" => ?beacon_block_root, - "slot" => ?failed_att.attestation().data.slot, + "slot" => ?failed_att.attestation().data().slot, "type" => ?attestation_type, "peer_id" => %peer_id, "error" => ?e, @@ -2771,7 +2805,7 @@ impl NetworkBeaconProcessor { /// timely), propagate it on gossip. Otherwise, ignore it. fn propagate_attestation_if_timely( &self, - attestation: &Attestation, + attestation: AttestationRef, message_id: MessageId, peer_id: PeerId, ) { diff --git a/beacon_node/network/src/network_beacon_processor/mod.rs b/beacon_node/network/src/network_beacon_processor/mod.rs index cabe39f929..ccdbb10720 100644 --- a/beacon_node/network/src/network_beacon_processor/mod.rs +++ b/beacon_node/network/src/network_beacon_processor/mod.rs @@ -142,7 +142,7 @@ impl NetworkBeaconProcessor { processor.process_gossip_aggregate_batch(aggregates, Some(reprocess_tx)) }; - let beacon_block_root = aggregate.message.aggregate.data.beacon_block_root; + let beacon_block_root = aggregate.message().aggregate().data().beacon_block_root; self.try_send(BeaconWorkEvent { drop_during_sync: true, work: Work::GossipAggregate { diff --git a/beacon_node/network/src/subnet_service/attestation_subnets.rs b/beacon_node/network/src/subnet_service/attestation_subnets.rs index 92c6bb6c3e..830c43cbb1 100644 --- a/beacon_node/network/src/subnet_service/attestation_subnets.rs +++ b/beacon_node/network/src/subnet_service/attestation_subnets.rs @@ -393,7 +393,7 @@ impl AttestationService { .map(|tracked_vals| { tracked_vals.contains_key(&ExactSubnet { subnet_id: subnet, - slot: attestation.data.slot, + slot: attestation.data().slot, }) }) .unwrap_or(true) diff --git a/beacon_node/operation_pool/src/attestation.rs b/beacon_node/operation_pool/src/attestation.rs index 5c6f684e72..97d0583e34 100644 --- a/beacon_node/operation_pool/src/attestation.rs +++ b/beacon_node/operation_pool/src/attestation.rs @@ -1,8 +1,8 @@ -use crate::attestation_storage::AttestationRef; +use crate::attestation_storage::{CompactAttestationRef, CompactIndexedAttestation}; use crate::max_cover::MaxCover; use crate::reward_cache::RewardCache; use state_processing::common::{ - base, get_attestation_participation_flag_indices, get_attesting_indices, + attesting_indices_base::get_attesting_indices, base, get_attestation_participation_flag_indices, }; use std::collections::HashMap; use types::{ @@ -14,14 +14,14 @@ use types::{ #[derive(Debug, Clone)] pub struct AttMaxCover<'a, E: EthSpec> { /// Underlying attestation. - pub att: AttestationRef<'a, E>, + pub att: CompactAttestationRef<'a, E>, /// Mapping of validator indices and their rewards. pub fresh_validators_rewards: HashMap, } impl<'a, E: EthSpec> AttMaxCover<'a, E> { pub fn new( - att: AttestationRef<'a, E>, + att: CompactAttestationRef<'a, E>, state: &BeaconState, reward_cache: &'a RewardCache, total_active_balance: u64, @@ -36,7 +36,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { /// Initialise an attestation cover object for base/phase0 hard fork. pub fn new_for_base( - att: AttestationRef<'a, E>, + att: CompactAttestationRef<'a, E>, state: &BeaconState, base_state: &BeaconStateBase, total_active_balance: u64, @@ -69,7 +69,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { /// Initialise an attestation cover object for Altair or later. pub fn new_for_altair_deneb( - att: AttestationRef<'a, E>, + att: CompactAttestationRef<'a, E>, state: &BeaconState, reward_cache: &'a RewardCache, spec: &ChainSpec, @@ -83,7 +83,7 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { let fresh_validators_rewards = att .indexed - .attesting_indices + .attesting_indices() .iter() .filter_map(|&index| { if reward_cache @@ -119,14 +119,14 @@ impl<'a, E: EthSpec> AttMaxCover<'a, E> { impl<'a, E: EthSpec> MaxCover for AttMaxCover<'a, E> { type Object = Attestation; - type Intermediate = AttestationRef<'a, E>; + type Intermediate = CompactAttestationRef<'a, E>; type Set = HashMap; - fn intermediate(&self) -> &AttestationRef<'a, E> { + fn intermediate(&self) -> &CompactAttestationRef<'a, E> { &self.att } - fn convert_to_object(att_ref: &AttestationRef<'a, E>) -> Attestation { + fn convert_to_object(att_ref: &CompactAttestationRef<'a, E>) -> Attestation { att_ref.clone_as_attestation() } @@ -144,9 +144,16 @@ impl<'a, E: EthSpec> MaxCover for AttMaxCover<'a, E> { /// because including two attestations on chain to satisfy different participation bits is /// impossible without the validator double voting. I.e. it is only suboptimal in the presence /// of slashable voting, which is rare. + /// + /// Post-Electra this optimisation is still OK. The `self.att.data.index` will always be 0 for + /// all Electra attestations, so when a new attestation is added to the solution, we will + /// remove its validators from all attestations at the same slot. It may happen that the + /// included attestation and the attestation being updated have no validators in common, in + /// which case the `retain` will be a no-op. We could consider optimising this in future by only + /// executing the `retain` when the `committee_bits` of the two attestations intersect. fn update_covering_set( &mut self, - best_att: &AttestationRef<'a, E>, + best_att: &CompactAttestationRef<'a, E>, covered_validators: &HashMap, ) { if self.att.data.slot == best_att.data.slot && self.att.data.index == best_att.data.index { @@ -170,12 +177,16 @@ impl<'a, E: EthSpec> MaxCover for AttMaxCover<'a, E> { /// /// This isn't optimal, but with the Altair fork this code is obsolete and not worth upgrading. pub fn earliest_attestation_validators( - attestation: &AttestationRef, + attestation: &CompactAttestationRef, state: &BeaconState, base_state: &BeaconStateBase, ) -> BitList { // Bitfield of validators whose attestations are new/fresh. - let mut new_validators = attestation.indexed.aggregation_bits.clone(); + let mut new_validators = match attestation.indexed { + CompactIndexedAttestation::Base(indexed_att) => indexed_att.aggregation_bits.clone(), + // This code path is obsolete post altair fork, so we just return an empty bitlist here. + CompactIndexedAttestation::Electra(_) => return BitList::with_capacity(0).unwrap(), + }; let state_attestations = if attestation.checkpoint.target_epoch == state.current_epoch() { &base_state.current_epoch_attestations diff --git a/beacon_node/operation_pool/src/attestation_storage.rs b/beacon_node/operation_pool/src/attestation_storage.rs index 43fdf3923b..4de9d351f3 100644 --- a/beacon_node/operation_pool/src/attestation_storage.rs +++ b/beacon_node/operation_pool/src/attestation_storage.rs @@ -1,9 +1,10 @@ use crate::AttestationStats; use itertools::Itertools; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use types::{ - AggregateSignature, Attestation, AttestationData, BeaconState, BitList, Checkpoint, Epoch, - EthSpec, Hash256, Slot, + attestation::{AttestationBase, AttestationElectra}, + superstruct, AggregateSignature, Attestation, AttestationData, BeaconState, BitList, BitVector, + Checkpoint, Epoch, EthSpec, Hash256, Slot, Unsigned, }; #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] @@ -20,11 +21,17 @@ pub struct CompactAttestationData { pub target_root: Hash256, } +#[superstruct(variants(Base, Electra), variant_attributes(derive(Debug, PartialEq,)))] #[derive(Debug, PartialEq)] pub struct CompactIndexedAttestation { pub attesting_indices: Vec, + #[superstruct(only(Base), partial_getter(rename = "aggregation_bits_base"))] pub aggregation_bits: BitList, + #[superstruct(only(Electra), partial_getter(rename = "aggregation_bits_electra"))] + pub aggregation_bits: BitList, pub signature: AggregateSignature, + #[superstruct(only(Electra))] + pub committee_bits: BitVector, } #[derive(Debug)] @@ -35,7 +42,7 @@ pub struct SplitAttestation { } #[derive(Debug, Clone)] -pub struct AttestationRef<'a, E: EthSpec> { +pub struct CompactAttestationRef<'a, E: EthSpec> { pub checkpoint: &'a CheckpointKey, pub data: &'a CompactAttestationData, pub indexed: &'a CompactIndexedAttestation, @@ -54,20 +61,34 @@ pub struct AttestationDataMap { impl SplitAttestation { pub fn new(attestation: Attestation, attesting_indices: Vec) -> Self { let checkpoint = CheckpointKey { - source: attestation.data.source, - target_epoch: attestation.data.target.epoch, + source: attestation.data().source, + target_epoch: attestation.data().target.epoch, }; let data = CompactAttestationData { - slot: attestation.data.slot, - index: attestation.data.index, - beacon_block_root: attestation.data.beacon_block_root, - target_root: attestation.data.target.root, + slot: attestation.data().slot, + index: attestation.data().index, + beacon_block_root: attestation.data().beacon_block_root, + target_root: attestation.data().target.root, }; - let indexed = CompactIndexedAttestation { - attesting_indices, - aggregation_bits: attestation.aggregation_bits, - signature: attestation.signature, + + let indexed = match attestation.clone() { + Attestation::Base(attn) => { + CompactIndexedAttestation::Base(CompactIndexedAttestationBase { + attesting_indices, + aggregation_bits: attn.aggregation_bits, + signature: attestation.signature().clone(), + }) + } + Attestation::Electra(attn) => { + CompactIndexedAttestation::Electra(CompactIndexedAttestationElectra { + attesting_indices, + aggregation_bits: attn.aggregation_bits, + signature: attestation.signature().clone(), + committee_bits: attn.committee_bits, + }) + } }; + Self { checkpoint, data, @@ -75,8 +96,8 @@ impl SplitAttestation { } } - pub fn as_ref(&self) -> AttestationRef { - AttestationRef { + pub fn as_ref(&self) -> CompactAttestationRef { + CompactAttestationRef { checkpoint: &self.checkpoint, data: &self.data, indexed: &self.indexed, @@ -84,7 +105,7 @@ impl SplitAttestation { } } -impl<'a, E: EthSpec> AttestationRef<'a, E> { +impl<'a, E: EthSpec> CompactAttestationRef<'a, E> { pub fn attestation_data(&self) -> AttestationData { AttestationData { slot: self.data.slot, @@ -99,10 +120,20 @@ impl<'a, E: EthSpec> AttestationRef<'a, E> { } pub fn clone_as_attestation(&self) -> Attestation { - Attestation { - aggregation_bits: self.indexed.aggregation_bits.clone(), - data: self.attestation_data(), - signature: self.indexed.signature.clone(), + match self.indexed { + CompactIndexedAttestation::Base(indexed_att) => Attestation::Base(AttestationBase { + aggregation_bits: indexed_att.aggregation_bits.clone(), + data: self.attestation_data(), + signature: indexed_att.signature.clone(), + }), + CompactIndexedAttestation::Electra(indexed_att) => { + Attestation::Electra(AttestationElectra { + aggregation_bits: indexed_att.aggregation_bits.clone(), + data: self.attestation_data(), + signature: indexed_att.signature.clone(), + committee_bits: indexed_att.committee_bits.clone(), + }) + } } } } @@ -125,7 +156,37 @@ impl CheckpointKey { } impl CompactIndexedAttestation { - pub fn signers_disjoint_from(&self, other: &Self) -> bool { + pub fn should_aggregate(&self, other: &Self) -> bool { + match (self, other) { + (CompactIndexedAttestation::Base(this), CompactIndexedAttestation::Base(other)) => { + this.should_aggregate(other) + } + ( + CompactIndexedAttestation::Electra(this), + CompactIndexedAttestation::Electra(other), + ) => this.should_aggregate(other), + _ => false, + } + } + + /// Returns `true` if aggregated, otherwise `false`. + pub fn aggregate(&mut self, other: &Self) -> bool { + match (self, other) { + (CompactIndexedAttestation::Base(this), CompactIndexedAttestation::Base(other)) => { + this.aggregate(other); + true + } + ( + CompactIndexedAttestation::Electra(this), + CompactIndexedAttestation::Electra(other), + ) => this.aggregate_same_committee(other), + _ => false, + } + } +} + +impl CompactIndexedAttestationBase { + pub fn should_aggregate(&self, other: &Self) -> bool { self.aggregation_bits .intersection(&other.aggregation_bits) .is_zero() @@ -143,13 +204,108 @@ impl CompactIndexedAttestation { } } +impl CompactIndexedAttestationElectra { + pub fn should_aggregate(&self, other: &Self) -> bool { + // For Electra, only aggregate attestations in the same committee. + self.committee_bits == other.committee_bits + && self + .aggregation_bits + .intersection(&other.aggregation_bits) + .is_zero() + } + + /// Returns `true` if aggregated, otherwise `false`. + pub fn aggregate_same_committee(&mut self, other: &Self) -> bool { + if self.committee_bits != other.committee_bits { + return false; + } + self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); + self.attesting_indices = self + .attesting_indices + .drain(..) + .merge(other.attesting_indices.iter().copied()) + .dedup() + .collect(); + self.signature.add_assign_aggregate(&other.signature); + true + } + + pub fn aggregate_with_disjoint_committees(&mut self, other: &Self) -> Option<()> { + if !self + .committee_bits + .intersection(&other.committee_bits) + .is_zero() + { + return None; + } + // The attestation being aggregated in must only have 1 committee bit set. + if other.committee_bits.num_set_bits() != 1 { + return None; + } + + // Check we are aggregating in increasing committee index order (so we can append + // aggregation bits). + if self.committee_bits.highest_set_bit() >= other.committee_bits.highest_set_bit() { + return None; + } + + self.committee_bits = self.committee_bits.union(&other.committee_bits); + if let Some(agg_bits) = bitlist_extend(&self.aggregation_bits, &other.aggregation_bits) { + self.aggregation_bits = agg_bits; + + self.attesting_indices = self + .attesting_indices + .drain(..) + .merge(other.attesting_indices.iter().copied()) + .dedup() + .collect(); + self.signature.add_assign_aggregate(&other.signature); + + return Some(()); + } + + None + } + + pub fn committee_index(&self) -> Option { + self.get_committee_indices().first().copied() + } + + pub fn get_committee_indices(&self) -> Vec { + self.committee_bits + .iter() + .enumerate() + .filter_map(|(index, bit)| if bit { Some(index as u64) } else { None }) + .collect() + } +} + +// TODO(electra): upstream this or a more efficient implementation +fn bitlist_extend(list1: &BitList, list2: &BitList) -> Option> { + let new_length = list1.len() + list2.len(); + let mut list = BitList::::with_capacity(new_length).ok()?; + + // Copy bits from list1. + for (i, bit) in list1.iter().enumerate() { + list.set(i, bit).ok()?; + } + + // Copy bits from list2, starting from the end of list1. + let offset = list1.len(); + for (i, bit) in list2.iter().enumerate() { + list.set(offset + i, bit).ok()?; + } + + Some(list) +} + impl AttestationMap { pub fn insert(&mut self, attestation: Attestation, attesting_indices: Vec) { let SplitAttestation { checkpoint, data, indexed, - } = SplitAttestation::new(attestation, attesting_indices); + } = SplitAttestation::new(attestation.clone(), attesting_indices); let attestation_map = self.checkpoint_map.entry(checkpoint).or_default(); let attestations = attestation_map.attestations.entry(data).or_default(); @@ -158,10 +314,10 @@ impl AttestationMap { // NOTE: this is sub-optimal and in future we will remove this in favour of max-clique // aggregation. let mut aggregated = false; + for existing_attestation in attestations.iter_mut() { - if existing_attestation.signers_disjoint_from(&indexed) { - existing_attestation.aggregate(&indexed); - aggregated = true; + if existing_attestation.should_aggregate(&indexed) { + aggregated = existing_attestation.aggregate(&indexed); } else if *existing_attestation == indexed { aggregated = true; } @@ -172,11 +328,93 @@ impl AttestationMap { } } + /// Aggregate Electra attestations for the same attestation data signed by different + /// committees. + /// + /// Non-Electra attestations are left as-is. + pub fn aggregate_across_committees(&mut self, checkpoint_key: CheckpointKey) { + let Some(attestation_map) = self.checkpoint_map.get_mut(&checkpoint_key) else { + return; + }; + for compact_indexed_attestations in attestation_map.attestations.values_mut() { + let unaggregated_attestations = std::mem::take(compact_indexed_attestations); + let mut aggregated_attestations: Vec> = vec![]; + + // Aggregate the best attestations for each committee and leave the rest. + let mut best_attestations_by_committee: BTreeMap< + u64, + CompactIndexedAttestationElectra, + > = BTreeMap::new(); + + for committee_attestation in unaggregated_attestations { + let mut electra_attestation = match committee_attestation { + CompactIndexedAttestation::Electra(att) + if att.committee_bits.num_set_bits() == 1 => + { + att + } + CompactIndexedAttestation::Electra(att) => { + // Aggregate already covers multiple committees, leave it as-is. + aggregated_attestations.push(CompactIndexedAttestation::Electra(att)); + continue; + } + CompactIndexedAttestation::Base(att) => { + // Leave as-is. + aggregated_attestations.push(CompactIndexedAttestation::Base(att)); + continue; + } + }; + if let Some(committee_index) = electra_attestation.committee_index() { + if let Some(existing_attestation) = + best_attestations_by_committee.get_mut(&committee_index) + { + // Search for the best (most aggregation bits) attestation for this committee + // index. + if electra_attestation.aggregation_bits.num_set_bits() + > existing_attestation.aggregation_bits.num_set_bits() + { + // New attestation is better than the previously known one for this + // committee. Replace it. + std::mem::swap(existing_attestation, &mut electra_attestation); + } + // Put the inferior attestation into the list of aggregated attestations + // without performing any cross-committee aggregation. + aggregated_attestations + .push(CompactIndexedAttestation::Electra(electra_attestation)); + } else { + // First attestation seen for this committee. Place it in the map + // provisionally. + best_attestations_by_committee.insert(committee_index, electra_attestation); + } + } + } + + if let Some(on_chain_aggregate) = + Self::compute_on_chain_aggregate(best_attestations_by_committee) + { + aggregated_attestations + .push(CompactIndexedAttestation::Electra(on_chain_aggregate)); + } + + *compact_indexed_attestations = aggregated_attestations; + } + } + + pub fn compute_on_chain_aggregate( + mut attestations_by_committee: BTreeMap>, + ) -> Option> { + let (_, mut on_chain_aggregate) = attestations_by_committee.pop_first()?; + for (_, attestation) in attestations_by_committee { + on_chain_aggregate.aggregate_with_disjoint_committees(&attestation); + } + Some(on_chain_aggregate) + } + /// Iterate all attestations matching the given `checkpoint_key`. pub fn get_attestations<'a>( &'a self, checkpoint_key: &'a CheckpointKey, - ) -> impl Iterator> + 'a { + ) -> impl Iterator> + 'a { self.checkpoint_map .get(checkpoint_key) .into_iter() @@ -184,7 +422,7 @@ impl AttestationMap { } /// Iterate all attestations in the map. - pub fn iter(&self) -> impl Iterator> { + pub fn iter(&self) -> impl Iterator> { self.checkpoint_map .iter() .flat_map(|(checkpoint_key, attestation_map)| attestation_map.iter(checkpoint_key)) @@ -215,9 +453,9 @@ impl AttestationDataMap { pub fn iter<'a>( &'a self, checkpoint_key: &'a CheckpointKey, - ) -> impl Iterator> + 'a { + ) -> impl Iterator> + 'a { self.attestations.iter().flat_map(|(data, vec_indexed)| { - vec_indexed.iter().map(|indexed| AttestationRef { + vec_indexed.iter().map(|indexed| CompactAttestationRef { checkpoint: checkpoint_key, data, indexed, diff --git a/beacon_node/operation_pool/src/attester_slashing.rs b/beacon_node/operation_pool/src/attester_slashing.rs index 725d4d2a85..c2411d4d72 100644 --- a/beacon_node/operation_pool/src/attester_slashing.rs +++ b/beacon_node/operation_pool/src/attester_slashing.rs @@ -1,17 +1,17 @@ use crate::max_cover::MaxCover; use state_processing::per_block_processing::get_slashable_indices_modular; use std::collections::{HashMap, HashSet}; -use types::{AttesterSlashing, BeaconState, EthSpec}; +use types::{AttesterSlashing, AttesterSlashingRef, BeaconState, EthSpec}; #[derive(Debug, Clone)] pub struct AttesterSlashingMaxCover<'a, E: EthSpec> { - slashing: &'a AttesterSlashing, + slashing: AttesterSlashingRef<'a, E>, effective_balances: HashMap, } impl<'a, E: EthSpec> AttesterSlashingMaxCover<'a, E> { pub fn new( - slashing: &'a AttesterSlashing, + slashing: AttesterSlashingRef<'a, E>, proposer_slashing_indices: &HashSet, state: &BeaconState, ) -> Option { @@ -39,16 +39,16 @@ impl<'a, E: EthSpec> AttesterSlashingMaxCover<'a, E> { impl<'a, E: EthSpec> MaxCover for AttesterSlashingMaxCover<'a, E> { /// The result type, of which we would eventually like a collection of maximal quality. type Object = AttesterSlashing; - type Intermediate = AttesterSlashing; + type Intermediate = AttesterSlashingRef<'a, E>; /// The type used to represent sets. type Set = HashMap; - fn intermediate(&self) -> &AttesterSlashing { - self.slashing + fn intermediate(&self) -> &AttesterSlashingRef<'a, E> { + &self.slashing } - fn convert_to_object(slashing: &AttesterSlashing) -> AttesterSlashing { - slashing.clone() + fn convert_to_object(slashing: &AttesterSlashingRef<'a, E>) -> AttesterSlashing { + slashing.clone_as_attester_slashing() } /// Get the set of elements covered. @@ -58,7 +58,7 @@ impl<'a, E: EthSpec> MaxCover for AttesterSlashingMaxCover<'a, E> { /// Update the set of items covered, for the inclusion of some object in the solution. fn update_covering_set( &mut self, - _best_slashing: &AttesterSlashing, + _best_slashing: &AttesterSlashingRef<'a, E>, covered_validator_indices: &HashMap, ) { self.effective_balances diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index c1b4aeb3e4..a1c9ada03a 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -11,11 +11,10 @@ mod sync_aggregate_id; pub use crate::bls_to_execution_changes::ReceivedPreCapella; pub use attestation::{earliest_attestation_validators, AttMaxCover}; -pub use attestation_storage::{AttestationRef, SplitAttestation}; +pub use attestation_storage::{CompactAttestationRef, SplitAttestation}; pub use max_cover::MaxCover; pub use persistence::{ - PersistedOperationPool, PersistedOperationPoolV12, PersistedOperationPoolV14, - PersistedOperationPoolV15, PersistedOperationPoolV5, + PersistedOperationPool, PersistedOperationPoolV15, PersistedOperationPoolV20, }; pub use reward_cache::RewardCache; use state_processing::epoch_cache::is_epoch_cache_initialized; @@ -228,7 +227,7 @@ impl OperationPool { state: &'a BeaconState, reward_cache: &'a RewardCache, total_active_balance: u64, - validity_filter: impl FnMut(&AttestationRef<'a, E>) -> bool + Send, + validity_filter: impl FnMut(&CompactAttestationRef<'a, E>) -> bool + Send, spec: &'a ChainSpec, ) -> impl Iterator> + Send { all_attestations @@ -252,10 +251,11 @@ impl OperationPool { pub fn get_attestations( &self, state: &BeaconState, - prev_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, E>) -> bool + Send, - curr_epoch_validity_filter: impl for<'a> FnMut(&AttestationRef<'a, E>) -> bool + Send, + prev_epoch_validity_filter: impl for<'a> FnMut(&CompactAttestationRef<'a, E>) -> bool + Send, + curr_epoch_validity_filter: impl for<'a> FnMut(&CompactAttestationRef<'a, E>) -> bool + Send, spec: &ChainSpec, ) -> Result>, OpPoolError> { + let fork_name = state.fork_name_unchecked(); if !matches!(state, BeaconState::Base(_)) { // Epoch cache must be initialized to fetch base reward values in the max cover `score` // function. Currently max cover ignores items on errors. If epoch cache is not @@ -267,7 +267,6 @@ impl OperationPool { // Attestations for the current fork, which may be from the current or previous epoch. let (prev_epoch_key, curr_epoch_key) = CheckpointKey::keys_for_state(state); - let all_attestations = self.attestations.read(); let total_active_balance = state .get_total_active_balance() .map_err(OpPoolError::GetAttestationsTotalBalanceError)?; @@ -284,6 +283,16 @@ impl OperationPool { let mut num_prev_valid = 0_i64; let mut num_curr_valid = 0_i64; + // TODO(electra): Work out how to do this more elegantly. This is a bit of a hack. + let mut all_attestations = self.attestations.write(); + + if fork_name.electra_enabled() { + all_attestations.aggregate_across_committees(prev_epoch_key); + all_attestations.aggregate_across_committees(curr_epoch_key); + } + + let all_attestations = parking_lot::RwLockWriteGuard::downgrade(all_attestations); + let prev_epoch_att = self .get_valid_attestations_for_epoch( &prev_epoch_key, @@ -307,6 +316,11 @@ impl OperationPool { ) .inspect(|_| num_curr_valid += 1); + let curr_epoch_limit = if fork_name.electra_enabled() { + E::MaxAttestationsElectra::to_usize() + } else { + E::MaxAttestations::to_usize() + }; let prev_epoch_limit = if let BeaconState::Base(base_state) = state { std::cmp::min( E::MaxPendingAttestations::to_usize() @@ -314,7 +328,7 @@ impl OperationPool { E::MaxAttestations::to_usize(), ) } else { - E::MaxAttestations::to_usize() + curr_epoch_limit }; let (prev_cover, curr_cover) = rayon::join( @@ -329,11 +343,7 @@ impl OperationPool { }, move || { let _timer = metrics::start_timer(&metrics::ATTESTATION_CURR_EPOCH_PACKING_TIME); - maximum_cover( - curr_epoch_att, - E::MaxAttestations::to_usize(), - "curr_epoch_attestations", - ) + maximum_cover(curr_epoch_att, curr_epoch_limit, "curr_epoch_attestations") }, ); @@ -343,7 +353,7 @@ impl OperationPool { Ok(max_cover::merge_solutions( curr_cover, prev_cover, - E::MaxAttestations::to_usize(), + curr_epoch_limit, )) } @@ -428,7 +438,7 @@ impl OperationPool { let relevant_attester_slashings = reader.iter().flat_map(|slashing| { if slashing.signature_is_still_valid(&state.fork()) { - AttesterSlashingMaxCover::new(slashing.as_inner(), to_be_slashed, state) + AttesterSlashingMaxCover::new(slashing.as_inner().to_ref(), to_be_slashed, state) } else { None } @@ -442,7 +452,7 @@ impl OperationPool { .into_iter() .map(|cover| { to_be_slashed.extend(cover.covering_set().keys()); - cover.intermediate().clone() + AttesterSlashingMaxCover::convert_to_object(cover.intermediate()) }) .collect() } @@ -463,16 +473,19 @@ impl OperationPool { // Check that the attestation's signature is still valid wrt the fork version. let signature_ok = slashing.signature_is_still_valid(&head_state.fork()); // Slashings that don't slash any validators can also be dropped. - let slashing_ok = - get_slashable_indices_modular(head_state, slashing.as_inner(), |_, validator| { + let slashing_ok = get_slashable_indices_modular( + head_state, + slashing.as_inner().to_ref(), + |_, validator| { // Declare that a validator is still slashable if they have not exited prior // to the finalized epoch. // // We cannot check the `slashed` field since the `head` is not finalized and // a fork could un-slash someone. validator.exit_epoch > head_state.finalized_checkpoint().epoch - }) - .map_or(false, |indices| !indices.is_empty()); + }, + ) + .map_or(false, |indices| !indices.is_empty()); signature_ok && slashing_ok }); @@ -891,7 +904,7 @@ mod release_tests { ); for (atts, aggregate) in &attestations { - let att2 = aggregate.as_ref().unwrap().message.aggregate.clone(); + let att2 = aggregate.as_ref().unwrap().message().aggregate().clone(); let att1 = atts .into_iter() @@ -899,7 +912,7 @@ mod release_tests { .take(2) .fold::>, _>(None, |att, new_att| { if let Some(mut a) = att { - a.aggregate(&new_att); + a.aggregate(new_att.to_ref()); Some(a) } else { Some(new_att.clone()) @@ -907,13 +920,13 @@ mod release_tests { }) .unwrap(); - let att1_indices = get_attesting_indices_from_state(&state, &att1).unwrap(); - let att2_indices = get_attesting_indices_from_state(&state, &att2).unwrap(); + let att1_indices = get_attesting_indices_from_state(&state, att1.to_ref()).unwrap(); + let att2_indices = get_attesting_indices_from_state(&state, att2).unwrap(); let att1_split = SplitAttestation::new(att1.clone(), att1_indices); - let att2_split = SplitAttestation::new(att2.clone(), att2_indices); + let att2_split = SplitAttestation::new(att2.clone_as_attestation(), att2_indices); assert_eq!( - att1.aggregation_bits.num_set_bits(), + att1.num_set_aggregation_bits(), earliest_attestation_validators( &att1_split.as_ref(), &state, @@ -927,8 +940,8 @@ mod release_tests { .unwrap() .current_epoch_attestations .push(PendingAttestation { - aggregation_bits: att1.aggregation_bits.clone(), - data: att1.data.clone(), + aggregation_bits: att1.aggregation_bits_base().unwrap().clone(), + data: att1.data().clone(), inclusion_delay: 0, proposer_index: 0, }) @@ -981,7 +994,8 @@ mod release_tests { for (atts, _) in attestations { for (att, _) in atts { - let attesting_indices = get_attesting_indices_from_state(&state, &att).unwrap(); + let attesting_indices = + get_attesting_indices_from_state(&state, att.to_ref()).unwrap(); op_pool.insert_attestation(att, attesting_indices).unwrap(); } } @@ -1007,7 +1021,7 @@ mod release_tests { let agg_att = &block_attestations[0]; assert_eq!( - agg_att.aggregation_bits.num_set_bits(), + agg_att.num_set_aggregation_bits(), spec.target_committee_size as usize ); @@ -1050,12 +1064,15 @@ mod release_tests { ); for (_, aggregate) in attestations { - let att = aggregate.unwrap().message.aggregate; - let attesting_indices = get_attesting_indices_from_state(&state, &att).unwrap(); + let agg = aggregate.unwrap(); + let att = agg.message().aggregate(); + let attesting_indices = get_attesting_indices_from_state(&state, att).unwrap(); op_pool - .insert_attestation(att.clone(), attesting_indices.clone()) + .insert_attestation(att.clone_as_attestation(), attesting_indices.clone()) + .unwrap(); + op_pool + .insert_attestation(att.clone_as_attestation(), attesting_indices) .unwrap(); - op_pool.insert_attestation(att, attesting_indices).unwrap(); } assert_eq!(op_pool.num_attestations(), committees.len()); @@ -1104,7 +1121,7 @@ mod release_tests { None, |att, new_att| { if let Some(mut a) = att { - a.aggregate(new_att); + a.aggregate(new_att.to_ref()); Some(a) } else { Some(new_att.clone()) @@ -1127,7 +1144,7 @@ mod release_tests { None, |att, new_att| { if let Some(mut a) = att { - a.aggregate(new_att); + a.aggregate(new_att.to_ref()); Some(a) } else { Some(new_att.clone()) @@ -1139,7 +1156,8 @@ mod release_tests { .collect::>(); for att in aggs1.into_iter().chain(aggs2.into_iter()) { - let attesting_indices = get_attesting_indices_from_state(&state, &att).unwrap(); + let attesting_indices = + get_attesting_indices_from_state(&state, att.to_ref()).unwrap(); op_pool.insert_attestation(att, attesting_indices).unwrap(); } } @@ -1203,7 +1221,7 @@ mod release_tests { .fold::, _>( att_0.clone(), |mut att, new_att| { - att.aggregate(new_att); + att.aggregate(new_att.to_ref()); att }, ) @@ -1211,7 +1229,8 @@ mod release_tests { .collect::>(); for att in aggs { - let attesting_indices = get_attesting_indices_from_state(&state, &att).unwrap(); + let attesting_indices = + get_attesting_indices_from_state(&state, att.to_ref()).unwrap(); op_pool.insert_attestation(att, attesting_indices).unwrap(); } }; @@ -1228,7 +1247,17 @@ mod release_tests { let num_big = target_committee_size / big_step_size; let stats = op_pool.attestation_stats(); - assert_eq!(stats.num_attestation_data, committees.len()); + let fork_name = state.fork_name_unchecked(); + + match fork_name { + ForkName::Electra => { + assert_eq!(stats.num_attestation_data, 1); + } + _ => { + assert_eq!(stats.num_attestation_data, committees.len()); + } + }; + assert_eq!( stats.num_attestations, (num_small + num_big) * committees.len() @@ -1239,11 +1268,25 @@ mod release_tests { let best_attestations = op_pool .get_attestations(&state, |_| true, |_| true, spec) .expect("should have best attestations"); - assert_eq!(best_attestations.len(), max_attestations); + match fork_name { + ForkName::Electra => { + assert_eq!(best_attestations.len(), 8); + } + _ => { + assert_eq!(best_attestations.len(), max_attestations); + } + }; // All the best attestations should be signed by at least `big_step_size` (4) validators. for att in &best_attestations { - assert!(att.aggregation_bits.num_set_bits() >= big_step_size); + match fork_name { + ForkName::Electra => { + assert!(att.num_set_aggregation_bits() >= small_step_size); + } + _ => { + assert!(att.num_set_aggregation_bits() >= big_step_size); + } + }; } } @@ -1298,7 +1341,7 @@ mod release_tests { .fold::, _>( att_0.clone(), |mut att, new_att| { - att.aggregate(new_att); + att.aggregate(new_att.to_ref()); att }, ) @@ -1306,7 +1349,8 @@ mod release_tests { .collect::>(); for att in aggs { - let attesting_indices = get_attesting_indices_from_state(&state, &att).unwrap(); + let attesting_indices = + get_attesting_indices_from_state(&state, att.to_ref()).unwrap(); op_pool.insert_attestation(att, attesting_indices).unwrap(); } }; @@ -1321,11 +1365,20 @@ mod release_tests { let num_small = target_committee_size / small_step_size; let num_big = target_committee_size / big_step_size; + let fork_name = state.fork_name_unchecked(); + + match fork_name { + ForkName::Electra => { + assert_eq!(op_pool.attestation_stats().num_attestation_data, 1); + } + _ => { + assert_eq!( + op_pool.attestation_stats().num_attestation_data, + committees.len() + ); + } + }; - assert_eq!( - op_pool.attestation_stats().num_attestation_data, - committees.len() - ); assert_eq!( op_pool.num_attestations(), (num_small + num_big) * committees.len() @@ -1336,7 +1389,15 @@ mod release_tests { let best_attestations = op_pool .get_attestations(&state, |_| true, |_| true, spec) .expect("should have valid best attestations"); - assert_eq!(best_attestations.len(), max_attestations); + + match fork_name { + ForkName::Electra => { + assert_eq!(best_attestations.len(), 8); + } + _ => { + assert_eq!(best_attestations.len(), max_attestations); + } + }; let total_active_balance = state.get_total_active_balance().unwrap(); @@ -1349,7 +1410,7 @@ mod release_tests { reward_cache.update(&state).unwrap(); for att in best_attestations { - let attesting_indices = get_attesting_indices_from_state(&state, &att).unwrap(); + let attesting_indices = get_attesting_indices_from_state(&state, att.to_ref()).unwrap(); let split_attestation = SplitAttestation::new(att, attesting_indices); let mut fresh_validators_rewards = AttMaxCover::new( split_attestation.as_ref(), diff --git a/beacon_node/operation_pool/src/persistence.rs b/beacon_node/operation_pool/src/persistence.rs index ef749a220d..79509e5f6c 100644 --- a/beacon_node/operation_pool/src/persistence.rs +++ b/beacon_node/operation_pool/src/persistence.rs @@ -1,4 +1,3 @@ -use crate::attestation_id::AttestationId; use crate::attestation_storage::AttestationMap; use crate::bls_to_execution_changes::{BlsToExecutionChanges, ReceivedPreCapella}; use crate::sync_aggregate_id::SyncAggregateId; @@ -12,6 +11,7 @@ use state_processing::SigVerifiedOp; use std::collections::HashSet; use std::mem; use store::{DBColumn, Error as StoreError, StoreItem}; +use types::attestation::AttestationOnDisk; use types::*; type PersistedSyncContributions = Vec<(SyncAggregateId, Vec>)>; @@ -21,7 +21,7 @@ type PersistedSyncContributions = Vec<(SyncAggregateId, Vec = Vec<(SyncAggregateId, Vec { - /// [DEPRECATED] Mapping from attestation ID to attestation mappings. - #[superstruct(only(V5))] - pub attestations_v5: Vec<(AttestationId, Vec>)>, + #[superstruct(only(V15))] + pub attestations_v15: Vec<(AttestationBase, Vec)>, /// Attestations and their attesting indices. - #[superstruct(only(V12, V14, V15))] - pub attestations: Vec<(Attestation, Vec)>, + #[superstruct(only(V20))] + pub attestations: Vec<(AttestationOnDisk, Vec)>, /// Mapping from sync contribution ID to sync contributions and aggregate. pub sync_contributions: PersistedSyncContributions, - /// [DEPRECATED] Attester slashings. - #[superstruct(only(V5))] - pub attester_slashings_v5: Vec<(AttesterSlashing, ForkVersion)>, + #[superstruct(only(V15))] + pub attester_slashings_v15: Vec, E>>, /// Attester slashings. - #[superstruct(only(V12, V14, V15))] + #[superstruct(only(V20))] pub attester_slashings: Vec, E>>, - /// [DEPRECATED] Proposer slashings. - #[superstruct(only(V5))] - pub proposer_slashings_v5: Vec, /// Proposer slashings with fork information. - #[superstruct(only(V12, V14, V15))] pub proposer_slashings: Vec>, - /// [DEPRECATED] Voluntary exits. - #[superstruct(only(V5))] - pub voluntary_exits_v5: Vec, /// Voluntary exits with fork information. - #[superstruct(only(V12, V14, V15))] pub voluntary_exits: Vec>, /// BLS to Execution Changes - #[superstruct(only(V14, V15))] pub bls_to_execution_changes: Vec>, /// Validator indices with BLS to Execution Changes to be broadcast at the /// Capella fork. - #[superstruct(only(V15))] pub capella_bls_change_broadcast_indices: Vec, } @@ -75,8 +63,8 @@ impl PersistedOperationPool { .iter() .map(|att| { ( - att.clone_as_attestation(), - att.indexed.attesting_indices.clone(), + AttestationOnDisk::from(att.clone_as_attestation()), + att.indexed.attesting_indices().clone(), ) }) .collect(); @@ -123,7 +111,7 @@ impl PersistedOperationPool { .copied() .collect(); - PersistedOperationPool::V15(PersistedOperationPoolV15 { + PersistedOperationPool::V20(PersistedOperationPoolV20 { attestations, sync_contributions, attester_slashings, @@ -136,56 +124,86 @@ impl PersistedOperationPool { /// Reconstruct an `OperationPool`. pub fn into_operation_pool(mut self) -> Result, OpPoolError> { - let attester_slashings = RwLock::new(self.attester_slashings()?.iter().cloned().collect()); + let attester_slashings = match &self { + PersistedOperationPool::V15(pool_v15) => RwLock::new( + pool_v15 + .attester_slashings_v15 + .iter() + .map(|slashing| slashing.clone().into()) + .collect(), + ), + PersistedOperationPool::V20(pool_v20) => { + RwLock::new(pool_v20.attester_slashings.iter().cloned().collect()) + } + }; + let proposer_slashings = RwLock::new( - self.proposer_slashings()? + self.proposer_slashings() .iter() .cloned() .map(|slashing| (slashing.as_inner().proposer_index(), slashing)) .collect(), ); let voluntary_exits = RwLock::new( - self.voluntary_exits()? + self.voluntary_exits() .iter() .cloned() .map(|exit| (exit.as_inner().message.validator_index, exit)) .collect(), ); let sync_contributions = RwLock::new(self.sync_contributions().iter().cloned().collect()); - let attestations = match self { - PersistedOperationPool::V5(_) | PersistedOperationPool::V12(_) => { - return Err(OpPoolError::IncorrectOpPoolVariant) - } - PersistedOperationPool::V14(_) | PersistedOperationPool::V15(_) => { + let attestations = match &self { + PersistedOperationPool::V15(pool_v15) => { let mut map = AttestationMap::default(); - for (att, attesting_indices) in self.attestations()?.clone() { + for (att, attesting_indices) in + pool_v15 + .attestations_v15 + .iter() + .map(|(att, attesting_indices)| { + (Attestation::Base(att.clone()), attesting_indices.clone()) + }) + { + map.insert(att, attesting_indices); + } + RwLock::new(map) + } + PersistedOperationPool::V20(pool_v20) => { + let mut map = AttestationMap::default(); + for (att, attesting_indices) in + pool_v20 + .attestations + .iter() + .map(|(att, attesting_indices)| { + ( + AttestationRef::from(att.to_ref()).clone_as_attestation(), + attesting_indices.clone(), + ) + }) + { map.insert(att, attesting_indices); } RwLock::new(map) } }; + let mut bls_to_execution_changes = BlsToExecutionChanges::default(); - if let Ok(persisted_changes) = self.bls_to_execution_changes_mut() { - let persisted_changes = mem::take(persisted_changes); + let persisted_changes = mem::take(self.bls_to_execution_changes_mut()); + let broadcast_indices: HashSet<_> = + mem::take(self.capella_bls_change_broadcast_indices_mut()) + .into_iter() + .collect(); - let broadcast_indices = - if let Ok(indices) = self.capella_bls_change_broadcast_indices_mut() { - mem::take(indices).into_iter().collect() - } else { - HashSet::new() - }; - - for bls_to_execution_change in persisted_changes { - let received_pre_capella = if broadcast_indices - .contains(&bls_to_execution_change.as_inner().message.validator_index) - { - ReceivedPreCapella::Yes - } else { - ReceivedPreCapella::No - }; - bls_to_execution_changes.insert(bls_to_execution_change, received_pre_capella); - } + for bls_to_execution_change in persisted_changes { + let received_pre_capella = if broadcast_indices + .contains(&bls_to_execution_change.as_inner().message.validator_index) + { + ReceivedPreCapella::Yes + } else { + ReceivedPreCapella::No + }; + bls_to_execution_changes.insert(bls_to_execution_change, received_pre_capella); } + let op_pool = OperationPool { attestations, sync_contributions, @@ -200,48 +218,6 @@ impl PersistedOperationPool { } } -impl StoreItem for PersistedOperationPoolV5 { - fn db_column() -> DBColumn { - DBColumn::OpPool - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - PersistedOperationPoolV5::from_ssz_bytes(bytes).map_err(Into::into) - } -} - -impl StoreItem for PersistedOperationPoolV12 { - fn db_column() -> DBColumn { - DBColumn::OpPool - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - PersistedOperationPoolV12::from_ssz_bytes(bytes).map_err(Into::into) - } -} - -impl StoreItem for PersistedOperationPoolV14 { - fn db_column() -> DBColumn { - DBColumn::OpPool - } - - fn as_store_bytes(&self) -> Vec { - self.as_ssz_bytes() - } - - fn from_store_bytes(bytes: &[u8]) -> Result { - PersistedOperationPoolV14::from_ssz_bytes(bytes).map_err(Into::into) - } -} - impl StoreItem for PersistedOperationPoolV15 { fn db_column() -> DBColumn { DBColumn::OpPool @@ -256,6 +232,20 @@ impl StoreItem for PersistedOperationPoolV15 { } } +impl StoreItem for PersistedOperationPoolV20 { + fn db_column() -> DBColumn { + DBColumn::OpPool + } + + fn as_store_bytes(&self) -> Vec { + self.as_ssz_bytes() + } + + fn from_store_bytes(bytes: &[u8]) -> Result { + PersistedOperationPoolV20::from_ssz_bytes(bytes).map_err(Into::into) + } +} + /// Deserialization for `PersistedOperationPool` defaults to `PersistedOperationPool::V12`. impl StoreItem for PersistedOperationPool { fn db_column() -> DBColumn { @@ -268,8 +258,8 @@ impl StoreItem for PersistedOperationPool { fn from_store_bytes(bytes: &[u8]) -> Result { // Default deserialization to the latest variant. - PersistedOperationPoolV15::from_ssz_bytes(bytes) - .map(Self::V15) + PersistedOperationPoolV20::from_ssz_bytes(bytes) + .map(Self::V20) .map_err(Into::into) } } diff --git a/beacon_node/src/lib.rs b/beacon_node/src/lib.rs index ab400d2e73..385da5b4fe 100644 --- a/beacon_node/src/lib.rs +++ b/beacon_node/src/lib.rs @@ -80,7 +80,7 @@ impl ProductionBeaconNode { let builder = ClientBuilder::new(context.eth_spec_instance.clone()) .runtime_context(context) - .chain_spec(spec) + .chain_spec(spec.clone()) .beacon_processor(client_config.beacon_processor.clone()) .http_api_config(client_config.http_api.clone()) .disk_store( @@ -113,8 +113,12 @@ impl ProductionBeaconNode { _ => {} } let slasher = Arc::new( - Slasher::open(slasher_config, log.new(slog::o!("service" => "slasher"))) - .map_err(|e| format!("Slasher open error: {:?}", e))?, + Slasher::open( + slasher_config, + Arc::new(spec), + log.new(slog::o!("service" => "slasher")), + ) + .map_err(|e| format!("Slasher open error: {:?}", e))?, ); builder.slasher(slasher) } else { diff --git a/beacon_node/store/src/consensus_context.rs b/beacon_node/store/src/consensus_context.rs index 08fad17b14..281106d9aa 100644 --- a/beacon_node/store/src/consensus_context.rs +++ b/beacon_node/store/src/consensus_context.rs @@ -1,7 +1,7 @@ use ssz_derive::{Decode, Encode}; use state_processing::ConsensusContext; use std::collections::HashMap; -use types::{AttestationData, BitList, EthSpec, Hash256, IndexedAttestation, Slot}; +use types::{EthSpec, Hash256, IndexedAttestation, Slot}; /// The consensus context is stored on disk as part of the data availability overflow cache. /// @@ -21,8 +21,7 @@ pub struct OnDiskConsensusContext { /// /// They are not part of the on-disk format. #[ssz(skip_serializing, skip_deserializing)] - indexed_attestations: - HashMap<(AttestationData, BitList), IndexedAttestation>, + indexed_attestations: HashMap>, } impl OnDiskConsensusContext { diff --git a/beacon_node/store/src/metadata.rs b/beacon_node/store/src/metadata.rs index 1675051bd8..116926ad3f 100644 --- a/beacon_node/store/src/metadata.rs +++ b/beacon_node/store/src/metadata.rs @@ -4,7 +4,7 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use types::{Checkpoint, Hash256, Slot}; -pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(19); +pub const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(20); // All the keys that get stored under the `BeaconMeta` column. // diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index c5a6ea5d87..70aa5aab3e 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -716,6 +716,21 @@ pub struct AttesterData { pub slot: Slot, } +impl AttesterData { + pub fn match_attestation_data( + &self, + attestation_data: &AttestationData, + spec: &ChainSpec, + ) -> bool { + if spec.fork_name_at_slot::(attestation_data.slot) < ForkName::Electra { + self.slot == attestation_data.slot && self.committee_index == attestation_data.index + } else { + // After electra `attestation_data.index` is set to 0 and does not match the duties + self.slot == attestation_data.slot + } + } +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ProposerData { pub pubkey: PublicKeyBytes, diff --git a/consensus/fork_choice/src/fork_choice.rs b/consensus/fork_choice/src/fork_choice.rs index 2846a0112c..c55219a676 100644 --- a/consensus/fork_choice/src/fork_choice.rs +++ b/consensus/fork_choice/src/fork_choice.rs @@ -14,8 +14,8 @@ use std::marker::PhantomData; use std::time::Duration; use types::{ consts::bellatrix::INTERVALS_PER_SLOT, AbstractExecPayload, AttestationShufflingId, - AttesterSlashing, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Checkpoint, Epoch, - EthSpec, ExecPayload, ExecutionBlockHash, Hash256, IndexedAttestation, RelativeEpoch, + AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, Checkpoint, + Epoch, EthSpec, ExecPayload, ExecutionBlockHash, Hash256, IndexedAttestationRef, RelativeEpoch, SignedBeaconBlock, Slot, }; @@ -238,13 +238,13 @@ pub struct QueuedAttestation { target_epoch: Epoch, } -impl From<&IndexedAttestation> for QueuedAttestation { - fn from(a: &IndexedAttestation) -> Self { +impl<'a, E: EthSpec> From> for QueuedAttestation { + fn from(a: IndexedAttestationRef<'a, E>) -> Self { Self { - slot: a.data.slot, - attesting_indices: a.attesting_indices[..].to_vec(), - block_root: a.data.beacon_block_root, - target_epoch: a.data.target.epoch, + slot: a.data().slot, + attesting_indices: a.attesting_indices_to_vec(), + block_root: a.data().beacon_block_root, + target_epoch: a.data().target.epoch, } } } @@ -940,7 +940,7 @@ where /// https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/fork-choice.md#validate_on_attestation fn validate_on_attestation( &self, - indexed_attestation: &IndexedAttestation, + indexed_attestation: IndexedAttestationRef, is_from_block: AttestationFromBlock, ) -> Result<(), InvalidAttestation> { // There is no point in processing an attestation with an empty bitfield. Reject @@ -948,20 +948,20 @@ where // // This is not in the specification, however it should be transparent to other nodes. We // return early here to avoid wasting precious resources verifying the rest of it. - if indexed_attestation.attesting_indices.is_empty() { + if indexed_attestation.attesting_indices_is_empty() { return Err(InvalidAttestation::EmptyAggregationBitfield); } - let target = indexed_attestation.data.target; + let target = indexed_attestation.data().target; if matches!(is_from_block, AttestationFromBlock::False) { self.validate_target_epoch_against_current_time(target.epoch)?; } - if target.epoch != indexed_attestation.data.slot.epoch(E::slots_per_epoch()) { + if target.epoch != indexed_attestation.data().slot.epoch(E::slots_per_epoch()) { return Err(InvalidAttestation::BadTargetEpoch { target: target.epoch, - slot: indexed_attestation.data.slot, + slot: indexed_attestation.data().slot, }); } @@ -983,9 +983,9 @@ where // attestation and do not delay consideration for later. let block = self .proto_array - .get_block(&indexed_attestation.data.beacon_block_root) + .get_block(&indexed_attestation.data().beacon_block_root) .ok_or(InvalidAttestation::UnknownHeadBlock { - beacon_block_root: indexed_attestation.data.beacon_block_root, + beacon_block_root: indexed_attestation.data().beacon_block_root, })?; // If an attestation points to a block that is from an earlier slot than the attestation, @@ -993,7 +993,7 @@ where // is from a prior epoch to the attestation, then the target root must be equal to the root // of the block that is being attested to. let expected_target = if target.epoch > block.slot.epoch(E::slots_per_epoch()) { - indexed_attestation.data.beacon_block_root + indexed_attestation.data().beacon_block_root } else { block.target_root }; @@ -1007,10 +1007,10 @@ where // Attestations must not be for blocks in the future. If this is the case, the attestation // should not be considered. - if block.slot > indexed_attestation.data.slot { + if block.slot > indexed_attestation.data().slot { return Err(InvalidAttestation::AttestsToFutureBlock { block: block.slot, - attestation: indexed_attestation.data.slot, + attestation: indexed_attestation.data().slot, }); } @@ -1037,7 +1037,7 @@ where pub fn on_attestation( &mut self, system_time_current_slot: Slot, - attestation: &IndexedAttestation, + attestation: IndexedAttestationRef, is_from_block: AttestationFromBlock, ) -> Result<(), Error> { self.update_time(system_time_current_slot)?; @@ -1055,18 +1055,18 @@ where // (1) becomes weird once we hit finality and fork choice drops the genesis block. (2) is // fine because votes to the genesis block are not useful; all validators implicitly attest // to genesis just by being present in the chain. - if attestation.data.beacon_block_root == Hash256::zero() { + if attestation.data().beacon_block_root == Hash256::zero() { return Ok(()); } self.validate_on_attestation(attestation, is_from_block)?; - if attestation.data.slot < self.fc_store.get_current_slot() { - for validator_index in attestation.attesting_indices.iter() { + if attestation.data().slot < self.fc_store.get_current_slot() { + for validator_index in attestation.attesting_indices_iter() { self.proto_array.process_attestation( *validator_index as usize, - attestation.data.beacon_block_root, - attestation.data.target.epoch, + attestation.data().beacon_block_root, + attestation.data().target.epoch, )?; } } else { @@ -1086,15 +1086,14 @@ where /// Apply an attester slashing to fork choice. /// /// We assume that the attester slashing provided to this function has already been verified. - pub fn on_attester_slashing(&mut self, slashing: &AttesterSlashing) { - let attesting_indices_set = |att: &IndexedAttestation| { - att.attesting_indices - .iter() + pub fn on_attester_slashing(&mut self, slashing: AttesterSlashingRef<'_, E>) { + let attesting_indices_set = |att: IndexedAttestationRef<'_, E>| { + att.attesting_indices_iter() .copied() .collect::>() }; - let att1_indices = attesting_indices_set(&slashing.attestation_1); - let att2_indices = attesting_indices_set(&slashing.attestation_2); + let att1_indices = attesting_indices_set(slashing.attestation_1()); + let att2_indices = attesting_indices_set(slashing.attestation_2()); self.fc_store .extend_equivocating_indices(att1_indices.intersection(&att2_indices).copied()); } diff --git a/consensus/fork_choice/tests/tests.rs b/consensus/fork_choice/tests/tests.rs index 3153275fb7..d2935dbca4 100644 --- a/consensus/fork_choice/tests/tests.rs +++ b/consensus/fork_choice/tests/tests.rs @@ -435,7 +435,12 @@ impl ForkChoiceTest { let validator_committee_index = 0; let validator_index = *head .beacon_state - .get_beacon_committee(current_slot, attestation.data.index) + .get_beacon_committee( + current_slot, + attestation + .committee_index() + .expect("should get committee index"), + ) .expect("should get committees") .committee .get(validator_committee_index) @@ -830,8 +835,13 @@ async fn invalid_attestation_empty_bitfield() { .await .apply_attestation_to_chain( MutationDelay::NoDelay, - |attestation, _| { - attestation.attesting_indices = vec![].into(); + |attestation, _| match attestation { + IndexedAttestation::Base(ref mut att) => { + att.attesting_indices = vec![].into(); + } + IndexedAttestation::Electra(ref mut att) => { + att.attesting_indices = vec![].into(); + } }, |result| { assert_invalid_attestation!(result, InvalidAttestation::EmptyAggregationBitfield) @@ -853,7 +863,7 @@ async fn invalid_attestation_future_epoch() { .apply_attestation_to_chain( MutationDelay::NoDelay, |attestation, _| { - attestation.data.target.epoch = Epoch::new(2); + attestation.data_mut().target.epoch = Epoch::new(2); }, |result| { assert_invalid_attestation!( @@ -879,7 +889,7 @@ async fn invalid_attestation_past_epoch() { .apply_attestation_to_chain( MutationDelay::NoDelay, |attestation, _| { - attestation.data.target.epoch = Epoch::new(0); + attestation.data_mut().target.epoch = Epoch::new(0); }, |result| { assert_invalid_attestation!( @@ -903,7 +913,7 @@ async fn invalid_attestation_target_epoch() { .apply_attestation_to_chain( MutationDelay::NoDelay, |attestation, _| { - attestation.data.slot = Slot::new(1); + attestation.data_mut().slot = Slot::new(1); }, |result| { assert_invalid_attestation!( @@ -929,7 +939,7 @@ async fn invalid_attestation_unknown_target_root() { .apply_attestation_to_chain( MutationDelay::NoDelay, |attestation, _| { - attestation.data.target.root = junk; + attestation.data_mut().target.root = junk; }, |result| { assert_invalid_attestation!( @@ -955,7 +965,7 @@ async fn invalid_attestation_unknown_beacon_block_root() { .apply_attestation_to_chain( MutationDelay::NoDelay, |attestation, _| { - attestation.data.beacon_block_root = junk; + attestation.data_mut().beacon_block_root = junk; }, |result| { assert_invalid_attestation!( @@ -979,7 +989,7 @@ async fn invalid_attestation_future_block() { .apply_attestation_to_chain( MutationDelay::Blocks(1), |attestation, chain| { - attestation.data.beacon_block_root = chain + attestation.data_mut().beacon_block_root = chain .block_at_slot(chain.slot().unwrap(), WhenSlotSkipped::Prev) .unwrap() .unwrap() @@ -1010,13 +1020,13 @@ async fn invalid_attestation_inconsistent_ffg_vote() { .apply_attestation_to_chain( MutationDelay::NoDelay, |attestation, chain| { - attestation.data.target.root = chain + attestation.data_mut().target.root = chain .block_at_slot(Slot::new(1), WhenSlotSkipped::Prev) .unwrap() .unwrap() .canonical_root(); - *attestation_opt.lock().unwrap() = Some(attestation.data.target.root); + *attestation_opt.lock().unwrap() = Some(attestation.data().target.root); *local_opt.lock().unwrap() = Some( chain .block_at_slot(Slot::new(0), WhenSlotSkipped::Prev) @@ -1069,8 +1079,8 @@ async fn valid_attestation_skip_across_epoch() { MutationDelay::NoDelay, |attestation, _chain| { assert_eq!( - attestation.data.target.root, - attestation.data.beacon_block_root + attestation.data().target.root, + attestation.data().beacon_block_root ) }, |result| result.unwrap(), diff --git a/consensus/state_processing/src/common/get_attesting_indices.rs b/consensus/state_processing/src/common/get_attesting_indices.rs index a89b71ff2b..b131f7679a 100644 --- a/consensus/state_processing/src/common/get_attesting_indices.rs +++ b/consensus/state_processing/src/common/get_attesting_indices.rs @@ -1,32 +1,172 @@ use types::*; -/// Returns validator indices which participated in the attestation, sorted by increasing index. -pub fn get_attesting_indices( - committee: &[usize], - bitlist: &BitList, -) -> Result, BeaconStateError> { - if bitlist.len() != committee.len() { - return Err(BeaconStateError::InvalidBitfield); +pub mod attesting_indices_base { + use crate::per_block_processing::errors::{AttestationInvalid as Invalid, BlockOperationError}; + use types::*; + + /// Convert `attestation` to (almost) indexed-verifiable form. + /// + /// Spec v0.12.1 + pub fn get_indexed_attestation( + committee: &[usize], + attestation: &AttestationBase, + ) -> Result, BlockOperationError> { + let attesting_indices = + get_attesting_indices::(committee, &attestation.aggregation_bits)?; + Ok(IndexedAttestation::Base(IndexedAttestationBase { + attesting_indices: VariableList::new(attesting_indices)?, + data: attestation.data.clone(), + signature: attestation.signature.clone(), + })) } - let mut indices = Vec::with_capacity(bitlist.num_set_bits()); - - for (i, validator_index) in committee.iter().enumerate() { - if let Ok(true) = bitlist.get(i) { - indices.push(*validator_index as u64) + /// Returns validator indices which participated in the attestation, sorted by increasing index. + pub fn get_attesting_indices( + committee: &[usize], + bitlist: &BitList, + ) -> Result, BeaconStateError> { + if bitlist.len() != committee.len() { + return Err(BeaconStateError::InvalidBitfield); } + + let mut indices = Vec::with_capacity(bitlist.num_set_bits()); + + for (i, validator_index) in committee.iter().enumerate() { + if let Ok(true) = bitlist.get(i) { + indices.push(*validator_index as u64) + } + } + + indices.sort_unstable(); + + Ok(indices) + } +} + +pub mod attesting_indices_electra { + use std::collections::HashSet; + + use crate::per_block_processing::errors::{AttestationInvalid as Invalid, BlockOperationError}; + use safe_arith::SafeArith; + use types::*; + + /// Compute an Electra IndexedAttestation given a list of committees. + /// + /// Committees must be sorted by ascending order 0..committees_per_slot + pub fn get_indexed_attestation( + committees: &[BeaconCommittee], + attestation: &AttestationElectra, + ) -> Result, BlockOperationError> { + let attesting_indices = get_attesting_indices::( + committees, + &attestation.aggregation_bits, + &attestation.committee_bits, + )?; + + Ok(IndexedAttestation::Electra(IndexedAttestationElectra { + attesting_indices: VariableList::new(attesting_indices)?, + data: attestation.data.clone(), + signature: attestation.signature.clone(), + })) } - indices.sort_unstable(); + pub fn get_indexed_attestation_from_state( + beacon_state: &BeaconState, + attestation: &AttestationElectra, + ) -> Result, BlockOperationError> { + let committees = beacon_state.get_beacon_committees_at_slot(attestation.data.slot)?; + get_indexed_attestation(&committees, attestation) + } - Ok(indices) + /// Shortcut for getting the attesting indices while fetching the committee from the state's cache. + pub fn get_attesting_indices_from_state( + state: &BeaconState, + att: &AttestationElectra, + ) -> Result, BeaconStateError> { + let committees = state.get_beacon_committees_at_slot(att.data.slot)?; + get_attesting_indices::(&committees, &att.aggregation_bits, &att.committee_bits) + } + + /// Returns validator indices which participated in the attestation, sorted by increasing index. + /// + /// Committees must be sorted by ascending order 0..committees_per_slot + pub fn get_attesting_indices( + committees: &[BeaconCommittee], + aggregation_bits: &BitList, + committee_bits: &BitVector, + ) -> Result, BeaconStateError> { + let mut attesting_indices = vec![]; + + let committee_indices = get_committee_indices::(committee_bits); + + let mut committee_offset = 0; + + let committee_count_per_slot = committees.len() as u64; + let mut participant_count = 0; + for index in committee_indices { + let beacon_committee = committees + .get(index as usize) + .ok_or(Error::NoCommitteeFound(index))?; + + // This check is new to the spec's `process_attestation` in Electra. + if index >= committee_count_per_slot { + return Err(BeaconStateError::InvalidCommitteeIndex(index)); + } + participant_count.safe_add_assign(beacon_committee.committee.len() as u64)?; + let committee_attesters = beacon_committee + .committee + .iter() + .enumerate() + .filter_map(|(i, &index)| { + if let Ok(aggregation_bit_index) = committee_offset.safe_add(i) { + if aggregation_bits.get(aggregation_bit_index).unwrap_or(false) { + return Some(index as u64); + } + } + None + }) + .collect::>(); + + attesting_indices.extend(committee_attesters); + committee_offset.safe_add_assign(beacon_committee.committee.len())?; + } + + // This check is new to the spec's `process_attestation` in Electra. + if participant_count as usize != aggregation_bits.len() { + return Err(BeaconStateError::InvalidBitfield); + } + + attesting_indices.sort_unstable(); + + Ok(attesting_indices) + } + + pub fn get_committee_indices( + committee_bits: &BitVector, + ) -> Vec { + committee_bits + .iter() + .enumerate() + .filter_map(|(index, bit)| if bit { Some(index as u64) } else { None }) + .collect() + } } /// Shortcut for getting the attesting indices while fetching the committee from the state's cache. pub fn get_attesting_indices_from_state( state: &BeaconState, - att: &Attestation, + att: AttestationRef, ) -> Result, BeaconStateError> { - let committee = state.get_beacon_committee(att.data.slot, att.data.index)?; - get_attesting_indices::(committee.committee, &att.aggregation_bits) + match att { + AttestationRef::Base(att) => { + let committee = state.get_beacon_committee(att.data.slot, att.data.index)?; + attesting_indices_base::get_attesting_indices::( + committee.committee, + &att.aggregation_bits, + ) + } + AttestationRef::Electra(att) => { + attesting_indices_electra::get_attesting_indices_from_state::(state, att) + } + } } diff --git a/consensus/state_processing/src/common/get_indexed_attestation.rs b/consensus/state_processing/src/common/get_indexed_attestation.rs deleted file mode 100644 index 9cf689df40..0000000000 --- a/consensus/state_processing/src/common/get_indexed_attestation.rs +++ /dev/null @@ -1,21 +0,0 @@ -use super::get_attesting_indices; -use crate::per_block_processing::errors::{AttestationInvalid as Invalid, BlockOperationError}; -use types::*; - -type Result = std::result::Result>; - -/// Convert `attestation` to (almost) indexed-verifiable form. -/// -/// Spec v0.12.1 -pub fn get_indexed_attestation( - committee: &[usize], - attestation: &Attestation, -) -> Result> { - let attesting_indices = get_attesting_indices::(committee, &attestation.aggregation_bits)?; - - Ok(IndexedAttestation { - attesting_indices: VariableList::new(attesting_indices)?, - data: attestation.data.clone(), - signature: attestation.signature.clone(), - }) -} diff --git a/consensus/state_processing/src/common/mod.rs b/consensus/state_processing/src/common/mod.rs index cefc47b023..0287748fd0 100644 --- a/consensus/state_processing/src/common/mod.rs +++ b/consensus/state_processing/src/common/mod.rs @@ -1,7 +1,6 @@ mod deposit_data_tree; mod get_attestation_participation; mod get_attesting_indices; -mod get_indexed_attestation; mod initiate_validator_exit; mod slash_validator; @@ -11,8 +10,9 @@ pub mod update_progressive_balances_cache; pub use deposit_data_tree::DepositDataTree; pub use get_attestation_participation::get_attestation_participation_flag_indices; -pub use get_attesting_indices::{get_attesting_indices, get_attesting_indices_from_state}; -pub use get_indexed_attestation::get_indexed_attestation; +pub use get_attesting_indices::{ + attesting_indices_base, attesting_indices_electra, get_attesting_indices_from_state, +}; pub use initiate_validator_exit::initiate_validator_exit; pub use slash_validator::slash_validator; diff --git a/consensus/state_processing/src/consensus_context.rs b/consensus/state_processing/src/consensus_context.rs index 073d87be85..b0eaf3422d 100644 --- a/consensus/state_processing/src/consensus_context.rs +++ b/consensus/state_processing/src/consensus_context.rs @@ -1,11 +1,11 @@ -use crate::common::get_indexed_attestation; +use crate::common::{attesting_indices_base, attesting_indices_electra}; use crate::per_block_processing::errors::{AttestationInvalid, BlockOperationError}; use crate::EpochCacheError; use std::collections::{hash_map::Entry, HashMap}; use tree_hash::TreeHash; use types::{ - AbstractExecPayload, Attestation, AttestationData, BeaconState, BeaconStateError, BitList, - ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, SignedBeaconBlock, Slot, + AbstractExecPayload, AttestationRef, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, + Hash256, IndexedAttestation, IndexedAttestationRef, SignedBeaconBlock, Slot, }; #[derive(Debug, PartialEq, Clone)] @@ -21,8 +21,7 @@ pub struct ConsensusContext { /// Block root of the block at `slot`. pub current_block_root: Option, /// Cache of indexed attestations constructed during block processing. - pub indexed_attestations: - HashMap<(AttestationData, BitList), IndexedAttestation>, + pub indexed_attestations: HashMap>, } #[derive(Debug, PartialEq, Clone)] @@ -148,26 +147,32 @@ impl ConsensusContext { } } - pub fn get_indexed_attestation( - &mut self, + pub fn get_indexed_attestation<'a>( + &'a mut self, state: &BeaconState, - attestation: &Attestation, - ) -> Result<&IndexedAttestation, BlockOperationError> { - let key = ( - attestation.data.clone(), - attestation.aggregation_bits.clone(), - ); - - match self.indexed_attestations.entry(key) { - Entry::Occupied(occupied) => Ok(occupied.into_mut()), - Entry::Vacant(vacant) => { - let committee = - state.get_beacon_committee(attestation.data.slot, attestation.data.index)?; - let indexed_attestation = - get_indexed_attestation(committee.committee, attestation)?; - Ok(vacant.insert(indexed_attestation)) - } + attestation: AttestationRef<'a, E>, + ) -> Result, BlockOperationError> { + let key = attestation.tree_hash_root(); + match attestation { + AttestationRef::Base(attn) => match self.indexed_attestations.entry(key) { + Entry::Occupied(occupied) => Ok(occupied.into_mut()), + Entry::Vacant(vacant) => { + let committee = state.get_beacon_committee(attn.data.slot, attn.data.index)?; + let indexed_attestation = + attesting_indices_base::get_indexed_attestation(committee.committee, attn)?; + Ok(vacant.insert(indexed_attestation)) + } + }, + AttestationRef::Electra(attn) => match self.indexed_attestations.entry(key) { + Entry::Occupied(occupied) => Ok(occupied.into_mut()), + Entry::Vacant(vacant) => { + let indexed_attestation = + attesting_indices_electra::get_indexed_attestation_from_state(state, attn)?; + Ok(vacant.insert(indexed_attestation)) + } + }, } + .map(|indexed_attestation| (*indexed_attestation).to_ref()) } pub fn num_cached_indexed_attestations(&self) -> usize { @@ -177,10 +182,7 @@ impl ConsensusContext { #[must_use] pub fn set_indexed_attestations( mut self, - attestations: HashMap< - (AttestationData, BitList), - IndexedAttestation, - >, + attestations: HashMap>, ) -> Self { self.indexed_attestations = attestations; self diff --git a/consensus/state_processing/src/lib.rs b/consensus/state_processing/src/lib.rs index 74f9d84bb1..adabf6862d 100644 --- a/consensus/state_processing/src/lib.rs +++ b/consensus/state_processing/src/lib.rs @@ -45,4 +45,4 @@ pub use per_epoch_processing::{ }; pub use per_slot_processing::{per_slot_processing, Error as SlotProcessingError}; pub use types::{EpochCache, EpochCacheError, EpochCacheKey}; -pub use verify_operation::{SigVerifiedOp, VerifyOperation, VerifyOperationAt}; +pub use verify_operation::{SigVerifiedOp, TransformPersist, VerifyOperation, VerifyOperationAt}; diff --git a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs index 3b8a8ea52c..74477f5e48 100644 --- a/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs +++ b/consensus/state_processing/src/per_block_processing/block_signature_verifier.rs @@ -247,13 +247,12 @@ where ) -> Result<()> { self.sets .sets - .reserve(block.message().body().attester_slashings().len() * 2); + .reserve(block.message().body().attester_slashings_len() * 2); block .message() .body() .attester_slashings() - .iter() .try_for_each(|attester_slashing| { let (set_1, set_2) = attester_slashing_signature_sets( self.state, @@ -277,20 +276,19 @@ where ) -> Result<()> { self.sets .sets - .reserve(block.message().body().attestations().len()); + .reserve(block.message().body().attestations_len()); block .message() .body() .attestations() - .iter() .try_for_each(|attestation| { let indexed_attestation = ctxt.get_indexed_attestation(self.state, attestation)?; self.sets.push(indexed_attestation_signature_set( self.state, self.get_pubkey.clone(), - &attestation.signature, + attestation.signature(), indexed_attestation, self.spec, )?); diff --git a/consensus/state_processing/src/per_block_processing/errors.rs b/consensus/state_processing/src/per_block_processing/errors.rs index 336895514f..71a284dea4 100644 --- a/consensus/state_processing/src/per_block_processing/errors.rs +++ b/consensus/state_processing/src/per_block_processing/errors.rs @@ -89,6 +89,7 @@ pub enum BlockProcessingError { found: Hash256, }, WithdrawalCredentialsInvalid, + PendingAttestationInElectra, } impl From for BlockProcessingError { diff --git a/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs b/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs index baccd1dbbd..4bad3315cc 100644 --- a/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/is_valid_indexed_attestation.rs @@ -13,11 +13,11 @@ fn error(reason: Invalid) -> BlockOperationError { /// Verify an `IndexedAttestation`. pub fn is_valid_indexed_attestation( state: &BeaconState, - indexed_attestation: &IndexedAttestation, + indexed_attestation: IndexedAttestationRef, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result<()> { - let indices = &indexed_attestation.attesting_indices; + let indices = indexed_attestation.attesting_indices_to_vec(); // Verify that indices aren't empty verify!(!indices.is_empty(), Invalid::IndicesEmpty); @@ -36,14 +36,14 @@ pub fn is_valid_indexed_attestation( })?; Ok(()) }; - check_sorted(indices)?; + check_sorted(&indices)?; if verify_signatures.is_true() { verify!( indexed_attestation_signature_set( state, |i| get_pubkey_from_state(state, i), - &indexed_attestation.signature, + indexed_attestation.signature(), indexed_attestation, spec )? diff --git a/consensus/state_processing/src/per_block_processing/process_operations.rs b/consensus/state_processing/src/per_block_processing/process_operations.rs index 3aefcf8a9c..bd354901a8 100644 --- a/consensus/state_processing/src/per_block_processing/process_operations.rs +++ b/consensus/state_processing/src/per_block_processing/process_operations.rs @@ -46,13 +46,16 @@ pub mod base { /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. - pub fn process_attestations( + pub fn process_attestations<'a, E: EthSpec, I>( state: &mut BeaconState, - attestations: &[Attestation], + attestations: I, verify_signatures: VerifySignatures, ctxt: &mut ConsensusContext, spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> { + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { // Ensure required caches are all built. These should be no-ops during regular operation. state.build_committee_cache(RelativeEpoch::Current, spec)?; state.build_committee_cache(RelativeEpoch::Previous, spec)?; @@ -63,7 +66,7 @@ pub mod base { let proposer_index = ctxt.get_proposer_index(state, spec)?; // Verify and apply each attestation. - for (i, attestation) in attestations.iter().enumerate() { + for (i, attestation) in attestations.enumerate() { verify_attestation_for_block_inclusion( state, attestation, @@ -73,6 +76,12 @@ pub mod base { ) .map_err(|e| e.into_with_index(i))?; + let AttestationRef::Base(attestation) = attestation else { + // Pending attestations have been deprecated in a altair, this branch should + // never happen + return Err(BlockProcessingError::PendingAttestationInElectra); + }; + let pending_attestation = PendingAttestation { aggregation_bits: attestation.aggregation_bits.clone(), data: attestation.data.clone(), @@ -101,24 +110,24 @@ pub mod altair_deneb { use super::*; use crate::common::update_progressive_balances_cache::update_progressive_balances_on_attestation; - pub fn process_attestations( + pub fn process_attestations<'a, E: EthSpec, I>( state: &mut BeaconState, - attestations: &[Attestation], + attestations: I, verify_signatures: VerifySignatures, ctxt: &mut ConsensusContext, spec: &ChainSpec, - ) -> Result<(), BlockProcessingError> { - attestations - .iter() - .enumerate() - .try_for_each(|(i, attestation)| { - process_attestation(state, attestation, i, ctxt, verify_signatures, spec) - }) + ) -> Result<(), BlockProcessingError> + where + I: Iterator>, + { + attestations.enumerate().try_for_each(|(i, attestation)| { + process_attestation(state, attestation, i, ctxt, verify_signatures, spec) + }) } pub fn process_attestation( state: &mut BeaconState, - attestation: &Attestation, + attestation: AttestationRef, att_index: usize, ctxt: &mut ConsensusContext, verify_signatures: VerifySignatures, @@ -128,26 +137,24 @@ pub mod altair_deneb { let previous_epoch = ctxt.previous_epoch; let current_epoch = ctxt.current_epoch; - let attesting_indices = verify_attestation_for_block_inclusion( + let indexed_att = verify_attestation_for_block_inclusion( state, attestation, ctxt, verify_signatures, spec, ) - .map_err(|e| e.into_with_index(att_index))? - .attesting_indices - .clone(); + .map_err(|e| e.into_with_index(att_index))?; // Matching roots, participation flag indices - let data = &attestation.data; + let data = attestation.data(); let inclusion_delay = state.slot().safe_sub(data.slot)?.as_u64(); let participation_flag_indices = get_attestation_participation_flag_indices(state, data, inclusion_delay, spec)?; // Update epoch participation flags. let mut proposer_reward_numerator = 0; - for index in &attesting_indices { + for index in indexed_att.attesting_indices_iter() { let index = *index as usize; let validator_effective_balance = state.epoch_cache().get_effective_balance(index)?; @@ -231,16 +238,19 @@ pub fn process_proposer_slashings( /// /// Returns `Ok(())` if the validation and state updates completed successfully, otherwise returns /// an `Err` describing the invalid object or cause of failure. -pub fn process_attester_slashings( +pub fn process_attester_slashings<'a, E: EthSpec, I>( state: &mut BeaconState, - attester_slashings: &[AttesterSlashing], + attester_slashings: I, verify_signatures: VerifySignatures, ctxt: &mut ConsensusContext, spec: &ChainSpec, -) -> Result<(), BlockProcessingError> { +) -> Result<(), BlockProcessingError> +where + I: Iterator>, +{ state.build_slashings_cache()?; - for (i, attester_slashing) in attester_slashings.iter().enumerate() { + for (i, attester_slashing) in attester_slashings.enumerate() { let slashable_indices = verify_attester_slashing(state, attester_slashing, verify_signatures, spec) .map_err(|e| e.into_with_index(i))?; diff --git a/consensus/state_processing/src/per_block_processing/signature_sets.rs b/consensus/state_processing/src/per_block_processing/signature_sets.rs index 9468893f76..2e00ee0341 100644 --- a/consensus/state_processing/src/per_block_processing/signature_sets.rs +++ b/consensus/state_processing/src/per_block_processing/signature_sets.rs @@ -7,10 +7,10 @@ use ssz::DecodeError; use std::borrow::Cow; use tree_hash::TreeHash; use types::{ - AbstractExecPayload, AggregateSignature, AttesterSlashing, BeaconBlockRef, BeaconState, + AbstractExecPayload, AggregateSignature, AttesterSlashingRef, BeaconBlockRef, BeaconState, BeaconStateError, ChainSpec, DepositData, Domain, Epoch, EthSpec, Fork, Hash256, - InconsistentFork, IndexedAttestation, ProposerSlashing, PublicKey, PublicKeyBytes, Signature, - SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, + InconsistentFork, IndexedAttestation, IndexedAttestationRef, ProposerSlashing, PublicKey, + PublicKeyBytes, Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, SignedBlsToExecutionChange, SignedContributionAndProof, SignedRoot, SignedVoluntaryExit, SigningData, Slot, SyncAggregate, SyncAggregatorSelectionData, Unsigned, }; @@ -272,28 +272,28 @@ pub fn indexed_attestation_signature_set<'a, 'b, E, F>( state: &'a BeaconState, get_pubkey: F, signature: &'a AggregateSignature, - indexed_attestation: &'b IndexedAttestation, + indexed_attestation: IndexedAttestationRef<'b, E>, spec: &'a ChainSpec, ) -> Result> where E: EthSpec, F: Fn(usize) -> Option>, { - let mut pubkeys = Vec::with_capacity(indexed_attestation.attesting_indices.len()); - for &validator_idx in &indexed_attestation.attesting_indices { + let mut pubkeys = Vec::with_capacity(indexed_attestation.attesting_indices_len()); + for &validator_idx in indexed_attestation.attesting_indices_iter() { pubkeys.push( get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?, ); } let domain = spec.get_domain( - indexed_attestation.data.target.epoch, + indexed_attestation.data().target.epoch, Domain::BeaconAttester, &state.fork(), state.genesis_validators_root(), ); - let message = indexed_attestation.data.signing_root(domain); + let message = indexed_attestation.data().signing_root(domain); Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } @@ -312,21 +312,21 @@ where E: EthSpec, F: Fn(usize) -> Option>, { - let mut pubkeys = Vec::with_capacity(indexed_attestation.attesting_indices.len()); - for &validator_idx in &indexed_attestation.attesting_indices { + let mut pubkeys = Vec::with_capacity(indexed_attestation.attesting_indices_len()); + for &validator_idx in indexed_attestation.attesting_indices_iter() { pubkeys.push( get_pubkey(validator_idx as usize).ok_or(Error::ValidatorUnknown(validator_idx))?, ); } let domain = spec.get_domain( - indexed_attestation.data.target.epoch, + indexed_attestation.data().target.epoch, Domain::BeaconAttester, fork, genesis_validators_root, ); - let message = indexed_attestation.data.signing_root(domain); + let message = indexed_attestation.data().signing_root(domain); Ok(SignatureSet::multiple_pubkeys(signature, pubkeys, message)) } @@ -335,7 +335,7 @@ where pub fn attester_slashing_signature_sets<'a, E, F>( state: &'a BeaconState, get_pubkey: F, - attester_slashing: &'a AttesterSlashing, + attester_slashing: AttesterSlashingRef<'a, E>, spec: &'a ChainSpec, ) -> Result<(SignatureSet<'a>, SignatureSet<'a>)> where @@ -346,15 +346,15 @@ where indexed_attestation_signature_set( state, get_pubkey.clone(), - &attester_slashing.attestation_1.signature, - &attester_slashing.attestation_1, + attester_slashing.attestation_1().signature(), + attester_slashing.attestation_1(), spec, )?, indexed_attestation_signature_set( state, get_pubkey, - &attester_slashing.attestation_2.signature, - &attester_slashing.attestation_2, + attester_slashing.attestation_2().signature(), + attester_slashing.attestation_2(), spec, )?, )) @@ -425,7 +425,7 @@ where E: EthSpec, F: Fn(usize) -> Option>, { - let slot = signed_aggregate_and_proof.message.aggregate.data.slot; + let slot = signed_aggregate_and_proof.message().aggregate().data().slot; let domain = spec.get_domain( slot.epoch(E::slots_per_epoch()), @@ -434,9 +434,8 @@ where genesis_validators_root, ); let message = slot.signing_root(domain); - let signature = &signed_aggregate_and_proof.message.selection_proof; - let validator_index = signed_aggregate_and_proof.message.aggregator_index; - + let signature = signed_aggregate_and_proof.message().selection_proof(); + let validator_index = signed_aggregate_and_proof.message().aggregator_index(); Ok(SignatureSet::single_pubkey( signature, get_pubkey(validator_index as usize).ok_or(Error::ValidatorUnknown(validator_index))?, @@ -456,9 +455,9 @@ where F: Fn(usize) -> Option>, { let target_epoch = signed_aggregate_and_proof - .message - .aggregate - .data + .message() + .aggregate() + .data() .target .epoch; @@ -468,9 +467,9 @@ where fork, genesis_validators_root, ); - let message = signed_aggregate_and_proof.message.signing_root(domain); - let signature = &signed_aggregate_and_proof.signature; - let validator_index = signed_aggregate_and_proof.message.aggregator_index; + let message = signed_aggregate_and_proof.message().signing_root(domain); + let signature = signed_aggregate_and_proof.signature(); + let validator_index = signed_aggregate_and_proof.message().aggregator_index(); Ok(SignatureSet::single_pubkey( signature, diff --git a/consensus/state_processing/src/per_block_processing/tests.rs b/consensus/state_processing/src/per_block_processing/tests.rs index f0055fa80d..2774dd3d87 100644 --- a/consensus/state_processing/src/per_block_processing/tests.rs +++ b/consensus/state_processing/src/per_block_processing/tests.rs @@ -388,8 +388,13 @@ async fn invalid_attestation_no_committee_for_index() { .clone() .deconstruct() .0; - head_block.to_mut().body_mut().attestations_mut()[0] - .data + head_block + .to_mut() + .body_mut() + .attestations_mut() + .next() + .unwrap() + .data_mut() .index += 1; let mut ctxt = ConsensusContext::new(state.slot()); let result = process_operations::process_attestations( @@ -423,11 +428,22 @@ async fn invalid_attestation_wrong_justified_checkpoint() { .clone() .deconstruct() .0; - let old_justified_checkpoint = head_block.body().attestations()[0].data.source; + let old_justified_checkpoint = head_block + .body() + .attestations() + .next() + .unwrap() + .data() + .source; let mut new_justified_checkpoint = old_justified_checkpoint; new_justified_checkpoint.epoch += Epoch::new(1); - head_block.to_mut().body_mut().attestations_mut()[0] - .data + head_block + .to_mut() + .body_mut() + .attestations_mut() + .next() + .unwrap() + .data_mut() .source = new_justified_checkpoint; let mut ctxt = ConsensusContext::new(state.slot()); @@ -467,8 +483,14 @@ async fn invalid_attestation_bad_aggregation_bitfield_len() { .clone() .deconstruct() .0; - head_block.to_mut().body_mut().attestations_mut()[0].aggregation_bits = - Bitfield::with_capacity(spec.target_committee_size).unwrap(); + *head_block + .to_mut() + .body_mut() + .attestations_mut() + .next() + .unwrap() + .aggregation_bits_base_mut() + .unwrap() = Bitfield::with_capacity(spec.target_committee_size).unwrap(); let mut ctxt = ConsensusContext::new(state.slot()); let result = process_operations::process_attestations( @@ -501,7 +523,13 @@ async fn invalid_attestation_bad_signature() { .clone() .deconstruct() .0; - head_block.to_mut().body_mut().attestations_mut()[0].signature = AggregateSignature::empty(); + *head_block + .to_mut() + .body_mut() + .attestations_mut() + .next() + .unwrap() + .signature_mut() = AggregateSignature::empty(); let mut ctxt = ConsensusContext::new(state.slot()); let result = process_operations::process_attestations( @@ -536,10 +564,15 @@ async fn invalid_attestation_included_too_early() { .clone() .deconstruct() .0; - let new_attesation_slot = head_block.body().attestations()[0].data.slot + let new_attesation_slot = head_block.body().attestations().next().unwrap().data().slot + Slot::new(MainnetEthSpec::slots_per_epoch()); - head_block.to_mut().body_mut().attestations_mut()[0] - .data + head_block + .to_mut() + .body_mut() + .attestations_mut() + .next() + .unwrap() + .data_mut() .slot = new_attesation_slot; let mut ctxt = ConsensusContext::new(state.slot()); @@ -579,10 +612,15 @@ async fn invalid_attestation_included_too_late() { .clone() .deconstruct() .0; - let new_attesation_slot = head_block.body().attestations()[0].data.slot + let new_attesation_slot = head_block.body().attestations().next().unwrap().data().slot - Slot::new(MainnetEthSpec::slots_per_epoch()); - head_block.to_mut().body_mut().attestations_mut()[0] - .data + head_block + .to_mut() + .body_mut() + .attestations_mut() + .next() + .unwrap() + .data_mut() .slot = new_attesation_slot; let mut ctxt = ConsensusContext::new(state.slot()); @@ -619,8 +657,13 @@ async fn invalid_attestation_target_epoch_slot_mismatch() { .clone() .deconstruct() .0; - head_block.to_mut().body_mut().attestations_mut()[0] - .data + head_block + .to_mut() + .body_mut() + .attestations_mut() + .next() + .unwrap() + .data_mut() .target .epoch += Epoch::new(1); @@ -655,7 +698,7 @@ async fn valid_insert_attester_slashing() { let mut ctxt = ConsensusContext::new(state.slot()); let result = process_operations::process_attester_slashings( &mut state, - &[attester_slashing], + [attester_slashing.to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, &spec, @@ -671,13 +714,20 @@ async fn invalid_attester_slashing_not_slashable() { let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); - attester_slashing.attestation_1 = attester_slashing.attestation_2.clone(); + match &mut attester_slashing { + AttesterSlashing::Base(ref mut attester_slashing) => { + attester_slashing.attestation_1 = attester_slashing.attestation_2.clone(); + } + AttesterSlashing::Electra(ref mut attester_slashing) => { + attester_slashing.attestation_1 = attester_slashing.attestation_2.clone(); + } + } let mut state = harness.get_current_state(); let mut ctxt = ConsensusContext::new(state.slot()); let result = process_operations::process_attester_slashings( &mut state, - &[attester_slashing], + [attester_slashing.to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, &spec, @@ -699,13 +749,20 @@ async fn invalid_attester_slashing_1_invalid() { let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); - attester_slashing.attestation_1.attesting_indices = VariableList::from(vec![2, 1]); + match &mut attester_slashing { + AttesterSlashing::Base(ref mut attester_slashing) => { + attester_slashing.attestation_1.attesting_indices = VariableList::from(vec![2, 1]); + } + AttesterSlashing::Electra(ref mut attester_slashing) => { + attester_slashing.attestation_1.attesting_indices = VariableList::from(vec![2, 1]); + } + } let mut state = harness.get_current_state(); let mut ctxt = ConsensusContext::new(state.slot()); let result = process_operations::process_attester_slashings( &mut state, - &[attester_slashing], + [attester_slashing.to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, &spec, @@ -730,13 +787,20 @@ async fn invalid_attester_slashing_2_invalid() { let harness = get_harness::(EPOCH_OFFSET, VALIDATOR_COUNT).await; let mut attester_slashing = harness.make_attester_slashing(vec![1, 2]); - attester_slashing.attestation_2.attesting_indices = VariableList::from(vec![2, 1]); + match &mut attester_slashing { + AttesterSlashing::Base(ref mut attester_slashing) => { + attester_slashing.attestation_2.attesting_indices = VariableList::from(vec![2, 1]); + } + AttesterSlashing::Electra(ref mut attester_slashing) => { + attester_slashing.attestation_2.attesting_indices = VariableList::from(vec![2, 1]); + } + } let mut state = harness.get_current_state(); let mut ctxt = ConsensusContext::new(state.slot()); let result = process_operations::process_attester_slashings( &mut state, - &[attester_slashing], + [attester_slashing.to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, &spec, diff --git a/consensus/state_processing/src/per_block_processing/verify_attestation.rs b/consensus/state_processing/src/per_block_processing/verify_attestation.rs index c904ba55f0..6bfb5d7cfe 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attestation.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attestation.rs @@ -17,12 +17,12 @@ fn error(reason: Invalid) -> BlockOperationError { /// Optionally verifies the aggregate signature, depending on `verify_signatures`. pub fn verify_attestation_for_block_inclusion<'ctxt, E: EthSpec>( state: &BeaconState, - attestation: &Attestation, + attestation: AttestationRef<'ctxt, E>, ctxt: &'ctxt mut ConsensusContext, verify_signatures: VerifySignatures, spec: &ChainSpec, -) -> Result<&'ctxt IndexedAttestation> { - let data = &attestation.data; +) -> Result> { + let data = attestation.data(); verify!( data.slot.safe_add(spec.min_attestation_inclusion_delay)? <= state.slot(), @@ -61,12 +61,12 @@ pub fn verify_attestation_for_block_inclusion<'ctxt, E: EthSpec>( /// Spec v0.12.1 pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( state: &BeaconState, - attestation: &Attestation, + attestation: AttestationRef<'ctxt, E>, ctxt: &'ctxt mut ConsensusContext, verify_signatures: VerifySignatures, spec: &ChainSpec, -) -> Result<&'ctxt IndexedAttestation> { - let data = &attestation.data; +) -> Result> { + let data = attestation.data(); verify!( data.index < state.get_committee_count_at_slot(data.slot)?, @@ -87,10 +87,10 @@ pub fn verify_attestation_for_state<'ctxt, E: EthSpec>( /// /// Spec v0.12.1 fn verify_casper_ffg_vote( - attestation: &Attestation, + attestation: AttestationRef, state: &BeaconState, ) -> Result<()> { - let data = &attestation.data; + let data = attestation.data(); verify!( data.target.epoch == data.slot.epoch(E::slots_per_epoch()), Invalid::TargetEpochSlotMismatch { diff --git a/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs b/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs index 0cb215fe93..7fe4c8bc08 100644 --- a/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs +++ b/consensus/state_processing/src/per_block_processing/verify_attester_slashing.rs @@ -18,12 +18,12 @@ fn error(reason: Invalid) -> BlockOperationError { /// invalidity. pub fn verify_attester_slashing( state: &BeaconState, - attester_slashing: &AttesterSlashing, + attester_slashing: AttesterSlashingRef<'_, E>, verify_signatures: VerifySignatures, spec: &ChainSpec, ) -> Result> { - let attestation_1 = &attester_slashing.attestation_1; - let attestation_2 = &attester_slashing.attestation_2; + let attestation_1 = attester_slashing.attestation_1(); + let attestation_2 = attester_slashing.attestation_2(); // Spec: is_slashable_attestation_data verify!( @@ -45,7 +45,7 @@ pub fn verify_attester_slashing( /// Returns Ok(indices) if `indices.len() > 0` pub fn get_slashable_indices( state: &BeaconState, - attester_slashing: &AttesterSlashing, + attester_slashing: AttesterSlashingRef<'_, E>, ) -> Result> { get_slashable_indices_modular(state, attester_slashing, |_, validator| { validator.is_slashable_at(state.current_epoch()) @@ -56,23 +56,22 @@ pub fn get_slashable_indices( /// for determining whether a given validator should be considered slashable. pub fn get_slashable_indices_modular( state: &BeaconState, - attester_slashing: &AttesterSlashing, + attester_slashing: AttesterSlashingRef<'_, E>, is_slashable: F, ) -> Result> where F: Fn(u64, &Validator) -> bool, { - let attestation_1 = &attester_slashing.attestation_1; - let attestation_2 = &attester_slashing.attestation_2; + let attestation_1 = attester_slashing.attestation_1(); + let attestation_2 = attester_slashing.attestation_2(); let attesting_indices_1 = attestation_1 - .attesting_indices - .iter() + .attesting_indices_iter() .cloned() .collect::>(); + let attesting_indices_2 = attestation_2 - .attesting_indices - .iter() + .attesting_indices_iter() .cloned() .collect::>(); diff --git a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs index 86a2f20985..c5ec80b92a 100644 --- a/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs +++ b/consensus/state_processing/src/per_epoch_processing/base/validator_statuses.rs @@ -1,4 +1,4 @@ -use crate::common::get_attesting_indices; +use crate::common::attesting_indices_base::get_attesting_indices; use safe_arith::SafeArith; use types::{BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, PendingAttestation}; diff --git a/consensus/state_processing/src/upgrade/altair.rs b/consensus/state_processing/src/upgrade/altair.rs index 872560db3d..3006da25ae 100644 --- a/consensus/state_processing/src/upgrade/altair.rs +++ b/consensus/state_processing/src/upgrade/altair.rs @@ -1,5 +1,7 @@ use crate::common::update_progressive_balances_cache::initialize_progressive_balances_cache; -use crate::common::{get_attestation_participation_flag_indices, get_attesting_indices}; +use crate::common::{ + attesting_indices_base::get_attesting_indices, get_attestation_participation_flag_indices, +}; use std::mem; use std::sync::Arc; use types::{ diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index b3924cd973..c4b7c6a026 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -13,29 +13,132 @@ use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; use std::marker::PhantomData; use types::{ - AttesterSlashing, BeaconState, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, ProposerSlashing, + AttesterSlashing, AttesterSlashingBase, AttesterSlashingOnDisk, AttesterSlashingRefOnDisk, +}; +use types::{ + BeaconState, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, ProposerSlashing, SignedBlsToExecutionChange, SignedVoluntaryExit, }; const MAX_FORKS_VERIFIED_AGAINST: usize = 2; +pub trait TransformPersist { + type Persistable: Encode + Decode; + type PersistableRef<'a>: Encode + where + Self: 'a; + + /// Returns a reference to the object in a form that implements `Encode` + fn as_persistable_ref(&self) -> Self::PersistableRef<'_>; + + /// Converts the object back into its original form. + fn from_persistable(persistable: Self::Persistable) -> Self; +} + /// Wrapper around an operation type that acts as proof that its signature has been checked. /// /// The inner `op` field is private, meaning instances of this type can only be constructed /// by calling `validate`. -#[derive(Derivative, Debug, Clone, Encode, Decode)] +#[derive(Derivative, Debug, Clone)] #[derivative( PartialEq, Eq, - Hash(bound = "T: Encode + Decode + std::hash::Hash, E: EthSpec") + Hash(bound = "T: TransformPersist + std::hash::Hash, E: EthSpec") )] -pub struct SigVerifiedOp { +pub struct SigVerifiedOp { op: T, verified_against: VerifiedAgainst, - #[ssz(skip_serializing, skip_deserializing)] _phantom: PhantomData, } +impl Encode for SigVerifiedOp { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + && ::is_ssz_fixed_len() + } + + #[allow(clippy::expect_used)] + fn ssz_fixed_len() -> usize { + if ::is_ssz_fixed_len() { + ::ssz_fixed_len() + .checked_add(::ssz_fixed_len()) + .expect("encode ssz_fixed_len length overflow") + } else { + ssz::BYTES_PER_LENGTH_OFFSET + } + } + + #[allow(clippy::expect_used)] + fn ssz_bytes_len(&self) -> usize { + if ::is_ssz_fixed_len() { + ::ssz_fixed_len() + } else { + let persistable = self.op.as_persistable_ref(); + persistable + .ssz_bytes_len() + .checked_add(self.verified_against.ssz_bytes_len()) + .expect("ssz_bytes_len length overflow") + } + } + + fn ssz_append(&self, buf: &mut Vec) { + let mut encoder = ssz::SszEncoder::container(buf, ::ssz_fixed_len()); + let persistable = self.op.as_persistable_ref(); + encoder.append(&persistable); + encoder.append(&self.verified_against); + encoder.finalize(); + } +} + +impl Decode for SigVerifiedOp { + fn is_ssz_fixed_len() -> bool { + ::is_ssz_fixed_len() + && ::is_ssz_fixed_len() + } + + #[allow(clippy::expect_used)] + fn ssz_fixed_len() -> usize { + if ::is_ssz_fixed_len() { + ::ssz_fixed_len() + .checked_add(::ssz_fixed_len()) + .expect("decode ssz_fixed_len length overflow") + } else { + ssz::BYTES_PER_LENGTH_OFFSET + } + } + + fn from_ssz_bytes(bytes: &[u8]) -> Result { + let mut builder = ssz::SszDecoderBuilder::new(bytes); + + // Register types based on whether they are fixed or variable length + if ::is_ssz_fixed_len() { + builder.register_type::()?; + } else { + builder.register_anonymous_variable_length_item()?; + } + + if ::is_ssz_fixed_len() { + builder.register_type::()?; + } else { + builder.register_anonymous_variable_length_item()?; + } + + let mut decoder = builder.build()?; + // Decode each component + let persistable: T::Persistable = decoder.decode_next()?; + let verified_against: VerifiedAgainst = decoder.decode_next()?; + + // Use TransformPersist to convert persistable back into the original type + let op = T::from_persistable(persistable); + + Ok(SigVerifiedOp { + op, + verified_against, + _phantom: PhantomData, + }) + } +} + /// Information about the fork versions that this message was verified against. /// /// In general it is not safe to assume that a `SigVerifiedOp` constructed at some point in the past @@ -109,7 +212,7 @@ where } /// Trait for operations that can be verified and transformed into a `SigVerifiedOp`. -pub trait VerifyOperation: Encode + Decode + Sized { +pub trait VerifyOperation: TransformPersist + Sized { type Error; fn validate( @@ -152,15 +255,15 @@ impl VerifyOperation for AttesterSlashing { state: &BeaconState, spec: &ChainSpec, ) -> Result, Self::Error> { - verify_attester_slashing(state, &self, VerifySignatures::True, spec)?; + verify_attester_slashing(state, self.to_ref(), VerifySignatures::True, spec)?; Ok(SigVerifiedOp::new(self, state)) } #[allow(clippy::arithmetic_side_effects)] fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { smallvec![ - self.attestation_1.data.target.epoch, - self.attestation_2.data.target.epoch + self.attestation_1().data().target.epoch, + self.attestation_2().data().target.epoch ] } } @@ -237,3 +340,98 @@ impl VerifyOperationAt for SignedVoluntaryExit { Ok(SigVerifiedOp::new(self, state)) } } + +impl TransformPersist for SignedVoluntaryExit { + type Persistable = Self; + type PersistableRef<'a> = &'a Self; + + fn as_persistable_ref(&self) -> Self::PersistableRef<'_> { + self + } + + fn from_persistable(persistable: Self::Persistable) -> Self { + persistable + } +} + +impl TransformPersist for AttesterSlashing { + type Persistable = AttesterSlashingOnDisk; + type PersistableRef<'a> = AttesterSlashingRefOnDisk<'a, E>; + + fn as_persistable_ref(&self) -> Self::PersistableRef<'_> { + self.to_ref().into() + } + + fn from_persistable(persistable: Self::Persistable) -> Self { + persistable.into() + } +} + +// TODO: Remove this once we no longer support DB schema version 17 +impl TransformPersist for types::AttesterSlashingBase { + type Persistable = Self; + type PersistableRef<'a> = &'a Self; + + fn as_persistable_ref(&self) -> Self::PersistableRef<'_> { + self + } + + fn from_persistable(persistable: Self::Persistable) -> Self { + persistable + } +} +// TODO: Remove this once we no longer support DB schema version 17 +impl From, E>> + for SigVerifiedOp, E> +{ + fn from(base: SigVerifiedOp, E>) -> Self { + SigVerifiedOp { + op: AttesterSlashing::Base(base.op), + verified_against: base.verified_against, + _phantom: PhantomData, + } + } +} +// TODO: Remove this once we no longer support DB schema version 17 +impl TryFrom, E>> + for SigVerifiedOp, E> +{ + type Error = String; + + fn try_from(slashing: SigVerifiedOp, E>) -> Result { + match slashing.op { + AttesterSlashing::Base(base) => Ok(SigVerifiedOp { + op: base, + verified_against: slashing.verified_against, + _phantom: PhantomData, + }), + AttesterSlashing::Electra(_) => Err("non-base attester slashing".to_string()), + } + } +} + +impl TransformPersist for ProposerSlashing { + type Persistable = Self; + type PersistableRef<'a> = &'a Self; + + fn as_persistable_ref(&self) -> Self::PersistableRef<'_> { + self + } + + fn from_persistable(persistable: Self::Persistable) -> Self { + persistable + } +} + +impl TransformPersist for SignedBlsToExecutionChange { + type Persistable = Self; + type PersistableRef<'a> = &'a Self; + + fn as_persistable_ref(&self) -> Self::PersistableRef<'_> { + self + } + + fn from_persistable(persistable: Self::Persistable) -> Self { + persistable + } +} diff --git a/consensus/types/src/aggregate_and_proof.rs b/consensus/types/src/aggregate_and_proof.rs index bfbf4d97af..223b12e768 100644 --- a/consensus/types/src/aggregate_and_proof.rs +++ b/consensus/types/src/aggregate_and_proof.rs @@ -1,41 +1,79 @@ +use super::{AttestationBase, AttestationElectra, AttestationRef}; use super::{ - Attestation, ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, SelectionProof, - Signature, SignedRoot, + ChainSpec, Domain, EthSpec, Fork, Hash256, PublicKey, SecretKey, SelectionProof, Signature, + SignedRoot, }; use crate::test_utils::TestRandom; +use crate::Attestation; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; -/// A Validators aggregate attestation and selection proof. -/// -/// Spec v0.12.1 -#[derive( - arbitrary::Arbitrary, - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - TreeHash, +#[superstruct( + variants(Base, Electra), + variant_attributes( + derive( + arbitrary::Arbitrary, + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Encode, + Decode, + TestRandom, + TreeHash, + ), + serde(bound = "E: EthSpec"), + arbitrary(bound = "E: EthSpec"), + ), + ref_attributes( + derive(Debug, PartialEq, TreeHash, Serialize,), + serde(untagged, bound = "E: EthSpec"), + tree_hash(enum_behaviour = "transparent") + ), + map_ref_into(AttestationRef) )] -#[serde(bound = "E: EthSpec")] +#[derive( + arbitrary::Arbitrary, Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash, +)] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] #[arbitrary(bound = "E: EthSpec")] pub struct AggregateAndProof { /// The index of the validator that created the attestation. #[serde(with = "serde_utils::quoted_u64")] + #[superstruct(getter(copy))] pub aggregator_index: u64, /// The aggregate attestation. + #[superstruct(flatten)] pub aggregate: Attestation, /// A proof provided by the validator that permits them to publish on the /// `beacon_aggregate_and_proof` gossipsub topic. pub selection_proof: Signature, } +impl<'a, E: EthSpec> AggregateAndProofRef<'a, E> { + /// Returns `true` if `validator_pubkey` signed over `self.aggregate.data.slot`. + pub fn aggregate(self) -> AttestationRef<'a, E> { + map_aggregate_and_proof_ref_into_attestation_ref!(&'a _, self, |inner, cons| { + cons(&inner.aggregate) + }) + } +} +impl AggregateAndProof { + /// Returns `true` if `validator_pubkey` signed over `self.aggregate.data.slot`. + pub fn aggregate<'a>(&'a self) -> AttestationRef<'a, E> { + map_aggregate_and_proof_ref_into_attestation_ref!(&'a _, self.to_ref(), |inner, cons| { + cons(&inner.aggregate) + }) + } +} + impl AggregateAndProof { /// Produces a new `AggregateAndProof` with a `selection_proof` generated by signing /// `aggregate.data.slot` with `secret_key`. @@ -43,29 +81,47 @@ impl AggregateAndProof { /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, - aggregate: Attestation, + aggregate: AttestationRef<'_, E>, selection_proof: Option, secret_key: &SecretKey, fork: &Fork, genesis_validators_root: Hash256, spec: &ChainSpec, ) -> Self { - let selection_proof = selection_proof - .unwrap_or_else(|| { - SelectionProof::new::( - aggregate.data.slot, - secret_key, - fork, - genesis_validators_root, - spec, - ) - }) - .into(); + let selection_proof = selection_proof.unwrap_or_else(|| { + SelectionProof::new::( + aggregate.data().slot, + secret_key, + fork, + genesis_validators_root, + spec, + ) + }); - Self { + Self::from_attestation( aggregator_index, - aggregate, + aggregate.clone_as_attestation(), selection_proof, + ) + } + + /// Produces a new `AggregateAndProof` given a `selection_proof` + pub fn from_attestation( + aggregator_index: u64, + aggregate: Attestation, + selection_proof: SelectionProof, + ) -> Self { + match aggregate { + Attestation::Base(aggregate) => Self::Base(AggregateAndProofBase { + aggregator_index, + aggregate, + selection_proof: selection_proof.into(), + }), + Attestation::Electra(aggregate) => Self::Electra(AggregateAndProofElectra { + aggregator_index, + aggregate, + selection_proof: selection_proof.into(), + }), } } @@ -77,16 +133,17 @@ impl AggregateAndProof { genesis_validators_root: Hash256, spec: &ChainSpec, ) -> bool { - let target_epoch = self.aggregate.data.slot.epoch(E::slots_per_epoch()); + let target_epoch = self.aggregate().data().slot.epoch(E::slots_per_epoch()); let domain = spec.get_domain( target_epoch, Domain::SelectionProof, fork, genesis_validators_root, ); - let message = self.aggregate.data.slot.signing_root(domain); - self.selection_proof.verify(validator_pubkey, message) + let message = self.aggregate().data().slot.signing_root(domain); + self.selection_proof().verify(validator_pubkey, message) } } impl SignedRoot for AggregateAndProof {} +impl<'a, E: EthSpec> SignedRoot for AggregateAndProofRef<'a, E> {} diff --git a/consensus/types/src/attestation.rs b/consensus/types/src/attestation.rs index e43077d059..88993267a9 100644 --- a/consensus/types/src/attestation.rs +++ b/consensus/types/src/attestation.rs @@ -1,13 +1,16 @@ +use crate::slot_data::SlotData; +use crate::Checkpoint; +use crate::{test_utils::TestRandom, Hash256, Slot}; use derivative::Derivative; use safe_arith::ArithError; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use ssz_types::BitVector; +use std::hash::{Hash, Hasher}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; -use crate::slot_data::SlotData; -use crate::{test_utils::TestRandom, Hash256, Slot}; - use super::{ AggregateSignature, AttestationData, BitList, ChainSpec, Domain, EthSpec, Fork, SecretKey, Signature, SignedRoot, @@ -18,38 +21,272 @@ pub enum Error { SszTypesError(ssz_types::Error), AlreadySigned(usize), SubnetCountIsZero(ArithError), + IncorrectStateVariant, + InvalidCommitteeLength, + InvalidCommitteeIndex, } -/// Details an attestation that can be slashable. -/// -/// Spec v0.12.1 +#[superstruct( + variants(Base, Electra), + variant_attributes( + derive( + Debug, + Clone, + Serialize, + Deserialize, + Decode, + Encode, + TestRandom, + Derivative, + arbitrary::Arbitrary, + TreeHash, + ), + derivative(PartialEq, Hash(bound = "E: EthSpec")), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), + ), + ref_attributes(derive(TreeHash), tree_hash(enum_behaviour = "transparent")), + cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), + partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") +)] #[derive( - arbitrary::Arbitrary, Debug, Clone, Serialize, - Deserialize, - Encode, - Decode, TreeHash, - TestRandom, + Encode, Derivative, + Deserialize, + arbitrary::Arbitrary, + PartialEq, )] -#[derivative(PartialEq, Hash(bound = "E: EthSpec"))] -#[serde(bound = "E: EthSpec")] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] #[arbitrary(bound = "E: EthSpec")] pub struct Attestation { + #[superstruct(only(Base), partial_getter(rename = "aggregation_bits_base"))] pub aggregation_bits: BitList, + #[superstruct(only(Electra), partial_getter(rename = "aggregation_bits_electra"))] + pub aggregation_bits: BitList, pub data: AttestationData, + #[superstruct(only(Electra))] + pub committee_bits: BitVector, pub signature: AggregateSignature, } +impl Hash for Attestation { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + match self { + Attestation::Base(att) => att.hash(state), + Attestation::Electra(att) => att.hash(state), + } + } +} + impl Attestation { - /// Are the aggregation bitfields of these attestations disjoint? - pub fn signers_disjoint_from(&self, other: &Self) -> bool { - self.aggregation_bits - .intersection(&other.aggregation_bits) - .is_zero() + /// Produces an attestation with empty signature. + pub fn empty_for_signing( + committee_index: u64, + committee_length: usize, + slot: Slot, + beacon_block_root: Hash256, + source: Checkpoint, + target: Checkpoint, + spec: &ChainSpec, + ) -> Result { + if spec.fork_name_at_slot::(slot).electra_enabled() { + let mut committee_bits: BitVector = BitVector::default(); + committee_bits + .set(committee_index as usize, true) + .map_err(|_| Error::InvalidCommitteeIndex)?; + Ok(Attestation::Electra(AttestationElectra { + aggregation_bits: BitList::with_capacity(committee_length) + .map_err(|_| Error::InvalidCommitteeLength)?, + data: AttestationData { + slot, + index: 0u64, + beacon_block_root, + source, + target, + }, + committee_bits, + signature: AggregateSignature::infinity(), + })) + } else { + Ok(Attestation::Base(AttestationBase { + aggregation_bits: BitList::with_capacity(committee_length) + .map_err(|_| Error::InvalidCommitteeLength)?, + data: AttestationData { + slot, + index: committee_index, + beacon_block_root, + source, + target, + }, + signature: AggregateSignature::infinity(), + })) + } + } + + /// Aggregate another Attestation into this one. + /// + /// The aggregation bitfields must be disjoint, and the data must be the same. + pub fn aggregate(&mut self, other: AttestationRef) { + match self { + Attestation::Base(att) => match other { + AttestationRef::Base(oth) => { + att.aggregate(oth); + } + AttestationRef::Electra(_) => { + debug_assert!(false, "Cannot aggregate base and electra attestations"); + } + }, + Attestation::Electra(att) => match other { + AttestationRef::Base(_) => { + debug_assert!(false, "Cannot aggregate base and electra attestations"); + } + AttestationRef::Electra(oth) => { + att.aggregate(oth); + } + }, + } + } + + /// Signs `self`, setting the `committee_position`'th bit of `aggregation_bits` to `true`. + /// + /// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`. + pub fn sign( + &mut self, + secret_key: &SecretKey, + committee_position: usize, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> Result<(), Error> { + match self { + Attestation::Base(att) => att.sign( + secret_key, + committee_position, + fork, + genesis_validators_root, + spec, + ), + Attestation::Electra(att) => att.sign( + secret_key, + committee_position, + fork, + genesis_validators_root, + spec, + ), + } + } + + /// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`. + pub fn add_signature( + &mut self, + signature: &Signature, + committee_position: usize, + ) -> Result<(), Error> { + match self { + Attestation::Base(att) => att.add_signature(signature, committee_position), + Attestation::Electra(att) => att.add_signature(signature, committee_position), + } + } + + pub fn committee_index(&self) -> Option { + match self { + Attestation::Base(att) => Some(att.data.index), + Attestation::Electra(att) => att.committee_index(), + } + } + + pub fn is_aggregation_bits_zero(&self) -> bool { + match self { + Attestation::Base(att) => att.aggregation_bits.is_zero(), + Attestation::Electra(att) => att.aggregation_bits.is_zero(), + } + } + + pub fn num_set_aggregation_bits(&self) -> usize { + match self { + Attestation::Base(att) => att.aggregation_bits.num_set_bits(), + Attestation::Electra(att) => att.aggregation_bits.num_set_bits(), + } + } + + pub fn get_aggregation_bit(&self, index: usize) -> Result { + match self { + Attestation::Base(att) => att.aggregation_bits.get(index), + Attestation::Electra(att) => att.aggregation_bits.get(index), + } + } +} + +impl<'a, E: EthSpec> AttestationRef<'a, E> { + pub fn clone_as_attestation(self) -> Attestation { + match self { + Self::Base(att) => Attestation::Base(att.clone()), + Self::Electra(att) => Attestation::Electra(att.clone()), + } + } + + pub fn is_aggregation_bits_zero(self) -> bool { + match self { + Self::Base(att) => att.aggregation_bits.is_zero(), + Self::Electra(att) => att.aggregation_bits.is_zero(), + } + } + + pub fn num_set_aggregation_bits(&self) -> usize { + match self { + Self::Base(att) => att.aggregation_bits.num_set_bits(), + Self::Electra(att) => att.aggregation_bits.num_set_bits(), + } + } + + pub fn committee_index(&self) -> Option { + match self { + AttestationRef::Base(att) => Some(att.data.index), + AttestationRef::Electra(att) => att.committee_index(), + } + } + + pub fn set_aggregation_bits(&self) -> Vec { + match self { + Self::Base(att) => att + .aggregation_bits + .iter() + .enumerate() + .filter(|(_i, bit)| *bit) + .map(|(i, _bit)| i) + .collect::>(), + Self::Electra(att) => att + .aggregation_bits + .iter() + .enumerate() + .filter(|(_i, bit)| *bit) + .map(|(i, _bit)| i) + .collect::>(), + } + } +} + +impl AttestationElectra { + pub fn committee_index(&self) -> Option { + self.get_committee_indices().first().cloned() + } + + pub fn get_committee_indices(&self) -> Vec { + self.committee_bits + .iter() + .enumerate() + .filter_map(|(index, bit)| if bit { Some(index as u64) } else { None }) + .collect() } /// Aggregate another Attestation into this one. @@ -57,8 +294,6 @@ impl Attestation { /// The aggregation bitfields must be disjoint, and the data must be the same. pub fn aggregate(&mut self, other: &Self) { debug_assert_eq!(self.data, other.data); - debug_assert!(self.signers_disjoint_from(other)); - self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); self.signature.add_assign_aggregate(&other.signature); } @@ -111,9 +346,144 @@ impl Attestation { } } +impl AttestationBase { + /// Aggregate another Attestation into this one. + /// + /// The aggregation bitfields must be disjoint, and the data must be the same. + pub fn aggregate(&mut self, other: &Self) { + debug_assert_eq!(self.data, other.data); + self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); + self.signature.add_assign_aggregate(&other.signature); + } + + /// Signs `self`, setting the `committee_position`'th bit of `aggregation_bits` to `true`. + /// + /// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`. + pub fn sign( + &mut self, + secret_key: &SecretKey, + committee_position: usize, + fork: &Fork, + genesis_validators_root: Hash256, + spec: &ChainSpec, + ) -> Result<(), Error> { + let domain = spec.get_domain( + self.data.target.epoch, + Domain::BeaconAttester, + fork, + genesis_validators_root, + ); + let message = self.data.signing_root(domain); + + self.add_signature(&secret_key.sign(message), committee_position) + } + + /// Adds `signature` to `self` and sets the `committee_position`'th bit of `aggregation_bits` to `true`. + /// + /// Returns an `AlreadySigned` error if the `committee_position`'th bit is already `true`. + pub fn add_signature( + &mut self, + signature: &Signature, + committee_position: usize, + ) -> Result<(), Error> { + if self + .aggregation_bits + .get(committee_position) + .map_err(Error::SszTypesError)? + { + Err(Error::AlreadySigned(committee_position)) + } else { + self.aggregation_bits + .set(committee_position, true) + .map_err(Error::SszTypesError)?; + + self.signature.add_assign(signature); + + Ok(()) + } + } + + pub fn extend_aggregation_bits( + &self, + ) -> Result, ssz_types::Error> { + let mut extended_aggregation_bits: BitList = + BitList::with_capacity(self.aggregation_bits.len())?; + + for (i, bit) in self.aggregation_bits.iter().enumerate() { + extended_aggregation_bits.set(i, bit)?; + } + Ok(extended_aggregation_bits) + } +} + impl SlotData for Attestation { fn get_slot(&self) -> Slot { - self.data.slot + self.data().slot + } +} + +impl<'a, E: EthSpec> SlotData for AttestationRef<'a, E> { + fn get_slot(&self) -> Slot { + self.data().slot + } +} + +#[derive(Debug, Clone, Encode, Decode, PartialEq)] +#[ssz(enum_behaviour = "union")] +pub enum AttestationOnDisk { + Base(AttestationBase), + Electra(AttestationElectra), +} + +impl AttestationOnDisk { + pub fn to_ref(&self) -> AttestationRefOnDisk { + match self { + AttestationOnDisk::Base(att) => AttestationRefOnDisk::Base(att), + AttestationOnDisk::Electra(att) => AttestationRefOnDisk::Electra(att), + } + } +} + +#[derive(Debug, Clone, Encode)] +#[ssz(enum_behaviour = "union")] +pub enum AttestationRefOnDisk<'a, E: EthSpec> { + Base(&'a AttestationBase), + Electra(&'a AttestationElectra), +} + +impl From> for AttestationOnDisk { + fn from(attestation: Attestation) -> Self { + match attestation { + Attestation::Base(attestation) => Self::Base(attestation), + Attestation::Electra(attestation) => Self::Electra(attestation), + } + } +} + +impl From> for Attestation { + fn from(attestation: AttestationOnDisk) -> Self { + match attestation { + AttestationOnDisk::Base(attestation) => Self::Base(attestation), + AttestationOnDisk::Electra(attestation) => Self::Electra(attestation), + } + } +} + +impl<'a, E: EthSpec> From> for AttestationRefOnDisk<'a, E> { + fn from(attestation: AttestationRef<'a, E>) -> Self { + match attestation { + AttestationRef::Base(attestation) => Self::Base(attestation), + AttestationRef::Electra(attestation) => Self::Electra(attestation), + } + } +} + +impl<'a, E: EthSpec> From> for AttestationRef<'a, E> { + fn from(attestation: AttestationRefOnDisk<'a, E>) -> Self { + match attestation { + AttestationRefOnDisk::Base(attestation) => Self::Base(attestation), + AttestationRefOnDisk::Electra(attestation) => Self::Electra(attestation), + } } } @@ -128,7 +498,7 @@ mod tests { // This test will only pass with `blst`, if we run these tests with another // BLS library in future we will have to make it generic. #[test] - fn size_of() { + fn size_of_base() { use std::mem::size_of; let aggregation_bits = @@ -143,10 +513,41 @@ mod tests { let attestation_expected = aggregation_bits + attestation_data + signature; assert_eq!(attestation_expected, 488); assert_eq!( - size_of::>(), + size_of::>(), attestation_expected ); } - ssz_and_tree_hash_tests!(Attestation); + #[test] + fn size_of_electra() { + use std::mem::size_of; + + let aggregation_bits = + size_of::::MaxValidatorsPerSlot>>(); + let attestation_data = size_of::(); + let committee_bits = + size_of::::MaxCommitteesPerSlot>>(); + let signature = size_of::(); + + assert_eq!(aggregation_bits, 56); + assert_eq!(committee_bits, 56); + assert_eq!(attestation_data, 128); + assert_eq!(signature, 288 + 16); + + let attestation_expected = aggregation_bits + committee_bits + attestation_data + signature; + assert_eq!(attestation_expected, 544); + assert_eq!( + size_of::>(), + attestation_expected + ); + } + + mod base { + use super::*; + ssz_and_tree_hash_tests!(AttestationBase); + } + mod electra { + use super::*; + ssz_and_tree_hash_tests!(AttestationElectra); + } } diff --git a/consensus/types/src/attester_slashing.rs b/consensus/types/src/attester_slashing.rs index 5ad5297d0c..a8d4e6989c 100644 --- a/consensus/types/src/attester_slashing.rs +++ b/consensus/types/src/attester_slashing.rs @@ -1,38 +1,175 @@ -use crate::{test_utils::TestRandom, EthSpec, IndexedAttestation}; - +use crate::indexed_attestation::{ + IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, +}; +use crate::{test_utils::TestRandom, EthSpec}; use derivative::Derivative; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; -/// Two conflicting attestations. -/// -/// Spec v0.12.1 +#[superstruct( + variants(Base, Electra), + variant_attributes( + derive( + Derivative, + Debug, + Clone, + Serialize, + Deserialize, + Encode, + Decode, + TreeHash, + TestRandom, + arbitrary::Arbitrary + ), + derivative(PartialEq, Eq, Hash(bound = "E: EthSpec")), + serde(bound = "E: EthSpec"), + arbitrary(bound = "E: EthSpec") + ), + ref_attributes(derive(Debug)) +)] #[derive( - Derivative, - Debug, - Clone, - Serialize, - Deserialize, - Encode, - Decode, - TreeHash, - TestRandom, - arbitrary::Arbitrary, + Debug, Clone, Serialize, Encode, Deserialize, TreeHash, Derivative, arbitrary::Arbitrary, )] #[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] -#[serde(bound = "E: EthSpec")] +#[serde(bound = "E: EthSpec", untagged)] #[arbitrary(bound = "E: EthSpec")] +#[ssz(enum_behaviour = "transparent")] +#[tree_hash(enum_behaviour = "transparent")] pub struct AttesterSlashing { + #[superstruct(flatten)] pub attestation_1: IndexedAttestation, + #[superstruct(flatten)] pub attestation_2: IndexedAttestation, } +/// This is a copy of the `AttesterSlashing` enum but with `Encode` and `Decode` derived +/// using the `union` behavior for the purposes of persistence on disk. We use a separate +/// type so that we don't accidentally use this non-spec encoding in consensus objects. +#[derive(Debug, Clone, Encode, Decode, Derivative)] +#[derivative(PartialEq, Eq, Hash(bound = "E: EthSpec"))] +#[ssz(enum_behaviour = "union")] +pub enum AttesterSlashingOnDisk { + Base(AttesterSlashingBase), + Electra(AttesterSlashingElectra), +} + +#[derive(Debug, Clone, Encode)] +#[ssz(enum_behaviour = "union")] +pub enum AttesterSlashingRefOnDisk<'a, E: EthSpec> { + Base(&'a AttesterSlashingBase), + Electra(&'a AttesterSlashingElectra), +} + +impl From> for AttesterSlashingOnDisk { + fn from(attester_slashing: AttesterSlashing) -> Self { + match attester_slashing { + AttesterSlashing::Base(attester_slashing) => Self::Base(attester_slashing), + AttesterSlashing::Electra(attester_slashing) => Self::Electra(attester_slashing), + } + } +} + +impl From> for AttesterSlashing { + fn from(attester_slashing: AttesterSlashingOnDisk) -> Self { + match attester_slashing { + AttesterSlashingOnDisk::Base(attester_slashing) => Self::Base(attester_slashing), + AttesterSlashingOnDisk::Electra(attester_slashing) => Self::Electra(attester_slashing), + } + } +} + +impl<'a, E: EthSpec> From> for AttesterSlashingRef<'a, E> { + fn from(attester_slashing: AttesterSlashingRefOnDisk<'a, E>) -> Self { + match attester_slashing { + AttesterSlashingRefOnDisk::Base(attester_slashing) => Self::Base(attester_slashing), + AttesterSlashingRefOnDisk::Electra(attester_slashing) => { + Self::Electra(attester_slashing) + } + } + } +} + +impl<'a, E: EthSpec> From> for AttesterSlashingRefOnDisk<'a, E> { + fn from(attester_slashing: AttesterSlashingRef<'a, E>) -> Self { + match attester_slashing { + AttesterSlashingRef::Base(attester_slashing) => Self::Base(attester_slashing), + AttesterSlashingRef::Electra(attester_slashing) => Self::Electra(attester_slashing), + } + } +} + +impl<'a, E: EthSpec> AttesterSlashingRef<'a, E> { + pub fn clone_as_attester_slashing(self) -> AttesterSlashing { + match self { + AttesterSlashingRef::Base(attester_slashing) => { + AttesterSlashing::Base(attester_slashing.clone()) + } + AttesterSlashingRef::Electra(attester_slashing) => { + AttesterSlashing::Electra(attester_slashing.clone()) + } + } + } + + pub fn attestation_1(&self) -> IndexedAttestationRef<'a, E> { + match self { + AttesterSlashingRef::Base(attester_slashing) => { + IndexedAttestationRef::Base(&attester_slashing.attestation_1) + } + AttesterSlashingRef::Electra(attester_slashing) => { + IndexedAttestationRef::Electra(&attester_slashing.attestation_1) + } + } + } + + pub fn attestation_2(&self) -> IndexedAttestationRef<'a, E> { + match self { + AttesterSlashingRef::Base(attester_slashing) => { + IndexedAttestationRef::Base(&attester_slashing.attestation_2) + } + AttesterSlashingRef::Electra(attester_slashing) => { + IndexedAttestationRef::Electra(&attester_slashing.attestation_2) + } + } + } +} + +impl AttesterSlashing { + pub fn attestation_1(&self) -> IndexedAttestationRef { + match self { + AttesterSlashing::Base(attester_slashing) => { + IndexedAttestationRef::Base(&attester_slashing.attestation_1) + } + AttesterSlashing::Electra(attester_slashing) => { + IndexedAttestationRef::Electra(&attester_slashing.attestation_1) + } + } + } + + pub fn attestation_2(&self) -> IndexedAttestationRef { + match self { + AttesterSlashing::Base(attester_slashing) => { + IndexedAttestationRef::Base(&attester_slashing.attestation_2) + } + AttesterSlashing::Electra(attester_slashing) => { + IndexedAttestationRef::Electra(&attester_slashing.attestation_2) + } + } + } +} + #[cfg(test)] mod tests { use super::*; use crate::*; - - ssz_and_tree_hash_tests!(AttesterSlashing); + mod base { + use super::*; + ssz_and_tree_hash_tests!(AttesterSlashingBase); + } + mod electra { + use super::*; + ssz_and_tree_hash_tests!(AttesterSlashingElectra); + } } diff --git a/consensus/types/src/beacon_block.rs b/consensus/types/src/beacon_block.rs index ed3d182772..f67a965955 100644 --- a/consensus/types/src/beacon_block.rs +++ b/consensus/types/src/beacon_block.rs @@ -1,3 +1,4 @@ +use crate::attestation::AttestationBase; use crate::test_utils::TestRandom; use crate::*; use derivative::Derivative; @@ -11,6 +12,8 @@ use test_random_derive::TestRandom; use tree_hash::TreeHash; use tree_hash_derive::TreeHash; +use self::indexed_attestation::{IndexedAttestationBase, IndexedAttestationElectra}; + /// A block of the `BeaconChain`. #[superstruct( variants(Base, Altair, Bellatrix, Capella, Deneb, Electra), @@ -325,7 +328,7 @@ impl> BeaconBlockBase { message: header, signature: Signature::empty(), }; - let indexed_attestation: IndexedAttestation = IndexedAttestation { + let indexed_attestation = IndexedAttestationBase { attesting_indices: VariableList::new(vec![ 0_u64; E::MaxValidatorsPerCommittee::to_usize() @@ -346,12 +349,12 @@ impl> BeaconBlockBase { signed_header_2: signed_header, }; - let attester_slashing = AttesterSlashing { + let attester_slashing = AttesterSlashingBase { attestation_1: indexed_attestation.clone(), attestation_2: indexed_attestation, }; - let attestation: Attestation = Attestation { + let attestation = AttestationBase { aggregation_bits: BitList::with_capacity(E::MaxValidatorsPerCommittee::to_usize()) .unwrap(), data: AttestationData::default(), @@ -604,6 +607,31 @@ impl> BeaconBlockElectra /// Return a Electra block where the block has maximum size. pub fn full(spec: &ChainSpec) -> Self { let base_block: BeaconBlockBase<_, Payload> = BeaconBlockBase::full(spec); + let indexed_attestation: IndexedAttestationElectra = IndexedAttestationElectra { + attesting_indices: VariableList::new(vec![0_u64; E::MaxValidatorsPerSlot::to_usize()]) + .unwrap(), + data: AttestationData::default(), + signature: AggregateSignature::empty(), + }; + let attester_slashings = vec![ + AttesterSlashingElectra { + attestation_1: indexed_attestation.clone(), + attestation_2: indexed_attestation, + }; + E::max_attester_slashings_electra() + ] + .into(); + let attestation = AttestationElectra { + aggregation_bits: BitList::with_capacity(E::MaxValidatorsPerSlot::to_usize()).unwrap(), + data: AttestationData::default(), + signature: AggregateSignature::empty(), + committee_bits: BitVector::new(), + }; + let mut attestations_electra = vec![]; + for _ in 0..E::MaxAttestationsElectra::to_usize() { + attestations_electra.push(attestation.clone()); + } + let bls_to_execution_changes = vec![ SignedBlsToExecutionChange { message: BlsToExecutionChange { @@ -627,8 +655,8 @@ impl> BeaconBlockElectra state_root: Hash256::zero(), body: BeaconBlockBodyElectra { proposer_slashings: base_block.body.proposer_slashings, - attester_slashings: base_block.body.attester_slashings, - attestations: base_block.body.attestations, + attester_slashings, + attestations: attestations_electra.into(), deposits: base_block.body.deposits, voluntary_exits: base_block.body.voluntary_exits, bls_to_execution_changes, @@ -642,6 +670,7 @@ impl> BeaconBlockElectra graffiti: Graffiti::default(), execution_payload: Payload::Electra::default(), blob_kzg_commitments: VariableList::empty(), + consolidations: VariableList::empty(), }, } } @@ -672,6 +701,7 @@ impl> EmptyBlock for BeaconBlockElec execution_payload: Payload::Electra::default(), bls_to_execution_changes: VariableList::empty(), blob_kzg_commitments: VariableList::empty(), + consolidations: VariableList::empty(), }, } } diff --git a/consensus/types/src/beacon_block_body.rs b/consensus/types/src/beacon_block_body.rs index c3077c4ab6..9cf66184f8 100644 --- a/consensus/types/src/beacon_block_body.rs +++ b/consensus/types/src/beacon_block_body.rs @@ -2,6 +2,7 @@ use crate::test_utils::TestRandom; use crate::*; use derivative::Derivative; use merkle_proof::{MerkleTree, MerkleTreeError}; +use metastruct::metastruct; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; use std::marker::PhantomData; @@ -50,6 +51,14 @@ pub const BLOB_KZG_COMMITMENTS_INDEX: usize = 11; ), arbitrary(bound = "E: EthSpec, Payload: AbstractExecPayload"), ), + specific_variant_attributes( + Base(metastruct(mappings(beacon_block_body_base_fields(groups(fields))))), + Altair(metastruct(mappings(beacon_block_body_altair_fields(groups(fields))))), + Bellatrix(metastruct(mappings(beacon_block_body_bellatrix_fields(groups(fields))))), + Capella(metastruct(mappings(beacon_block_body_capella_fields(groups(fields))))), + Deneb(metastruct(mappings(beacon_block_body_deneb_fields(groups(fields))))), + Electra(metastruct(mappings(beacon_block_body_electra_fields(groups(fields))))), + ), cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"), partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant") )] @@ -63,8 +72,21 @@ pub struct BeaconBlockBody = FullPay pub eth1_data: Eth1Data, pub graffiti: Graffiti, pub proposer_slashings: VariableList, - pub attester_slashings: VariableList, E::MaxAttesterSlashings>, - pub attestations: VariableList, E::MaxAttestations>, + #[superstruct( + only(Base, Altair, Bellatrix, Capella, Deneb), + partial_getter(rename = "attester_slashings_base") + )] + pub attester_slashings: VariableList, E::MaxAttesterSlashings>, + #[superstruct(only(Electra), partial_getter(rename = "attester_slashings_electra"))] + pub attester_slashings: + VariableList, E::MaxAttesterSlashingsElectra>, + #[superstruct( + only(Base, Altair, Bellatrix, Capella, Deneb), + partial_getter(rename = "attestations_base") + )] + pub attestations: VariableList, E::MaxAttestations>, + #[superstruct(only(Electra), partial_getter(rename = "attestations_electra"))] + pub attestations: VariableList, E::MaxAttestationsElectra>, pub deposits: VariableList, pub voluntary_exits: VariableList, #[superstruct(only(Altair, Bellatrix, Capella, Deneb, Electra))] @@ -92,7 +114,10 @@ pub struct BeaconBlockBody = FullPay VariableList, #[superstruct(only(Deneb, Electra))] pub blob_kzg_commitments: KzgCommitments, + #[superstruct(only(Electra))] + pub consolidations: VariableList, #[superstruct(only(Base, Altair))] + #[metastruct(exclude_from(fields))] #[ssz(skip_serializing, skip_deserializing)] #[tree_hash(skip_hashing)] #[serde(skip)] @@ -117,138 +142,88 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, } } + fn body_merkle_leaves(&self) -> Vec { + let mut leaves = vec![]; + match self { + Self::Base(body) => { + beacon_block_body_base_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } + Self::Altair(body) => { + beacon_block_body_altair_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } + Self::Bellatrix(body) => { + beacon_block_body_bellatrix_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } + Self::Capella(body) => { + beacon_block_body_capella_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } + Self::Deneb(body) => { + beacon_block_body_deneb_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } + Self::Electra(body) => { + beacon_block_body_electra_fields!(body, |_, field| leaves + .push(field.tree_hash_root())); + } + } + leaves + } + /// Produces the proof of inclusion for a `KzgCommitment` in `self.blob_kzg_commitments` /// at `index`. pub fn kzg_commitment_merkle_proof( &self, index: usize, ) -> Result, Error> { - match self { - Self::Base(_) | Self::Altair(_) | Self::Bellatrix(_) | Self::Capella(_) => { - Err(Error::IncorrectStateVariant) - } - Self::Deneb(body) => { - // We compute the branches by generating 2 merkle trees: - // 1. Merkle tree for the `blob_kzg_commitments` List object - // 2. Merkle tree for the `BeaconBlockBody` container - // We then merge the branches for both the trees all the way up to the root. + // We compute the branches by generating 2 merkle trees: + // 1. Merkle tree for the `blob_kzg_commitments` List object + // 2. Merkle tree for the `BeaconBlockBody` container + // We then merge the branches for both the trees all the way up to the root. - // Part1 (Branches for the subtree rooted at `blob_kzg_commitments`) - // - // Branches for `blob_kzg_commitments` without length mix-in - let depth = E::max_blob_commitments_per_block() - .next_power_of_two() - .ilog2(); - let leaves: Vec<_> = body - .blob_kzg_commitments - .iter() - .map(|commitment| commitment.tree_hash_root()) - .collect(); - let tree = MerkleTree::create(&leaves, depth as usize); - let (_, mut proof) = tree - .generate_proof(index, depth as usize) - .map_err(Error::MerkleTreeError)?; + // Part1 (Branches for the subtree rooted at `blob_kzg_commitments`) + // + // Branches for `blob_kzg_commitments` without length mix-in + let blob_leaves = self + .blob_kzg_commitments()? + .iter() + .map(|commitment| commitment.tree_hash_root()) + .collect::>(); + let depth = E::max_blob_commitments_per_block() + .next_power_of_two() + .ilog2(); + let tree = MerkleTree::create(&blob_leaves, depth as usize); + let (_, mut proof) = tree + .generate_proof(index, depth as usize) + .map_err(Error::MerkleTreeError)?; - // Add the branch corresponding to the length mix-in. - let length = body.blob_kzg_commitments.len(); - let usize_len = std::mem::size_of::(); - let mut length_bytes = [0; BYTES_PER_CHUNK]; - length_bytes - .get_mut(0..usize_len) - .ok_or(Error::MerkleTreeError(MerkleTreeError::PleaseNotifyTheDevs))? - .copy_from_slice(&length.to_le_bytes()); - let length_root = Hash256::from_slice(length_bytes.as_slice()); - proof.push(length_root); + // Add the branch corresponding to the length mix-in. + let length = blob_leaves.len(); + let usize_len = std::mem::size_of::(); + let mut length_bytes = [0; BYTES_PER_CHUNK]; + length_bytes + .get_mut(0..usize_len) + .ok_or(Error::MerkleTreeError(MerkleTreeError::PleaseNotifyTheDevs))? + .copy_from_slice(&length.to_le_bytes()); + let length_root = Hash256::from_slice(length_bytes.as_slice()); + proof.push(length_root); - // Part 2 - // Branches for `BeaconBlockBody` container - let leaves = [ - body.randao_reveal.tree_hash_root(), - body.eth1_data.tree_hash_root(), - body.graffiti.tree_hash_root(), - body.proposer_slashings.tree_hash_root(), - body.attester_slashings.tree_hash_root(), - body.attestations.tree_hash_root(), - body.deposits.tree_hash_root(), - body.voluntary_exits.tree_hash_root(), - body.sync_aggregate.tree_hash_root(), - body.execution_payload.tree_hash_root(), - body.bls_to_execution_changes.tree_hash_root(), - body.blob_kzg_commitments.tree_hash_root(), - ]; - let beacon_block_body_depth = leaves.len().next_power_of_two().ilog2() as usize; - let tree = MerkleTree::create(&leaves, beacon_block_body_depth); - let (_, mut proof_body) = tree - .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) - .map_err(Error::MerkleTreeError)?; - // Join the proofs for the subtree and the main tree - proof.append(&mut proof_body); + // Part 2 + // Branches for `BeaconBlockBody` container + let body_leaves = self.body_merkle_leaves(); + let beacon_block_body_depth = body_leaves.len().next_power_of_two().ilog2() as usize; + let tree = MerkleTree::create(&body_leaves, beacon_block_body_depth); + let (_, mut proof_body) = tree + .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) + .map_err(Error::MerkleTreeError)?; + // Join the proofs for the subtree and the main tree + proof.append(&mut proof_body); + debug_assert_eq!(proof.len(), E::kzg_proof_inclusion_proof_depth()); - debug_assert_eq!(proof.len(), E::kzg_proof_inclusion_proof_depth()); - Ok(proof.into()) - } - // TODO(electra): De-duplicate proof computation. - Self::Electra(body) => { - // We compute the branches by generating 2 merkle trees: - // 1. Merkle tree for the `blob_kzg_commitments` List object - // 2. Merkle tree for the `BeaconBlockBody` container - // We then merge the branches for both the trees all the way up to the root. - - // Part1 (Branches for the subtree rooted at `blob_kzg_commitments`) - // - // Branches for `blob_kzg_commitments` without length mix-in - let depth = E::max_blob_commitments_per_block() - .next_power_of_two() - .ilog2(); - let leaves: Vec<_> = body - .blob_kzg_commitments - .iter() - .map(|commitment| commitment.tree_hash_root()) - .collect(); - let tree = MerkleTree::create(&leaves, depth as usize); - let (_, mut proof) = tree - .generate_proof(index, depth as usize) - .map_err(Error::MerkleTreeError)?; - - // Add the branch corresponding to the length mix-in. - let length = body.blob_kzg_commitments.len(); - let usize_len = std::mem::size_of::(); - let mut length_bytes = [0; BYTES_PER_CHUNK]; - length_bytes - .get_mut(0..usize_len) - .ok_or(Error::MerkleTreeError(MerkleTreeError::PleaseNotifyTheDevs))? - .copy_from_slice(&length.to_le_bytes()); - let length_root = Hash256::from_slice(length_bytes.as_slice()); - proof.push(length_root); - - // Part 2 - // Branches for `BeaconBlockBody` container - let leaves = [ - body.randao_reveal.tree_hash_root(), - body.eth1_data.tree_hash_root(), - body.graffiti.tree_hash_root(), - body.proposer_slashings.tree_hash_root(), - body.attester_slashings.tree_hash_root(), - body.attestations.tree_hash_root(), - body.deposits.tree_hash_root(), - body.voluntary_exits.tree_hash_root(), - body.sync_aggregate.tree_hash_root(), - body.execution_payload.tree_hash_root(), - body.bls_to_execution_changes.tree_hash_root(), - body.blob_kzg_commitments.tree_hash_root(), - ]; - let beacon_block_body_depth = leaves.len().next_power_of_two().ilog2() as usize; - let tree = MerkleTree::create(&leaves, beacon_block_body_depth); - let (_, mut proof_body) = tree - .generate_proof(BLOB_KZG_COMMITMENTS_INDEX, beacon_block_body_depth) - .map_err(Error::MerkleTreeError)?; - // Join the proofs for the subtree and the main tree - proof.append(&mut proof_body); - - debug_assert_eq!(proof.len(), E::kzg_proof_inclusion_proof_depth()); - Ok(proof.into()) - } - } + Ok(proof.into()) } /// Return `true` if this block body has a non-zero number of blobs. @@ -256,6 +231,99 @@ impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, self.blob_kzg_commitments() .map_or(false, |blobs| !blobs.is_empty()) } + + pub fn attestations_len(&self) -> usize { + match self { + Self::Base(body) => body.attestations.len(), + Self::Altair(body) => body.attestations.len(), + Self::Bellatrix(body) => body.attestations.len(), + Self::Capella(body) => body.attestations.len(), + Self::Deneb(body) => body.attestations.len(), + Self::Electra(body) => body.attestations.len(), + } + } + + pub fn attester_slashings_len(&self) -> usize { + match self { + Self::Base(body) => body.attester_slashings.len(), + Self::Altair(body) => body.attester_slashings.len(), + Self::Bellatrix(body) => body.attester_slashings.len(), + Self::Capella(body) => body.attester_slashings.len(), + Self::Deneb(body) => body.attester_slashings.len(), + Self::Electra(body) => body.attester_slashings.len(), + } + } + + pub fn attestations(&self) -> Box> + 'a> { + match self { + Self::Base(body) => Box::new(body.attestations.iter().map(AttestationRef::Base)), + Self::Altair(body) => Box::new(body.attestations.iter().map(AttestationRef::Base)), + Self::Bellatrix(body) => Box::new(body.attestations.iter().map(AttestationRef::Base)), + Self::Capella(body) => Box::new(body.attestations.iter().map(AttestationRef::Base)), + Self::Deneb(body) => Box::new(body.attestations.iter().map(AttestationRef::Base)), + Self::Electra(body) => Box::new(body.attestations.iter().map(AttestationRef::Electra)), + } + } + + pub fn attester_slashings(&self) -> Box> + 'a> { + match self { + Self::Base(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Base), + ), + Self::Altair(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Base), + ), + Self::Bellatrix(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Base), + ), + Self::Capella(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Base), + ), + Self::Deneb(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Base), + ), + Self::Electra(body) => Box::new( + body.attester_slashings + .iter() + .map(AttesterSlashingRef::Electra), + ), + } + } +} + +impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRefMut<'a, E, Payload> { + pub fn attestations_mut( + &'a mut self, + ) -> Box> + 'a> { + match self { + Self::Base(body) => Box::new(body.attestations.iter_mut().map(AttestationRefMut::Base)), + Self::Altair(body) => { + Box::new(body.attestations.iter_mut().map(AttestationRefMut::Base)) + } + Self::Bellatrix(body) => { + Box::new(body.attestations.iter_mut().map(AttestationRefMut::Base)) + } + Self::Capella(body) => { + Box::new(body.attestations.iter_mut().map(AttestationRefMut::Base)) + } + Self::Deneb(body) => { + Box::new(body.attestations.iter_mut().map(AttestationRefMut::Base)) + } + Self::Electra(body) => { + Box::new(body.attestations.iter_mut().map(AttestationRefMut::Electra)) + } + } + } } impl<'a, E: EthSpec, Payload: AbstractExecPayload> BeaconBlockBodyRef<'a, E, Payload> { @@ -556,6 +624,7 @@ impl From>> execution_payload: FullPayloadElectra { execution_payload }, bls_to_execution_changes, blob_kzg_commitments, + consolidations, } = body; ( @@ -574,6 +643,7 @@ impl From>> }, bls_to_execution_changes, blob_kzg_commitments: blob_kzg_commitments.clone(), + consolidations, }, Some(execution_payload), ) @@ -712,6 +782,7 @@ impl BeaconBlockBodyElectra> { execution_payload: FullPayloadElectra { execution_payload }, bls_to_execution_changes, blob_kzg_commitments, + consolidations, } = self; BeaconBlockBodyElectra { @@ -729,6 +800,7 @@ impl BeaconBlockBodyElectra> { }, bls_to_execution_changes: bls_to_execution_changes.clone(), blob_kzg_commitments: blob_kzg_commitments.clone(), + consolidations: consolidations.clone(), } } } @@ -748,6 +820,11 @@ impl From>> } impl BeaconBlockBody { + /// Returns the name of the fork pertaining to `self`. + pub fn fork_name(&self) -> ForkName { + self.to_ref().fork_name() + } + pub fn block_body_merkle_proof(&self, generalized_index: usize) -> Result, Error> { let field_index = match generalized_index { light_client_update::EXECUTION_PAYLOAD_INDEX => { @@ -762,13 +839,25 @@ impl BeaconBlockBody { _ => return Err(Error::IndexNotSupported(generalized_index)), }; + let attestations_root = if self.fork_name() > ForkName::Electra { + self.attestations_electra()?.tree_hash_root() + } else { + self.attestations_base()?.tree_hash_root() + }; + + let attester_slashings_root = if self.fork_name() > ForkName::Electra { + self.attester_slashings_electra()?.tree_hash_root() + } else { + self.attester_slashings_base()?.tree_hash_root() + }; + let mut leaves = vec![ self.randao_reveal().tree_hash_root(), self.eth1_data().tree_hash_root(), self.graffiti().tree_hash_root(), self.proposer_slashings().tree_hash_root(), - self.attester_slashings().tree_hash_root(), - self.attestations().tree_hash_root(), + attester_slashings_root, + attestations_root, self.deposits().tree_hash_root(), self.voluntary_exits().tree_hash_root(), ]; diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 577f282a55..bc20f3aa7b 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -159,6 +159,14 @@ pub enum Error { IndexNotSupported(usize), InvalidFlagIndex(usize), MerkleTreeError(merkle_proof::MerkleTreeError), + NoCommitteeFound(CommitteeIndex), + InvalidCommitteeIndex(CommitteeIndex), + InvalidSelectionProof { + aggregator_index: u64, + }, + AggregatorNotInCommittee { + aggregator_index: u64, + }, } /// Control whether an epoch-indexed field can be indexed at the next epoch or not. diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/beacon_state/committee_cache.rs index 209659ea88..161f854157 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -183,6 +183,8 @@ impl CommitteeCache { } /// Get all the Beacon committees at a given `slot`. + /// + /// Committees are sorted by ascending index order 0..committees_per_slot pub fn get_beacon_committees_at_slot(&self, slot: Slot) -> Result, Error> { if self.initialized_epoch.is_none() { return Err(Error::CommitteeCacheUninitialized(None)); diff --git a/consensus/types/src/chain_spec.rs b/consensus/types/src/chain_spec.rs index b0346a14ef..7609e36035 100644 --- a/consensus/types/src/chain_spec.rs +++ b/consensus/types/src/chain_spec.rs @@ -376,7 +376,7 @@ impl ChainSpec { state: &BeaconState, ) -> u64 { let fork_name = state.fork_name_unchecked(); - if fork_name >= ForkName::Electra { + if fork_name.electra_enabled() { self.min_slashing_penalty_quotient_electra } else if fork_name >= ForkName::Bellatrix { self.min_slashing_penalty_quotient_bellatrix diff --git a/consensus/types/src/eth_spec.rs b/consensus/types/src/eth_spec.rs index 38a048d469..1c379f5de4 100644 --- a/consensus/types/src/eth_spec.rs +++ b/consensus/types/src/eth_spec.rs @@ -63,6 +63,8 @@ pub trait EthSpec: * Misc */ type MaxValidatorsPerCommittee: Unsigned + Clone + Sync + Send + Debug + PartialEq + Eq; + type MaxValidatorsPerSlot: Unsigned + Clone + Sync + Send + Debug + PartialEq + Eq; + type MaxCommitteesPerSlot: Unsigned + Clone + Sync + Send + Debug + PartialEq + Eq; /* * Time parameters */ @@ -365,6 +367,8 @@ impl EthSpec for MainnetEthSpec { type JustificationBitsLength = U4; type SubnetBitfieldLength = U64; type MaxValidatorsPerCommittee = U2048; + type MaxCommitteesPerSlot = U64; + type MaxValidatorsPerSlot = U131072; type GenesisEpoch = U0; type SlotsPerEpoch = U32; type EpochsPerEth1VotingPeriod = U64; @@ -420,6 +424,8 @@ impl EthSpec for MainnetEthSpec { pub struct MinimalEthSpec; impl EthSpec for MinimalEthSpec { + type MaxCommitteesPerSlot = U4; + type MaxValidatorsPerSlot = U8192; type SlotsPerEpoch = U8; type EpochsPerEth1VotingPeriod = U4; type SlotsPerHistoricalRoot = U64; @@ -484,6 +490,8 @@ impl EthSpec for GnosisEthSpec { type JustificationBitsLength = U4; type SubnetBitfieldLength = U64; type MaxValidatorsPerCommittee = U2048; + type MaxCommitteesPerSlot = U64; + type MaxValidatorsPerSlot = U131072; type GenesisEpoch = U0; type SlotsPerEpoch = U16; type EpochsPerEth1VotingPeriod = U64; @@ -537,10 +545,12 @@ impl EthSpec for GnosisEthSpec { #[cfg(test)] mod test { use crate::{EthSpec, GnosisEthSpec, MainnetEthSpec, MinimalEthSpec}; + use ssz_types::typenum::Unsigned; fn assert_valid_spec() { E::kzg_commitments_tree_depth(); E::block_body_tree_depth(); + assert!(E::MaxValidatorsPerSlot::to_i32() >= E::MaxValidatorsPerCommittee::to_i32()); } #[test] diff --git a/consensus/types/src/execution_payload_header.rs b/consensus/types/src/execution_payload_header.rs index 98ed3d9f36..b8d0c8c2b0 100644 --- a/consensus/types/src/execution_payload_header.rs +++ b/consensus/types/src/execution_payload_header.rs @@ -77,16 +77,13 @@ pub struct ExecutionPayloadHeader { pub block_hash: ExecutionBlockHash, #[superstruct(getter(copy))] pub transactions_root: Hash256, - #[superstruct(only(Capella, Deneb, Electra))] - #[superstruct(getter(copy))] + #[superstruct(only(Capella, Deneb, Electra), partial_getter(copy))] pub withdrawals_root: Hash256, - #[superstruct(only(Deneb, Electra))] + #[superstruct(only(Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] - #[superstruct(getter(copy))] pub blob_gas_used: u64, - #[superstruct(only(Deneb, Electra))] + #[superstruct(only(Deneb, Electra), partial_getter(copy))] #[serde(with = "serde_utils::quoted_u64")] - #[superstruct(getter(copy))] pub excess_blob_gas: u64, #[superstruct(only(Electra), partial_getter(copy))] pub deposit_receipts_root: Hash256, @@ -121,12 +118,8 @@ impl ExecutionPayloadHeader { pub fn ssz_max_var_len_for_fork(fork_name: ForkName) -> usize { // Matching here in case variable fields are added in future forks. match fork_name { - ForkName::Base - | ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Base | ForkName::Altair => 0, + ForkName::Bellatrix | ForkName::Capella | ForkName::Deneb | ForkName::Electra => { // Max size of variable length `extra_data` field E::max_extra_data_bytes() * ::ssz_fixed_len() } diff --git a/consensus/types/src/fork_name.rs b/consensus/types/src/fork_name.rs index 5cc6621473..96f206d454 100644 --- a/consensus/types/src/fork_name.rs +++ b/consensus/types/src/fork_name.rs @@ -119,6 +119,14 @@ impl ForkName { ForkName::Electra => None, } } + + pub fn deneb_enabled(self) -> bool { + self >= ForkName::Deneb + } + + pub fn electra_enabled(self) -> bool { + self >= ForkName::Electra + } } /// Map a fork name into a fork-versioned superstruct type like `BeaconBlock`. diff --git a/consensus/types/src/indexed_attestation.rs b/consensus/types/src/indexed_attestation.rs index d80b49d55a..19d1501075 100644 --- a/consensus/types/src/indexed_attestation.rs +++ b/consensus/types/src/indexed_attestation.rs @@ -1,9 +1,11 @@ use crate::{test_utils::TestRandom, AggregateSignature, AttestationData, EthSpec, VariableList}; +use core::slice::Iter; use derivative::Derivative; use serde::{Deserialize, Serialize}; use ssz::Encode; use ssz_derive::{Decode, Encode}; use std::hash::{Hash, Hasher}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; @@ -12,25 +14,50 @@ use tree_hash_derive::TreeHash; /// To be included in an `AttesterSlashing`. /// /// Spec v0.12.1 +#[superstruct( + variants(Base, Electra), + variant_attributes( + derive( + Debug, + Clone, + Serialize, + Deserialize, + Decode, + Encode, + TestRandom, + Derivative, + arbitrary::Arbitrary, + TreeHash, + ), + derivative(PartialEq, Hash(bound = "E: EthSpec")), + serde(bound = "E: EthSpec", deny_unknown_fields), + arbitrary(bound = "E: EthSpec"), + ) +)] #[derive( - Derivative, Debug, Clone, Serialize, - Deserialize, - Encode, - Decode, TreeHash, - TestRandom, + Encode, + Derivative, + Deserialize, arbitrary::Arbitrary, + PartialEq, )] -#[derivative(PartialEq, Eq)] // to satisfy Clippy's lint about `Hash` -#[serde(bound = "E: EthSpec")] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] #[arbitrary(bound = "E: EthSpec")] pub struct IndexedAttestation { /// Lists validator registry indices, not committee indices. + #[superstruct(only(Base), partial_getter(rename = "attesting_indices_base"))] #[serde(with = "quoted_variable_list_u64")] pub attesting_indices: VariableList, + #[superstruct(only(Electra), partial_getter(rename = "attesting_indices_electra"))] + #[serde(with = "quoted_variable_list_u64")] + pub attesting_indices: VariableList, pub data: AttestationData, pub signature: AggregateSignature, } @@ -40,15 +67,123 @@ impl IndexedAttestation { /// /// Spec v0.12.1 pub fn is_double_vote(&self, other: &Self) -> bool { - self.data.target.epoch == other.data.target.epoch && self.data != other.data + // reuse the ref implementation to ensure logic is the same + self.to_ref().is_double_vote(other.to_ref()) } /// Check if ``attestation_data_1`` surrounds ``attestation_data_2``. /// /// Spec v0.12.1 pub fn is_surround_vote(&self, other: &Self) -> bool { - self.data.source.epoch < other.data.source.epoch - && other.data.target.epoch < self.data.target.epoch + // reuse the ref implementation to ensure logic is the same + self.to_ref().is_surround_vote(other.to_ref()) + } + + pub fn attesting_indices_len(&self) -> usize { + match self { + IndexedAttestation::Base(att) => att.attesting_indices.len(), + IndexedAttestation::Electra(att) => att.attesting_indices.len(), + } + } + + pub fn attesting_indices_to_vec(&self) -> Vec { + match self { + IndexedAttestation::Base(att) => att.attesting_indices.to_vec(), + IndexedAttestation::Electra(att) => att.attesting_indices.to_vec(), + } + } + + pub fn attesting_indices_is_empty(&self) -> bool { + match self { + IndexedAttestation::Base(att) => att.attesting_indices.is_empty(), + IndexedAttestation::Electra(att) => att.attesting_indices.is_empty(), + } + } + + pub fn attesting_indices_iter(&self) -> Iter<'_, u64> { + match self { + IndexedAttestation::Base(att) => att.attesting_indices.iter(), + IndexedAttestation::Electra(att) => att.attesting_indices.iter(), + } + } + + pub fn attesting_indices_first(&self) -> Option<&u64> { + match self { + IndexedAttestation::Base(att) => att.attesting_indices.first(), + IndexedAttestation::Electra(att) => att.attesting_indices.first(), + } + } + + pub fn to_electra(self) -> IndexedAttestationElectra { + match self { + Self::Base(att) => { + let extended_attesting_indices: VariableList = + VariableList::new(att.attesting_indices.to_vec()) + .expect("MaxValidatorsPerSlot must be >= MaxValidatorsPerCommittee"); + // Note a unit test in consensus/types/src/eth_spec.rs asserts this invariant for + // all known specs + + IndexedAttestationElectra { + attesting_indices: extended_attesting_indices, + data: att.data, + signature: att.signature, + } + } + Self::Electra(att) => att, + } + } +} + +impl<'a, E: EthSpec> IndexedAttestationRef<'a, E> { + pub fn is_double_vote(&self, other: Self) -> bool { + self.data().target.epoch == other.data().target.epoch && self.data() != other.data() + } + + pub fn is_surround_vote(&self, other: Self) -> bool { + self.data().source.epoch < other.data().source.epoch + && other.data().target.epoch < self.data().target.epoch + } + + pub fn attesting_indices_len(&self) -> usize { + match self { + IndexedAttestationRef::Base(att) => att.attesting_indices.len(), + IndexedAttestationRef::Electra(att) => att.attesting_indices.len(), + } + } + + pub fn attesting_indices_to_vec(&self) -> Vec { + match self { + IndexedAttestationRef::Base(att) => att.attesting_indices.to_vec(), + IndexedAttestationRef::Electra(att) => att.attesting_indices.to_vec(), + } + } + + pub fn attesting_indices_is_empty(&self) -> bool { + match self { + IndexedAttestationRef::Base(att) => att.attesting_indices.is_empty(), + IndexedAttestationRef::Electra(att) => att.attesting_indices.is_empty(), + } + } + + pub fn attesting_indices_iter(&self) -> Iter<'_, u64> { + match self { + IndexedAttestationRef::Base(att) => att.attesting_indices.iter(), + IndexedAttestationRef::Electra(att) => att.attesting_indices.iter(), + } + } + + pub fn attesting_indices_first(&self) -> Option<&u64> { + match self { + IndexedAttestationRef::Base(att) => att.attesting_indices.first(), + IndexedAttestationRef::Electra(att) => att.attesting_indices.first(), + } + } + + pub fn clone_as_indexed_attestation(self) -> IndexedAttestation { + match self { + IndexedAttestationRef::Base(att) => IndexedAttestation::Base(att.clone()), + IndexedAttestationRef::Electra(att) => IndexedAttestation::Electra(att.clone()), + } } } @@ -59,9 +194,12 @@ impl IndexedAttestation { /// Used in the operation pool. impl Hash for IndexedAttestation { fn hash(&self, state: &mut H) { - self.attesting_indices.hash(state); - self.data.hash(state); - self.signature.as_ssz_bytes().hash(state); + match self { + IndexedAttestation::Base(att) => att.attesting_indices.hash(state), + IndexedAttestation::Electra(att) => att.attesting_indices.hash(state), + }; + self.data().hash(state); + self.signature().as_ssz_bytes().hash(state); } } @@ -157,17 +295,25 @@ mod tests { assert!(!indexed_vote_first.is_surround_vote(&indexed_vote_second)); } - ssz_and_tree_hash_tests!(IndexedAttestation); + mod base { + use super::*; + ssz_and_tree_hash_tests!(IndexedAttestationBase); + } + mod electra { + use super::*; + ssz_and_tree_hash_tests!(IndexedAttestationElectra); + } fn create_indexed_attestation( target_epoch: u64, source_epoch: u64, ) -> IndexedAttestation { let mut rng = XorShiftRng::from_seed([42; 16]); - let mut indexed_vote = IndexedAttestation::random_for_test(&mut rng); + let mut indexed_vote = + IndexedAttestation::Base(IndexedAttestationBase::random_for_test(&mut rng)); - indexed_vote.data.source.epoch = Epoch::new(source_epoch); - indexed_vote.data.target.epoch = Epoch::new(target_epoch); + indexed_vote.data_mut().source.epoch = Epoch::new(source_epoch); + indexed_vote.data_mut().target.epoch = Epoch::new(target_epoch); indexed_vote } } diff --git a/consensus/types/src/lib.rs b/consensus/types/src/lib.rs index db429095d8..6a98d7ade9 100644 --- a/consensus/types/src/lib.rs +++ b/consensus/types/src/lib.rs @@ -112,11 +112,19 @@ pub mod runtime_var_list; use ethereum_types::{H160, H256}; pub use crate::activation_queue::ActivationQueue; -pub use crate::aggregate_and_proof::AggregateAndProof; -pub use crate::attestation::{Attestation, Error as AttestationError}; +pub use crate::aggregate_and_proof::{ + AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, AggregateAndProofRef, +}; +pub use crate::attestation::{ + Attestation, AttestationBase, AttestationElectra, AttestationRef, AttestationRefMut, + Error as AttestationError, +}; pub use crate::attestation_data::AttestationData; pub use crate::attestation_duty::AttestationDuty; -pub use crate::attester_slashing::AttesterSlashing; +pub use crate::attester_slashing::{ + AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra, AttesterSlashingOnDisk, + AttesterSlashingRef, AttesterSlashingRefOnDisk, +}; pub use crate::beacon_block::{ BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BeaconBlockBellatrix, BeaconBlockCapella, BeaconBlockDeneb, BeaconBlockElectra, BeaconBlockRef, BeaconBlockRefMut, BlindedBeaconBlock, @@ -167,25 +175,29 @@ pub use crate::fork_name::{ForkName, InconsistentFork}; pub use crate::fork_versioned_response::{ForkVersionDeserialize, ForkVersionedResponse}; pub use crate::graffiti::{Graffiti, GRAFFITI_BYTES_LEN}; pub use crate::historical_batch::HistoricalBatch; -pub use crate::indexed_attestation::IndexedAttestation; +pub use crate::indexed_attestation::{ + IndexedAttestation, IndexedAttestationBase, IndexedAttestationElectra, IndexedAttestationRef, +}; pub use crate::light_client_bootstrap::{ LightClientBootstrap, LightClientBootstrapAltair, LightClientBootstrapCapella, - LightClientBootstrapDeneb, + LightClientBootstrapDeneb, LightClientBootstrapElectra, }; pub use crate::light_client_finality_update::{ LightClientFinalityUpdate, LightClientFinalityUpdateAltair, LightClientFinalityUpdateCapella, - LightClientFinalityUpdateDeneb, + LightClientFinalityUpdateDeneb, LightClientFinalityUpdateElectra, }; pub use crate::light_client_header::{ LightClientHeader, LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, + LightClientHeaderElectra, }; pub use crate::light_client_optimistic_update::{ LightClientOptimisticUpdate, LightClientOptimisticUpdateAltair, LightClientOptimisticUpdateCapella, LightClientOptimisticUpdateDeneb, + LightClientOptimisticUpdateElectra, }; pub use crate::light_client_update::{ Error as LightClientError, LightClientUpdate, LightClientUpdateAltair, - LightClientUpdateCapella, LightClientUpdateDeneb, + LightClientUpdateCapella, LightClientUpdateDeneb, LightClientUpdateElectra, }; pub use crate::participation_flags::ParticipationFlags; pub use crate::participation_list::ParticipationList; @@ -208,7 +220,9 @@ pub use crate::relative_epoch::{Error as RelativeEpochError, RelativeEpoch}; pub use crate::runtime_var_list::RuntimeVariableList; pub use crate::selection_proof::SelectionProof; pub use crate::shuffling_id::AttestationShufflingId; -pub use crate::signed_aggregate_and_proof::SignedAggregateAndProof; +pub use crate::signed_aggregate_and_proof::{ + SignedAggregateAndProof, SignedAggregateAndProofBase, SignedAggregateAndProofElectra, +}; pub use crate::signed_beacon_block::{ ssz_tagged_signed_beacon_block, ssz_tagged_signed_beacon_block_arc, SignedBeaconBlock, SignedBeaconBlockAltair, SignedBeaconBlockBase, SignedBeaconBlockBellatrix, diff --git a/consensus/types/src/light_client_bootstrap.rs b/consensus/types/src/light_client_bootstrap.rs index 61da0e1b11..e3a85744de 100644 --- a/consensus/types/src/light_client_bootstrap.rs +++ b/consensus/types/src/light_client_bootstrap.rs @@ -1,7 +1,8 @@ use crate::{ light_client_update::*, test_utils::TestRandom, BeaconState, ChainSpec, EthSpec, FixedVector, ForkName, ForkVersionDeserialize, Hash256, LightClientHeader, LightClientHeaderAltair, - LightClientHeaderCapella, LightClientHeaderDeneb, SignedBeaconBlock, Slot, SyncCommittee, + LightClientHeaderCapella, LightClientHeaderDeneb, LightClientHeaderElectra, SignedBeaconBlock, + Slot, SyncCommittee, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -16,7 +17,7 @@ use tree_hash_derive::TreeHash; /// A LightClientBootstrap is the initializer we send over to light_client nodes /// that are trying to generate their basic storage when booting up. #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -51,6 +52,8 @@ pub struct LightClientBootstrap { pub header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "header_deneb"))] pub header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "header_electra"))] + pub header: LightClientHeaderElectra, /// The `SyncCommittee` used in the requested period. pub current_sync_committee: Arc>, /// Merkle proof for sync committee @@ -66,6 +69,7 @@ impl LightClientBootstrap { Self::Altair(_) => func(ForkName::Altair), Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), + Self::Electra(_) => func(ForkName::Electra), } } @@ -82,9 +86,8 @@ impl LightClientBootstrap { Self::Altair(LightClientBootstrapAltair::from_ssz_bytes(bytes)?) } ForkName::Capella => Self::Capella(LightClientBootstrapCapella::from_ssz_bytes(bytes)?), - ForkName::Deneb | ForkName::Electra => { - Self::Deneb(LightClientBootstrapDeneb::from_ssz_bytes(bytes)?) - } + ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => Self::Electra(LightClientBootstrapElectra::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientBootstrap decoding for {fork_name} not implemented" @@ -97,18 +100,16 @@ impl LightClientBootstrap { #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { - // TODO(electra): review electra changes - match fork_name { + let fixed_len = match fork_name { ForkName::Base => 0, - ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Altair | ForkName::Bellatrix => { as Encode>::ssz_fixed_len() - + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } - } + ForkName::Capella => as Encode>::ssz_fixed_len(), + ForkName::Deneb => as Encode>::ssz_fixed_len(), + ForkName::Electra => as Encode>::ssz_fixed_len(), + }; + fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } pub fn from_beacon_state( @@ -138,11 +139,16 @@ impl LightClientBootstrap { current_sync_committee, current_sync_committee_branch, }), - ForkName::Deneb | ForkName::Electra => Self::Deneb(LightClientBootstrapDeneb { + ForkName::Deneb => Self::Deneb(LightClientBootstrapDeneb { header: LightClientHeaderDeneb::block_to_light_client_header(block)?, current_sync_committee, current_sync_committee_branch, }), + ForkName::Electra => Self::Electra(LightClientBootstrapElectra { + header: LightClientHeaderElectra::block_to_light_client_header(block)?, + current_sync_committee, + current_sync_committee_branch, + }), }; Ok(light_client_bootstrap) diff --git a/consensus/types/src/light_client_finality_update.rs b/consensus/types/src/light_client_finality_update.rs index 29c526e291..a9e24e03db 100644 --- a/consensus/types/src/light_client_finality_update.rs +++ b/consensus/types/src/light_client_finality_update.rs @@ -2,7 +2,8 @@ use super::{EthSpec, FixedVector, Hash256, LightClientHeader, Slot, SyncAggregat use crate::ChainSpec; use crate::{ light_client_update::*, test_utils::TestRandom, ForkName, ForkVersionDeserialize, - LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, SignedBeaconBlock, + LightClientHeaderAltair, LightClientHeaderCapella, LightClientHeaderDeneb, + LightClientHeaderElectra, SignedBeaconBlock, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -15,7 +16,7 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -50,6 +51,8 @@ pub struct LightClientFinalityUpdate { pub attested_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] pub attested_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] + pub attested_header: LightClientHeaderElectra, /// The last `BeaconBlockHeader` from the last attested finalized block (end of epoch). #[superstruct(only(Altair), partial_getter(rename = "finalized_header_altair"))] pub finalized_header: LightClientHeaderAltair, @@ -57,6 +60,8 @@ pub struct LightClientFinalityUpdate { pub finalized_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "finalized_header_deneb"))] pub finalized_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "finalized_header_electra"))] + pub finalized_header: LightClientHeaderElectra, /// Merkle proof attesting finalized header. #[test_random(default)] pub finality_branch: FixedVector, @@ -80,7 +85,7 @@ impl LightClientFinalityUpdate { .map_err(|_| Error::InconsistentFork)? { ForkName::Altair | ForkName::Bellatrix => { - let finality_update = LightClientFinalityUpdateAltair { + Self::Altair(LightClientFinalityUpdateAltair { attested_header: LightClientHeaderAltair::block_to_light_client_header( attested_block, )?, @@ -90,37 +95,42 @@ impl LightClientFinalityUpdate { finality_branch, sync_aggregate, signature_slot, - }; - Self::Altair(finality_update) - } - ForkName::Capella => { - let finality_update = LightClientFinalityUpdateCapella { - attested_header: LightClientHeaderCapella::block_to_light_client_header( - attested_block, - )?, - finalized_header: LightClientHeaderCapella::block_to_light_client_header( - finalized_block, - )?, - finality_branch, - sync_aggregate, - signature_slot, - }; - Self::Capella(finality_update) - } - ForkName::Deneb | ForkName::Electra => { - let finality_update = LightClientFinalityUpdateDeneb { - attested_header: LightClientHeaderDeneb::block_to_light_client_header( - attested_block, - )?, - finalized_header: LightClientHeaderDeneb::block_to_light_client_header( - finalized_block, - )?, - finality_branch, - sync_aggregate, - signature_slot, - }; - Self::Deneb(finality_update) + }) } + ForkName::Capella => Self::Capella(LightClientFinalityUpdateCapella { + attested_header: LightClientHeaderCapella::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderCapella::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }), + ForkName::Deneb => Self::Deneb(LightClientFinalityUpdateDeneb { + attested_header: LightClientHeaderDeneb::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderDeneb::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }), + ForkName::Electra => Self::Electra(LightClientFinalityUpdateElectra { + attested_header: LightClientHeaderElectra::block_to_light_client_header( + attested_block, + )?, + finalized_header: LightClientHeaderElectra::block_to_light_client_header( + finalized_block, + )?, + finality_branch, + sync_aggregate, + signature_slot, + }), + ForkName::Base => return Err(Error::AltairForkNotActive), }; @@ -135,6 +145,7 @@ impl LightClientFinalityUpdate { Self::Altair(_) => func(ForkName::Altair), Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), + Self::Electra(_) => func(ForkName::Electra), } } @@ -153,8 +164,9 @@ impl LightClientFinalityUpdate { ForkName::Capella => { Self::Capella(LightClientFinalityUpdateCapella::from_ssz_bytes(bytes)?) } - ForkName::Deneb | ForkName::Electra => { - Self::Deneb(LightClientFinalityUpdateDeneb::from_ssz_bytes(bytes)?) + ForkName::Deneb => Self::Deneb(LightClientFinalityUpdateDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => { + Self::Electra(LightClientFinalityUpdateElectra::from_ssz_bytes(bytes)?) } ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( @@ -168,18 +180,17 @@ impl LightClientFinalityUpdate { #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { - // TODO(electra): review electra changes - match fork_name { + let fixed_size = match fork_name { ForkName::Base => 0, - ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Altair | ForkName::Bellatrix => { as Encode>::ssz_fixed_len() - + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } - } + ForkName::Capella => as Encode>::ssz_fixed_len(), + ForkName::Deneb => as Encode>::ssz_fixed_len(), + ForkName::Electra => as Encode>::ssz_fixed_len(), + }; + // `2 *` because there are two headers in the update + fixed_size + 2 * LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } } diff --git a/consensus/types/src/light_client_header.rs b/consensus/types/src/light_client_header.rs index 213ec90f95..1d6432ed6f 100644 --- a/consensus/types/src/light_client_header.rs +++ b/consensus/types/src/light_client_header.rs @@ -4,7 +4,7 @@ use crate::ForkVersionDeserialize; use crate::{light_client_update::*, BeaconBlockBody}; use crate::{ test_utils::TestRandom, EthSpec, ExecutionPayloadHeaderCapella, ExecutionPayloadHeaderDeneb, - FixedVector, Hash256, SignedBeaconBlock, + ExecutionPayloadHeaderElectra, FixedVector, Hash256, SignedBeaconBlock, }; use crate::{BeaconBlockHeader, ExecutionPayloadHeader}; use derivative::Derivative; @@ -17,7 +17,7 @@ use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -54,8 +54,13 @@ pub struct LightClientHeader { pub execution: ExecutionPayloadHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_header_deneb"))] pub execution: ExecutionPayloadHeaderDeneb, + #[superstruct( + only(Electra), + partial_getter(rename = "execution_payload_header_electra") + )] + pub execution: ExecutionPayloadHeaderElectra, - #[superstruct(only(Capella, Deneb))] + #[superstruct(only(Capella, Deneb, Electra))] pub execution_branch: FixedVector, #[ssz(skip_serializing, skip_deserializing)] @@ -81,9 +86,12 @@ impl LightClientHeader { ForkName::Capella => LightClientHeader::Capella( LightClientHeaderCapella::block_to_light_client_header(block)?, ), - ForkName::Deneb | ForkName::Electra => LightClientHeader::Deneb( + ForkName::Deneb => LightClientHeader::Deneb( LightClientHeaderDeneb::block_to_light_client_header(block)?, ), + ForkName::Electra => LightClientHeader::Electra( + LightClientHeaderElectra::block_to_light_client_header(block)?, + ), }; Ok(header) } @@ -96,9 +104,12 @@ impl LightClientHeader { ForkName::Capella => { LightClientHeader::Capella(LightClientHeaderCapella::from_ssz_bytes(bytes)?) } - ForkName::Deneb | ForkName::Electra => { + ForkName::Deneb => { LightClientHeader::Deneb(LightClientHeaderDeneb::from_ssz_bytes(bytes)?) } + ForkName::Electra => { + LightClientHeader::Electra(LightClientHeaderElectra::from_ssz_bytes(bytes)?) + } ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientHeader decoding for {fork_name} not implemented" @@ -192,6 +203,34 @@ impl LightClientHeaderDeneb { } } +impl LightClientHeaderElectra { + pub fn block_to_light_client_header(block: &SignedBeaconBlock) -> Result { + let payload = block + .message() + .execution_payload()? + .execution_payload_electra()?; + + let header = ExecutionPayloadHeaderElectra::from(payload); + let beacon_block_body = BeaconBlockBody::from( + block + .message() + .body_electra() + .map_err(|_| Error::BeaconBlockBodyError)? + .to_owned(), + ); + + let execution_branch = + beacon_block_body.block_body_merkle_proof(EXECUTION_PAYLOAD_INDEX)?; + + Ok(LightClientHeaderElectra { + beacon: block.message().block_header(), + execution: header, + execution_branch: FixedVector::new(execution_branch)?, + _phantom_data: PhantomData, + }) + } +} + impl ForkVersionDeserialize for LightClientHeader { fn deserialize_by_fork<'de, D: serde::Deserializer<'de>>( value: serde_json::value::Value, @@ -204,9 +243,12 @@ impl ForkVersionDeserialize for LightClientHeader { ForkName::Capella => serde_json::from_value(value) .map(|light_client_header| Self::Capella(light_client_header)) .map_err(serde::de::Error::custom), - ForkName::Deneb | ForkName::Electra => serde_json::from_value(value) + ForkName::Deneb => serde_json::from_value(value) .map(|light_client_header| Self::Deneb(light_client_header)) .map_err(serde::de::Error::custom), + ForkName::Electra => serde_json::from_value(value) + .map(|light_client_header| Self::Electra(light_client_header)) + .map_err(serde::de::Error::custom), ForkName::Base => Err(serde::de::Error::custom(format!( "LightClientHeader deserialization for {fork_name} not implemented" ))), diff --git a/consensus/types/src/light_client_optimistic_update.rs b/consensus/types/src/light_client_optimistic_update.rs index 4727673f6c..708f24e770 100644 --- a/consensus/types/src/light_client_optimistic_update.rs +++ b/consensus/types/src/light_client_optimistic_update.rs @@ -2,7 +2,7 @@ use super::{EthSpec, ForkName, ForkVersionDeserialize, LightClientHeader, Slot, use crate::test_utils::TestRandom; use crate::{ light_client_update::*, ChainSpec, LightClientHeaderAltair, LightClientHeaderCapella, - LightClientHeaderDeneb, SignedBeaconBlock, + LightClientHeaderDeneb, LightClientHeaderElectra, SignedBeaconBlock, }; use derivative::Derivative; use serde::{Deserialize, Deserializer, Serialize}; @@ -18,7 +18,7 @@ use tree_hash_derive::TreeHash; /// A LightClientOptimisticUpdate is the update we send on each slot, /// it is based off the current unfinalized epoch is verified only against BLS signature. #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -53,6 +53,8 @@ pub struct LightClientOptimisticUpdate { pub attested_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] pub attested_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] + pub attested_header: LightClientHeaderElectra, /// current sync aggregate pub sync_aggregate: SyncAggregate, /// Slot of the sync aggregated signature @@ -86,13 +88,20 @@ impl LightClientOptimisticUpdate { sync_aggregate, signature_slot, }), - ForkName::Deneb | ForkName::Electra => Self::Deneb(LightClientOptimisticUpdateDeneb { + ForkName::Deneb => Self::Deneb(LightClientOptimisticUpdateDeneb { attested_header: LightClientHeaderDeneb::block_to_light_client_header( attested_block, )?, sync_aggregate, signature_slot, }), + ForkName::Electra => Self::Electra(LightClientOptimisticUpdateElectra { + attested_header: LightClientHeaderElectra::block_to_light_client_header( + attested_block, + )?, + sync_aggregate, + signature_slot, + }), ForkName::Base => return Err(Error::AltairForkNotActive), }; @@ -107,6 +116,7 @@ impl LightClientOptimisticUpdate { Self::Altair(_) => func(ForkName::Altair), Self::Capella(_) => func(ForkName::Capella), Self::Deneb(_) => func(ForkName::Deneb), + Self::Electra(_) => func(ForkName::Electra), } } @@ -139,9 +149,12 @@ impl LightClientOptimisticUpdate { ForkName::Capella => { Self::Capella(LightClientOptimisticUpdateCapella::from_ssz_bytes(bytes)?) } - ForkName::Deneb | ForkName::Electra => { + ForkName::Deneb => { Self::Deneb(LightClientOptimisticUpdateDeneb::from_ssz_bytes(bytes)?) } + ForkName::Electra => { + Self::Electra(LightClientOptimisticUpdateElectra::from_ssz_bytes(bytes)?) + } ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientOptimisticUpdate decoding for {fork_name} not implemented" @@ -154,18 +167,16 @@ impl LightClientOptimisticUpdate { #[allow(clippy::arithmetic_side_effects)] pub fn ssz_max_len_for_fork(fork_name: ForkName) -> usize { - // TODO(electra): review electra changes - match fork_name { + let fixed_len = match fork_name { ForkName::Base => 0, - ForkName::Altair - | ForkName::Bellatrix - | ForkName::Capella - | ForkName::Deneb - | ForkName::Electra => { + ForkName::Altair | ForkName::Bellatrix => { as Encode>::ssz_fixed_len() - + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } - } + ForkName::Capella => as Encode>::ssz_fixed_len(), + ForkName::Deneb => as Encode>::ssz_fixed_len(), + ForkName::Electra => as Encode>::ssz_fixed_len(), + }; + fixed_len + LightClientHeader::::ssz_max_var_len_for_fork(fork_name) } } diff --git a/consensus/types/src/light_client_update.rs b/consensus/types/src/light_client_update.rs index 002fbea2d3..76ad568988 100644 --- a/consensus/types/src/light_client_update.rs +++ b/consensus/types/src/light_client_update.rs @@ -1,4 +1,5 @@ use super::{EthSpec, FixedVector, Hash256, Slot, SyncAggregate, SyncCommittee}; +use crate::light_client_header::LightClientHeaderElectra; use crate::{ beacon_state, test_utils::TestRandom, BeaconBlock, BeaconBlockHeader, BeaconState, ChainSpec, ForkName, ForkVersionDeserialize, LightClientHeaderAltair, LightClientHeaderCapella, @@ -76,7 +77,7 @@ impl From for Error { /// or to sync up to the last committee period, we need to have one ready for each ALTAIR period /// we go over, note: there is no need to keep all of the updates from [ALTAIR_PERIOD, CURRENT_PERIOD]. #[superstruct( - variants(Altair, Capella, Deneb), + variants(Altair, Capella, Deneb, Electra), variant_attributes( derive( Debug, @@ -111,6 +112,8 @@ pub struct LightClientUpdate { pub attested_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "attested_header_deneb"))] pub attested_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "attested_header_electra"))] + pub attested_header: LightClientHeaderElectra, /// The `SyncCommittee` used in the next period. pub next_sync_committee: Arc>, /// Merkle proof for next sync committee @@ -122,6 +125,8 @@ pub struct LightClientUpdate { pub finalized_header: LightClientHeaderCapella, #[superstruct(only(Deneb), partial_getter(rename = "finalized_header_deneb"))] pub finalized_header: LightClientHeaderDeneb, + #[superstruct(only(Electra), partial_getter(rename = "finalized_header_electra"))] + pub finalized_header: LightClientHeaderElectra, /// Merkle proof attesting finalized header. pub finality_branch: FixedVector, /// current sync aggreggate @@ -221,7 +226,7 @@ impl LightClientUpdate { signature_slot: block.slot(), }) } - ForkName::Deneb | ForkName::Electra => { + ForkName::Deneb => { let attested_header = LightClientHeaderDeneb::block_to_light_client_header(attested_block)?; let finalized_header = @@ -236,6 +241,23 @@ impl LightClientUpdate { signature_slot: block.slot(), }) } + ForkName::Electra => { + let attested_header = + LightClientHeaderElectra::block_to_light_client_header(attested_block)?; + let finalized_header = + LightClientHeaderElectra::block_to_light_client_header(finalized_block)?; + Self::Electra(LightClientUpdateElectra { + attested_header, + next_sync_committee: attested_state.next_sync_committee()?.clone(), + next_sync_committee_branch: FixedVector::new(next_sync_committee_branch)?, + finalized_header, + finality_branch: FixedVector::new(finality_branch)?, + sync_aggregate: sync_aggregate.clone(), + signature_slot: block.slot(), + }) + } // To add a new fork, just append the new fork variant on the latest fork. Forks that + // have a distinct execution header will need a new LightClientUdpate variant only + // if you need to test or support lightclient usages }; Ok(light_client_update) @@ -247,9 +269,8 @@ impl LightClientUpdate { Self::Altair(LightClientUpdateAltair::from_ssz_bytes(bytes)?) } ForkName::Capella => Self::Capella(LightClientUpdateCapella::from_ssz_bytes(bytes)?), - ForkName::Deneb | ForkName::Electra => { - Self::Deneb(LightClientUpdateDeneb::from_ssz_bytes(bytes)?) - } + ForkName::Deneb => Self::Deneb(LightClientUpdateDeneb::from_ssz_bytes(bytes)?), + ForkName::Electra => Self::Electra(LightClientUpdateElectra::from_ssz_bytes(bytes)?), ForkName::Base => { return Err(ssz::DecodeError::BytesInvalid(format!( "LightClientUpdate decoding for {fork_name} not implemented" diff --git a/consensus/types/src/signed_aggregate_and_proof.rs b/consensus/types/src/signed_aggregate_and_proof.rs index c31c50ea17..26eca19bf1 100644 --- a/consensus/types/src/signed_aggregate_and_proof.rs +++ b/consensus/types/src/signed_aggregate_and_proof.rs @@ -1,10 +1,15 @@ use super::{ - AggregateAndProof, Attestation, ChainSpec, Domain, EthSpec, Fork, Hash256, SecretKey, - SelectionProof, Signature, SignedRoot, + AggregateAndProof, AggregateAndProofBase, AggregateAndProofElectra, AggregateAndProofRef, +}; +use super::{ + AttestationRef, ChainSpec, Domain, EthSpec, Fork, Hash256, SecretKey, SelectionProof, + Signature, SignedRoot, }; use crate::test_utils::TestRandom; +use crate::Attestation; use serde::{Deserialize, Serialize}; use ssz_derive::{Decode, Encode}; +use superstruct::superstruct; use test_random_derive::TestRandom; use tree_hash_derive::TreeHash; @@ -12,22 +17,38 @@ use tree_hash_derive::TreeHash; /// gossipsub topic. /// /// Spec v0.12.1 -#[derive( - Debug, - Clone, - PartialEq, - Serialize, - Deserialize, - Encode, - Decode, - TestRandom, - TreeHash, - arbitrary::Arbitrary, +#[superstruct( + variants(Base, Electra), + variant_attributes( + derive( + arbitrary::Arbitrary, + Debug, + Clone, + PartialEq, + Serialize, + Deserialize, + Encode, + Decode, + TestRandom, + TreeHash, + ), + serde(bound = "E: EthSpec"), + arbitrary(bound = "E: EthSpec"), + ), + map_into(Attestation), + map_ref_into(AggregateAndProofRef) )] -#[serde(bound = "E: EthSpec")] +#[derive( + arbitrary::Arbitrary, Debug, Clone, PartialEq, Serialize, Deserialize, Encode, TreeHash, +)] +#[serde(untagged)] +#[tree_hash(enum_behaviour = "transparent")] +#[ssz(enum_behaviour = "transparent")] +#[serde(bound = "E: EthSpec", deny_unknown_fields)] #[arbitrary(bound = "E: EthSpec")] pub struct SignedAggregateAndProof { /// The `AggregateAndProof` that was signed. + #[superstruct(flatten)] pub message: AggregateAndProof, /// The aggregate attestation. pub signature: Signature, @@ -40,7 +61,7 @@ impl SignedAggregateAndProof { /// If `selection_proof.is_none()` it will be computed locally. pub fn from_aggregate( aggregator_index: u64, - aggregate: Attestation, + aggregate: AttestationRef<'_, E>, selection_proof: Option, secret_key: &SecretKey, fork: &Fork, @@ -56,8 +77,7 @@ impl SignedAggregateAndProof { genesis_validators_root, spec, ); - - let target_epoch = message.aggregate.data.slot.epoch(E::slots_per_epoch()); + let target_epoch = message.aggregate().data().slot.epoch(E::slots_per_epoch()); let domain = spec.get_domain( target_epoch, Domain::AggregateAndProof, @@ -66,9 +86,35 @@ impl SignedAggregateAndProof { ); let signing_message = message.signing_root(domain); - SignedAggregateAndProof { - message, - signature: secret_key.sign(signing_message), + Self::from_aggregate_and_proof(message, secret_key.sign(signing_message)) + } + + /// Produces a new `SignedAggregateAndProof` given a `signature` of `aggregate` + pub fn from_aggregate_and_proof(aggregate: AggregateAndProof, signature: Signature) -> Self { + match aggregate { + AggregateAndProof::Base(message) => { + SignedAggregateAndProof::Base(SignedAggregateAndProofBase { message, signature }) + } + AggregateAndProof::Electra(message) => { + SignedAggregateAndProof::Electra(SignedAggregateAndProofElectra { + message, + signature, + }) + } } } + + pub fn message<'a>(&'a self) -> AggregateAndProofRef<'a, E> { + map_signed_aggregate_and_proof_ref_into_aggregate_and_proof_ref!( + &'a _, + self.to_ref(), + |inner, cons| { cons(&inner.message) } + ) + } + + pub fn into_attestation(self) -> Attestation { + map_signed_aggregate_and_proof_into_attestation!(self, |inner, cons| { + cons(inner.message.aggregate) + }) + } } diff --git a/consensus/types/src/signed_beacon_block.rs b/consensus/types/src/signed_beacon_block.rs index 4d3279a7f7..a22df49ad7 100644 --- a/consensus/types/src/signed_beacon_block.rs +++ b/consensus/types/src/signed_beacon_block.rs @@ -498,6 +498,7 @@ impl SignedBeaconBlockElectra> { execution_payload: BlindedPayloadElectra { .. }, bls_to_execution_changes, blob_kzg_commitments, + consolidations, }, }, signature, @@ -521,6 +522,7 @@ impl SignedBeaconBlockElectra> { execution_payload: FullPayloadElectra { execution_payload }, bls_to_execution_changes, blob_kzg_commitments, + consolidations, }, }, signature, diff --git a/consensus/types/src/subnet_id.rs b/consensus/types/src/subnet_id.rs index 1fff2518e8..66786b5129 100644 --- a/consensus/types/src/subnet_id.rs +++ b/consensus/types/src/subnet_id.rs @@ -1,5 +1,5 @@ //! Identifies each shard by an integer identifier. -use crate::{AttestationData, ChainSpec, CommitteeIndex, Epoch, EthSpec, Slot}; +use crate::{AttestationRef, ChainSpec, CommitteeIndex, Epoch, EthSpec, Slot}; use lazy_static::lazy_static; use safe_arith::{ArithError, SafeArith}; use serde::{Deserialize, Serialize}; @@ -38,16 +38,18 @@ impl SubnetId { id.into() } - /// Compute the subnet for an attestation with `attestation_data` where each slot in the + /// Compute the subnet for an attestation where each slot in the /// attestation epoch contains `committee_count_per_slot` committees. - pub fn compute_subnet_for_attestation_data( - attestation_data: &AttestationData, + pub fn compute_subnet_for_attestation( + attestation: AttestationRef, committee_count_per_slot: u64, spec: &ChainSpec, ) -> Result { + let committee_index = attestation.committee_index().ok_or(ArithError::Overflow)?; + Self::compute_subnet::( - attestation_data.slot, - attestation_data.index, + attestation.data().slot, + committee_index, committee_count_per_slot, spec, ) diff --git a/consensus/types/src/sync_committee_contribution.rs b/consensus/types/src/sync_committee_contribution.rs index 2c20daa970..c348c3e8be 100644 --- a/consensus/types/src/sync_committee_contribution.rs +++ b/consensus/types/src/sync_committee_contribution.rs @@ -63,13 +63,6 @@ impl SyncCommitteeContribution { }) } - /// Are the aggregation bitfields of these sync contribution disjoint? - pub fn signers_disjoint_from(&self, other: &Self) -> bool { - self.aggregation_bits - .intersection(&other.aggregation_bits) - .is_zero() - } - /// Aggregate another `SyncCommitteeContribution` into this one. /// /// The aggregation bitfields must be disjoint, and the data must be the same. @@ -77,7 +70,6 @@ impl SyncCommitteeContribution { debug_assert_eq!(self.slot, other.slot); debug_assert_eq!(self.beacon_block_root, other.beacon_block_root); debug_assert_eq!(self.subcommittee_index, other.subcommittee_index); - debug_assert!(self.signers_disjoint_from(other)); self.aggregation_bits = self.aggregation_bits.union(&other.aggregation_bits); self.signature.add_assign_aggregate(&other.signature); @@ -110,6 +102,12 @@ impl SlotData for SyncCommitteeContribution { } } +impl SlotData for &SyncCommitteeContribution { + fn get_slot(&self) -> Slot { + self.slot + } +} + impl SlotData for SyncContributionData { fn get_slot(&self) -> Slot { self.slot diff --git a/consensus/types/src/validator.rs b/consensus/types/src/validator.rs index ba99a0a68d..f2b36ee153 100644 --- a/consensus/types/src/validator.rs +++ b/consensus/types/src/validator.rs @@ -63,7 +63,7 @@ impl Validator { spec: &ChainSpec, current_fork: ForkName, ) -> bool { - if current_fork >= ForkName::Electra { + if current_fork.electra_enabled() { self.is_eligible_for_activation_queue_electra(spec) } else { self.is_eligible_for_activation_queue_base(spec) @@ -162,7 +162,7 @@ impl Validator { spec: &ChainSpec, current_fork: ForkName, ) -> bool { - if current_fork >= ForkName::Electra { + if current_fork.electra_enabled() { self.is_fully_withdrawable_at_electra(balance, epoch, spec) } else { self.is_fully_withdrawable_at_capella(balance, epoch, spec) @@ -202,7 +202,7 @@ impl Validator { spec: &ChainSpec, current_fork: ForkName, ) -> bool { - if current_fork >= ForkName::Electra { + if current_fork.electra_enabled() { self.is_partially_withdrawable_validator_electra(balance, spec) } else { self.is_partially_withdrawable_validator_capella(balance, spec) diff --git a/lcli/src/indexed_attestations.rs b/lcli/src/indexed_attestations.rs index 63f8cd9463..ccc1417112 100644 --- a/lcli/src/indexed_attestations.rs +++ b/lcli/src/indexed_attestations.rs @@ -1,6 +1,6 @@ use clap::ArgMatches; use clap_utils::parse_required; -use state_processing::common::get_indexed_attestation; +use state_processing::common::{attesting_indices_base, attesting_indices_electra}; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; @@ -33,9 +33,14 @@ pub fn run(matches: &ArgMatches) -> Result<(), String> { let indexed_attestations = attestations .into_iter() - .map(|att| { - let committee = state.get_beacon_committee(att.data.slot, att.data.index)?; - get_indexed_attestation(committee.committee, &att) + .map(|att| match att { + Attestation::Base(att) => { + let committee = state.get_beacon_committee(att.data.slot, att.data.index)?; + attesting_indices_base::get_indexed_attestation(committee.committee, &att) + } + Attestation::Electra(att) => { + attesting_indices_electra::get_indexed_attestation_from_state(&state, &att) + } }) .collect::, _>>() .map_err(|e| format!("Error constructing indexed attestation: {:?}", e))?; diff --git a/lcli/src/transition_blocks.rs b/lcli/src/transition_blocks.rs index 5a450fed77..62ae602187 100644 --- a/lcli/src/transition_blocks.rs +++ b/lcli/src/transition_blocks.rs @@ -391,7 +391,7 @@ fn do_transition( // Signature verification should prime the indexed attestation cache. assert_eq!( ctxt.num_cached_indexed_attestations(), - block.message().body().attestations().len() + block.message().body().attestations_len() ); } diff --git a/slasher/Cargo.toml b/slasher/Cargo.toml index ef5cb8249e..563c4599d8 100644 --- a/slasher/Cargo.toml +++ b/slasher/Cargo.toml @@ -29,6 +29,7 @@ tree_hash = { workspace = true } tree_hash_derive = { workspace = true } types = { workspace = true } strum = { workspace = true } +ssz_types = { workspace = true } # MDBX is pinned at the last version with Windows and macOS support. mdbx = { package = "libmdbx", git = "https://github.com/sigp/libmdbx-rs", tag = "v0.1.4", optional = true } diff --git a/slasher/src/array.rs b/slasher/src/array.rs index b733b07c63..77ddceb85f 100644 --- a/slasher/src/array.rs +++ b/slasher/src/array.rs @@ -226,12 +226,12 @@ impl TargetArrayChunk for MinTargetChunk { ) -> Result, Error> { let min_target = self.chunk - .get_target(validator_index, attestation.data.source.epoch, config)?; - if attestation.data.target.epoch > min_target { + .get_target(validator_index, attestation.data().source.epoch, config)?; + if attestation.data().target.epoch > min_target { let existing_attestation = db.get_attestation_for_validator(txn, validator_index, min_target)?; - if attestation.data.source.epoch < existing_attestation.data.source.epoch { + if attestation.data().source.epoch < existing_attestation.data().source.epoch { Ok(AttesterSlashingStatus::SurroundsExisting(Box::new( existing_attestation, ))) @@ -329,12 +329,12 @@ impl TargetArrayChunk for MaxTargetChunk { ) -> Result, Error> { let max_target = self.chunk - .get_target(validator_index, attestation.data.source.epoch, config)?; - if attestation.data.target.epoch < max_target { + .get_target(validator_index, attestation.data().source.epoch, config)?; + if attestation.data().target.epoch < max_target { let existing_attestation = db.get_attestation_for_validator(txn, validator_index, max_target)?; - if existing_attestation.data.source.epoch < attestation.data.source.epoch { + if existing_attestation.data().source.epoch < attestation.data().source.epoch { Ok(AttesterSlashingStatus::SurroundedByExisting(Box::new( existing_attestation, ))) @@ -428,7 +428,7 @@ pub fn apply_attestation_for_validator( current_epoch: Epoch, config: &Config, ) -> Result, Error> { - let mut chunk_index = config.chunk_index(attestation.data.source.epoch); + let mut chunk_index = config.chunk_index(attestation.data().source.epoch); let mut current_chunk = get_chunk_for_update( db, txn, @@ -446,7 +446,7 @@ pub fn apply_attestation_for_validator( } let Some(mut start_epoch) = - T::first_start_epoch(attestation.data.source.epoch, current_epoch, config) + T::first_start_epoch(attestation.data().source.epoch, current_epoch, config) else { return Ok(slashing_status); }; @@ -465,7 +465,7 @@ pub fn apply_attestation_for_validator( chunk_index, validator_index, start_epoch, - attestation.data.target.epoch, + attestation.data().target.epoch, current_epoch, config, )?; @@ -492,7 +492,7 @@ pub fn update( let mut chunk_attestations = BTreeMap::new(); for attestation in batch { chunk_attestations - .entry(config.chunk_index(attestation.indexed.data.source.epoch)) + .entry(config.chunk_index(attestation.indexed.data().source.epoch)) .or_insert_with(Vec::new) .push(attestation); } diff --git a/slasher/src/attestation_queue.rs b/slasher/src/attestation_queue.rs index 3d23932df9..62a1bb0945 100644 --- a/slasher/src/attestation_queue.rs +++ b/slasher/src/attestation_queue.rs @@ -47,7 +47,8 @@ impl AttestationBatch { self.attestations.push(Arc::downgrade(&indexed_record)); let attestation_data_hash = indexed_record.record.attestation_data_hash; - for &validator_index in &indexed_record.indexed.attesting_indices { + + for &validator_index in indexed_record.indexed.attesting_indices_iter() { self.attesters .entry((validator_index, attestation_data_hash)) .and_modify(|existing_entry| { @@ -56,8 +57,8 @@ impl AttestationBatch { // smaller indexed attestation. Single-bit attestations will usually be removed // completely by this process, and aggregates will only be retained if they // are not redundant with respect to a larger aggregate seen in the same batch. - if existing_entry.indexed.attesting_indices.len() - < indexed_record.indexed.attesting_indices.len() + if existing_entry.indexed.attesting_indices_len() + < indexed_record.indexed.attesting_indices_len() { *existing_entry = indexed_record.clone(); } diff --git a/slasher/src/attester_record.rs b/slasher/src/attester_record.rs index 56fdcb809f..1cd4ba7d4e 100644 --- a/slasher/src/attester_record.rs +++ b/slasher/src/attester_record.rs @@ -80,18 +80,20 @@ impl IndexedAttesterRecord { #[derive(Debug, Clone, Encode, Decode, TreeHash)] struct IndexedAttestationHeader { - pub attesting_indices: VariableList, + pub attesting_indices: VariableList, pub data_root: Hash256, pub signature: AggregateSignature, } impl From> for AttesterRecord { fn from(indexed_attestation: IndexedAttestation) -> AttesterRecord { - let attestation_data_hash = indexed_attestation.data.tree_hash_root(); + let attestation_data_hash = indexed_attestation.data().tree_hash_root(); + let attesting_indices = + VariableList::new(indexed_attestation.attesting_indices_to_vec()).unwrap_or_default(); let header = IndexedAttestationHeader:: { - attesting_indices: indexed_attestation.attesting_indices, + attesting_indices, data_root: attestation_data_hash, - signature: indexed_attestation.signature, + signature: indexed_attestation.signature().clone(), }; let indexed_attestation_hash = header.tree_hash_root(); AttesterRecord { @@ -104,15 +106,15 @@ impl From> for AttesterRecord { #[cfg(test)] mod test { use super::*; - use crate::test_utils::indexed_att; + use crate::test_utils::indexed_att_electra; // Check correctness of fast hashing #[test] fn fast_hash() { let data = vec![ - indexed_att(vec![], 0, 0, 0), - indexed_att(vec![1, 2, 3], 12, 14, 1), - indexed_att(vec![4], 0, 5, u64::MAX), + indexed_att_electra(vec![], 0, 0, 0), + indexed_att_electra(vec![1, 2, 3], 12, 14, 1), + indexed_att_electra(vec![4], 0, 5, u64::MAX), ]; for att in data { assert_eq!( diff --git a/slasher/src/config.rs b/slasher/src/config.rs index 54161f6ce9..1851e2e441 100644 --- a/slasher/src/config.rs +++ b/slasher/src/config.rs @@ -166,8 +166,7 @@ impl Config { validator_chunk_index: usize, ) -> impl Iterator + 'a { attestation - .attesting_indices - .iter() + .attesting_indices_iter() .filter(move |v| self.validator_chunk_index(**v) == validator_chunk_index) .copied() } diff --git a/slasher/src/database.rs b/slasher/src/database.rs index 49d2b00a4c..801abe9283 100644 --- a/slasher/src/database.rs +++ b/slasher/src/database.rs @@ -13,12 +13,15 @@ use parking_lot::Mutex; use serde::de::DeserializeOwned; use slog::{info, Logger}; use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; use std::borrow::{Borrow, Cow}; use std::marker::PhantomData; use std::sync::Arc; use tree_hash::TreeHash; use types::{ - Epoch, EthSpec, Hash256, IndexedAttestation, ProposerSlashing, SignedBeaconBlockHeader, Slot, + AggregateSignature, AttestationData, ChainSpec, Epoch, EthSpec, Hash256, IndexedAttestation, + IndexedAttestationBase, IndexedAttestationElectra, ProposerSlashing, SignedBeaconBlockHeader, + Slot, VariableList, }; /// Current database schema version, to check compatibility of on-disk DB with software. @@ -69,6 +72,7 @@ pub struct SlasherDB { /// LRU cache mapping indexed attestation IDs to their attestation data roots. attestation_root_cache: Mutex>, pub(crate) config: Arc, + pub(crate) spec: Arc, _phantom: PhantomData, } @@ -235,6 +239,43 @@ impl AsRef<[u8]> for IndexedAttestationId { } } +/// Indexed attestation that abstracts over Phase0 and Electra variants by using a plain `Vec` for +/// the attesting indices. +/// +/// This allows us to avoid rewriting the entire indexed attestation database at Electra, which +/// saves a lot of execution time. The bytes that it encodes to are the same as the bytes that a +/// regular IndexedAttestation encodes to, because SSZ doesn't care about the length-bound. +#[derive(Debug, PartialEq, Decode, Encode)] +pub struct IndexedAttestationOnDisk { + attesting_indices: Vec, + data: AttestationData, + signature: AggregateSignature, +} + +impl IndexedAttestationOnDisk { + fn into_indexed_attestation( + self, + spec: &ChainSpec, + ) -> Result, Error> { + let fork_at_target_epoch = spec.fork_name_at_epoch(self.data.target.epoch); + if fork_at_target_epoch.electra_enabled() { + let attesting_indices = VariableList::new(self.attesting_indices)?; + Ok(IndexedAttestation::Electra(IndexedAttestationElectra { + attesting_indices, + data: self.data, + signature: self.signature, + })) + } else { + let attesting_indices = VariableList::new(self.attesting_indices)?; + Ok(IndexedAttestation::Base(IndexedAttestationBase { + attesting_indices, + data: self.data, + signature: self.signature, + })) + } + } +} + /// Bincode deserialization specialised to `Cow<[u8]>`. fn bincode_deserialize(bytes: Cow<[u8]>) -> Result { Ok(bincode::deserialize(bytes.borrow())?) @@ -245,7 +286,7 @@ fn ssz_decode(bytes: Cow<[u8]>) -> Result { } impl SlasherDB { - pub fn open(config: Arc, log: Logger) -> Result { + pub fn open(config: Arc, spec: Arc, log: Logger) -> Result { info!(log, "Opening slasher database"; "backend" => %config.backend); std::fs::create_dir_all(&config.database_path)?; @@ -268,6 +309,7 @@ impl SlasherDB { databases, attestation_root_cache, config, + spec, _phantom: PhantomData, }; @@ -438,7 +480,7 @@ impl SlasherDB { ) -> Result { // Look-up ID by hash. let id_key = IndexedAttestationIdKey::new( - indexed_attestation.data.target.epoch, + indexed_attestation.data().target.epoch, indexed_attestation_hash, ); @@ -457,6 +499,7 @@ impl SlasherDB { }; let attestation_key = IndexedAttestationId::new(indexed_att_id); + // IndexedAttestationOnDisk and IndexedAttestation have compatible encodings. let data = indexed_attestation.as_ssz_bytes(); cursor.put(attestation_key.as_ref(), &data)?; @@ -481,7 +524,8 @@ impl SlasherDB { .ok_or(Error::MissingIndexedAttestation { id: indexed_attestation_id.as_u64(), })?; - ssz_decode(bytes) + let indexed_attestation_on_disk: IndexedAttestationOnDisk = ssz_decode(bytes)?; + indexed_attestation_on_disk.into_indexed_attestation(&self.spec) } fn get_attestation_data_root( @@ -500,7 +544,7 @@ impl SlasherDB { // Otherwise, load the indexed attestation, compute the root and cache it. let indexed_attestation = self.get_indexed_attestation(txn, indexed_id)?; - let attestation_data_root = indexed_attestation.data.tree_hash_root(); + let attestation_data_root = indexed_attestation.data().tree_hash_root(); cache.put(indexed_id, attestation_data_root); @@ -536,7 +580,7 @@ impl SlasherDB { indexed_attestation_id: IndexedAttestationId, ) -> Result, Error> { // See if there's an existing attestation for this attester. - let target_epoch = attestation.data.target.epoch; + let target_epoch = attestation.data().target.epoch; let prev_max_target = self.get_attester_max_target(validator_index, txn)?; @@ -771,3 +815,93 @@ impl SlasherDB { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use types::{Checkpoint, ForkName, MainnetEthSpec, Unsigned}; + + type E = MainnetEthSpec; + + fn indexed_attestation_on_disk_roundtrip_test( + spec: &ChainSpec, + make_attestation: fn( + Vec, + AttestationData, + AggregateSignature, + ) -> IndexedAttestation, + committee_len: u64, + ) { + let attestation_data = AttestationData { + slot: Slot::new(1000), + index: 0, + beacon_block_root: Hash256::repeat_byte(0xaa), + source: Checkpoint { + epoch: Epoch::new(0), + root: Hash256::repeat_byte(0xbb), + }, + target: Checkpoint { + epoch: Epoch::new(31), + root: Hash256::repeat_byte(0xcc), + }, + }; + + let attesting_indices = (0..committee_len).collect::>(); + let signature = AggregateSignature::infinity(); + + let fork_attestation = make_attestation( + attesting_indices.clone(), + attestation_data.clone(), + signature.clone(), + ); + + let on_disk = IndexedAttestationOnDisk { + attesting_indices, + data: attestation_data, + signature, + }; + let encoded = on_disk.as_ssz_bytes(); + assert_eq!(encoded, fork_attestation.as_ssz_bytes()); + + let decoded_on_disk = IndexedAttestationOnDisk::from_ssz_bytes(&encoded).unwrap(); + assert_eq!(decoded_on_disk, on_disk); + + let decoded = on_disk.into_indexed_attestation(spec).unwrap(); + assert_eq!(decoded, fork_attestation); + } + + /// Check that `IndexedAttestationOnDisk` and `IndexedAttestation` have compatible encodings. + #[test] + fn indexed_attestation_on_disk_roundtrip_base() { + let spec = ForkName::Base.make_genesis_spec(E::default_spec()); + let make_attestation = |attesting_indices, data, signature| { + IndexedAttestation::::Base(IndexedAttestationBase { + attesting_indices: VariableList::new(attesting_indices).unwrap(), + data, + signature, + }) + }; + indexed_attestation_on_disk_roundtrip_test( + &spec, + make_attestation, + ::MaxValidatorsPerCommittee::to_u64(), + ) + } + + #[test] + fn indexed_attestation_on_disk_roundtrip_electra() { + let spec = ForkName::Electra.make_genesis_spec(E::default_spec()); + let make_attestation = |attesting_indices, data, signature| { + IndexedAttestation::::Electra(IndexedAttestationElectra { + attesting_indices: VariableList::new(attesting_indices).unwrap(), + data, + signature, + }) + }; + indexed_attestation_on_disk_roundtrip_test( + &spec, + make_attestation, + ::MaxValidatorsPerSlot::to_u64(), + ) + } +} diff --git a/slasher/src/error.rs b/slasher/src/error.rs index b939c281e9..8d3295b22a 100644 --- a/slasher/src/error.rs +++ b/slasher/src/error.rs @@ -13,6 +13,7 @@ pub enum Error { DatabaseIOError(io::Error), DatabasePermissionsError(filesystem::Error), SszDecodeError(ssz::DecodeError), + SszTypesError(ssz_types::Error), BincodeError(bincode::Error), ArithError(safe_arith::ArithError), ChunkIndexOutOfBounds(usize), @@ -100,6 +101,12 @@ impl From for Error { } } +impl From for Error { + fn from(e: ssz_types::Error) -> Self { + Error::SszTypesError(e) + } +} + impl From for Error { fn from(e: bincode::Error) -> Self { Error::BincodeError(e) diff --git a/slasher/src/lib.rs b/slasher/src/lib.rs index 45cbef84f2..4d58fa7702 100644 --- a/slasher/src/lib.rs +++ b/slasher/src/lib.rs @@ -28,7 +28,8 @@ pub use database::{ }; pub use error::Error; -use types::{AttesterSlashing, EthSpec, IndexedAttestation, ProposerSlashing}; +use types::{AttesterSlashing, AttesterSlashingBase, AttesterSlashingElectra}; +use types::{EthSpec, IndexedAttestation, ProposerSlashing}; #[derive(Debug, PartialEq)] pub enum AttesterSlashingStatus { @@ -59,14 +60,34 @@ impl AttesterSlashingStatus { match self { NotSlashable => None, AlreadyDoubleVoted => None, - DoubleVote(existing) | SurroundedByExisting(existing) => Some(AttesterSlashing { - attestation_1: *existing, - attestation_2: new_attestation.clone(), - }), - SurroundsExisting(existing) => Some(AttesterSlashing { - attestation_1: new_attestation.clone(), - attestation_2: *existing, - }), + DoubleVote(existing) | SurroundedByExisting(existing) => { + match (&*existing, new_attestation) { + (IndexedAttestation::Base(existing_att), IndexedAttestation::Base(new)) => { + Some(AttesterSlashing::Base(AttesterSlashingBase { + attestation_1: existing_att.clone(), + attestation_2: new.clone(), + })) + } + // A slashing involving an electra attestation type must return an `AttesterSlashingElectra` type + (_, _) => Some(AttesterSlashing::Electra(AttesterSlashingElectra { + attestation_1: existing.clone().to_electra(), + attestation_2: new_attestation.clone().to_electra(), + })), + } + } + SurroundsExisting(existing) => match (&*existing, new_attestation) { + (IndexedAttestation::Base(existing_att), IndexedAttestation::Base(new)) => { + Some(AttesterSlashing::Base(AttesterSlashingBase { + attestation_1: new.clone(), + attestation_2: existing_att.clone(), + })) + } + // A slashing involving an electra attestation type must return an `AttesterSlashingElectra` type + (_, _) => Some(AttesterSlashing::Electra(AttesterSlashingElectra { + attestation_1: new_attestation.clone().to_electra(), + attestation_2: existing.clone().to_electra(), + })), + }, } } } diff --git a/slasher/src/slasher.rs b/slasher/src/slasher.rs index 066c8d63d9..0bb7c9c3ff 100644 --- a/slasher/src/slasher.rs +++ b/slasher/src/slasher.rs @@ -13,7 +13,8 @@ use slog::{debug, error, info, Logger}; use std::collections::HashSet; use std::sync::Arc; use types::{ - AttesterSlashing, Epoch, EthSpec, IndexedAttestation, ProposerSlashing, SignedBeaconBlockHeader, + AttesterSlashing, ChainSpec, Epoch, EthSpec, IndexedAttestation, ProposerSlashing, + SignedBeaconBlockHeader, }; #[derive(Debug)] @@ -28,10 +29,10 @@ pub struct Slasher { } impl Slasher { - pub fn open(config: Config, log: Logger) -> Result { + pub fn open(config: Config, spec: Arc, log: Logger) -> Result { config.validate()?; let config = Arc::new(config); - let db = SlasherDB::open(config.clone(), log.clone())?; + let db = SlasherDB::open(config.clone(), spec, log.clone())?; let attester_slashings = Mutex::new(HashSet::new()); let proposer_slashings = Mutex::new(HashSet::new()); let attestation_queue = AttestationQueue::default(); @@ -299,7 +300,7 @@ impl Slasher { self.log, "Found double-vote slashing"; "validator_index" => validator_index, - "epoch" => slashing.attestation_1.data.target.epoch, + "epoch" => slashing.attestation_1().data().target.epoch, ); slashings.insert(slashing); } @@ -325,8 +326,8 @@ impl Slasher { for indexed_record in batch { let attestation = &indexed_record.indexed; - let target_epoch = attestation.data.target.epoch; - let source_epoch = attestation.data.source.epoch; + let target_epoch = attestation.data().target.epoch; + let source_epoch = attestation.data().source.epoch; if source_epoch > target_epoch || source_epoch + self.config.history_length as u64 <= current_epoch diff --git a/slasher/src/test_utils.rs b/slasher/src/test_utils.rs index 0011df1ffb..453d0e6667 100644 --- a/slasher/src/test_utils.rs +++ b/slasher/src/test_utils.rs @@ -1,18 +1,21 @@ use std::collections::HashSet; +use std::sync::Arc; use types::{ - AggregateSignature, AttestationData, AttesterSlashing, BeaconBlockHeader, Checkpoint, Epoch, - Hash256, IndexedAttestation, MainnetEthSpec, Signature, SignedBeaconBlockHeader, Slot, + indexed_attestation::{IndexedAttestationBase, IndexedAttestationElectra}, + AggregateSignature, AttestationData, AttesterSlashing, AttesterSlashingBase, + AttesterSlashingElectra, BeaconBlockHeader, ChainSpec, Checkpoint, Epoch, EthSpec, Hash256, + IndexedAttestation, MainnetEthSpec, Signature, SignedBeaconBlockHeader, Slot, }; pub type E = MainnetEthSpec; -pub fn indexed_att( +pub fn indexed_att_electra( attesting_indices: impl AsRef<[u64]>, source_epoch: u64, target_epoch: u64, target_root: u64, ) -> IndexedAttestation { - IndexedAttestation { + IndexedAttestation::Electra(IndexedAttestationElectra { attesting_indices: attesting_indices.as_ref().to_vec().into(), data: AttestationData { slot: Slot::new(0), @@ -28,16 +31,50 @@ pub fn indexed_att( }, }, signature: AggregateSignature::empty(), - } + }) +} + +pub fn indexed_att( + attesting_indices: impl AsRef<[u64]>, + source_epoch: u64, + target_epoch: u64, + target_root: u64, +) -> IndexedAttestation { + IndexedAttestation::Base(IndexedAttestationBase { + attesting_indices: attesting_indices.as_ref().to_vec().into(), + data: AttestationData { + slot: Slot::new(0), + index: 0, + beacon_block_root: Hash256::zero(), + source: Checkpoint { + epoch: Epoch::new(source_epoch), + root: Hash256::from_low_u64_be(0), + }, + target: Checkpoint { + epoch: Epoch::new(target_epoch), + root: Hash256::from_low_u64_be(target_root), + }, + }, + signature: AggregateSignature::empty(), + }) } pub fn att_slashing( attestation_1: &IndexedAttestation, attestation_2: &IndexedAttestation, ) -> AttesterSlashing { - AttesterSlashing { - attestation_1: attestation_1.clone(), - attestation_2: attestation_2.clone(), + match (attestation_1, attestation_2) { + (IndexedAttestation::Base(att1), IndexedAttestation::Base(att2)) => { + AttesterSlashing::Base(AttesterSlashingBase { + attestation_1: att1.clone(), + attestation_2: att2.clone(), + }) + } + // A slashing involving an electra attestation type must return an electra AttesterSlashing type + (_, _) => AttesterSlashing::Electra(AttesterSlashingElectra { + attestation_1: attestation_1.clone().to_electra(), + attestation_2: attestation_2.clone().to_electra(), + }), } } @@ -59,14 +96,16 @@ pub fn slashed_validators_from_slashings(slashings: &HashSet slashings .iter() .flat_map(|slashing| { - let att1 = &slashing.attestation_1; - let att2 = &slashing.attestation_2; + let att1 = slashing.attestation_1(); + let att2 = slashing.attestation_2(); assert!( att1.is_double_vote(att2) || att1.is_surround_vote(att2), "invalid slashing: {:#?}", slashing ); - hashset_intersection(&att1.attesting_indices, &att2.attesting_indices) + let attesting_indices_1 = att1.attesting_indices_to_vec(); + let attesting_indices_2 = att2.attesting_indices_to_vec(); + hashset_intersection(&attesting_indices_1, &attesting_indices_2) }) .collect() } @@ -83,9 +122,11 @@ pub fn slashed_validators_from_attestations( } if att1.is_double_vote(att2) || att1.is_surround_vote(att2) { + let attesting_indices_1 = att1.attesting_indices_to_vec(); + let attesting_indices_2 = att2.attesting_indices_to_vec(); slashed_validators.extend(hashset_intersection( - &att1.attesting_indices, - &att2.attesting_indices, + &attesting_indices_1, + &attesting_indices_2, )); } } @@ -105,3 +146,7 @@ pub fn block(slot: u64, proposer_index: u64, block_root: u64) -> SignedBeaconBlo signature: Signature::empty(), } } + +pub fn chain_spec() -> Arc { + Arc::new(E::default_spec()) +} diff --git a/slasher/tests/attester_slashings.rs b/slasher/tests/attester_slashings.rs index 40d9fa511c..902141d971 100644 --- a/slasher/tests/attester_slashings.rs +++ b/slasher/tests/attester_slashings.rs @@ -5,7 +5,10 @@ use maplit::hashset; use rayon::prelude::*; use slasher::{ config::DEFAULT_CHUNK_SIZE, - test_utils::{att_slashing, indexed_att, slashed_validators_from_slashings, E}, + test_utils::{ + att_slashing, chain_spec, indexed_att, indexed_att_electra, + slashed_validators_from_slashings, E, + }, Config, Slasher, }; use std::collections::HashSet; @@ -15,23 +18,35 @@ use types::{AttesterSlashing, Epoch, IndexedAttestation}; #[test] fn double_vote_single_val() { let v = vec![99]; - let att1 = indexed_att(&v, 0, 1, 0); - let att2 = indexed_att(&v, 0, 1, 1); - let slashings = hashset![att_slashing(&att1, &att2)]; - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &slashings, 1); - slasher_test_indiv(&attestations, &slashings, 1000); + for (att1, att2) in [ + (indexed_att(&v, 0, 1, 0), indexed_att(&v, 0, 1, 1)), + ( + indexed_att_electra(&v, 0, 1, 0), + indexed_att_electra(&v, 0, 1, 1), + ), + ] { + let slashings = hashset![att_slashing(&att1, &att2)]; + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &slashings, 1); + slasher_test_indiv(&attestations, &slashings, 1000); + } } #[test] fn double_vote_multi_vals() { let v = vec![0, 1, 2]; - let att1 = indexed_att(&v, 0, 1, 0); - let att2 = indexed_att(&v, 0, 1, 1); - let slashings = hashset![att_slashing(&att1, &att2)]; - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &slashings, 1); - slasher_test_indiv(&attestations, &slashings, 1000); + for (att1, att2) in [ + (indexed_att(&v, 0, 1, 0), indexed_att(&v, 0, 1, 1)), + ( + indexed_att_electra(&v, 0, 1, 0), + indexed_att_electra(&v, 0, 1, 1), + ), + ] { + let slashings = hashset![att_slashing(&att1, &att2)]; + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &slashings, 1); + slasher_test_indiv(&attestations, &slashings, 1000); + } } // A subset of validators double vote. @@ -39,12 +54,18 @@ fn double_vote_multi_vals() { fn double_vote_some_vals() { let v1 = vec![0, 1, 2, 3, 4, 5, 6]; let v2 = vec![0, 2, 4, 6]; - let att1 = indexed_att(v1, 0, 1, 0); - let att2 = indexed_att(v2, 0, 1, 1); - let slashings = hashset![att_slashing(&att1, &att2)]; - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &slashings, 1); - slasher_test_indiv(&attestations, &slashings, 1000); + for (att1, att2) in [ + (indexed_att(&v1, 0, 1, 0), indexed_att(&v2, 0, 1, 1)), + ( + indexed_att_electra(&v1, 0, 1, 0), + indexed_att_electra(&v2, 0, 1, 1), + ), + ] { + let slashings = hashset![att_slashing(&att1, &att2)]; + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &slashings, 1); + slasher_test_indiv(&attestations, &slashings, 1000); + } } // A subset of validators double vote, others vote twice for the same thing. @@ -53,13 +74,23 @@ fn double_vote_some_vals_repeat() { let v1 = vec![0, 1, 2, 3, 4, 5, 6]; let v2 = vec![0, 2, 4, 6]; let v3 = vec![1, 3, 5]; - let att1 = indexed_att(v1, 0, 1, 0); - let att2 = indexed_att(v2, 0, 1, 1); - let att3 = indexed_att(v3, 0, 1, 0); - let slashings = hashset![att_slashing(&att1, &att2)]; - let attestations = vec![att1, att2, att3]; - slasher_test_indiv(&attestations, &slashings, 1); - slasher_test_indiv(&attestations, &slashings, 1000); + for (att1, att2, att3) in [ + ( + indexed_att(&v1, 0, 1, 0), + indexed_att(&v2, 0, 1, 1), + indexed_att(&v3, 0, 1, 0), + ), + ( + indexed_att_electra(&v1, 0, 1, 0), + indexed_att_electra(&v2, 0, 1, 1), + indexed_att_electra(&v3, 0, 1, 0), + ), + ] { + let slashings = hashset![att_slashing(&att1, &att2)]; + let attestations = vec![att1, att2, att3]; + slasher_test_indiv(&attestations, &slashings, 1); + slasher_test_indiv(&attestations, &slashings, 1000); + } } // Nobody double votes, nobody gets slashed. @@ -67,11 +98,17 @@ fn double_vote_some_vals_repeat() { fn no_double_vote_same_target() { let v1 = vec![0, 1, 2, 3, 4, 5, 6]; let v2 = vec![0, 1, 2, 3, 4, 5, 7, 8]; - let att1 = indexed_att(v1, 0, 1, 0); - let att2 = indexed_att(v2, 0, 1, 0); - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &hashset! {}, 1); - slasher_test_indiv(&attestations, &hashset! {}, 1000); + for (att1, att2) in [ + (indexed_att(&v1, 0, 1, 0), indexed_att(&v2, 0, 1, 0)), + ( + indexed_att_electra(&v1, 0, 1, 0), + indexed_att_electra(&v2, 0, 1, 0), + ), + ] { + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &hashset! {}, 1); + slasher_test_indiv(&attestations, &hashset! {}, 1000); + } } // Two groups votes for different things, no slashings. @@ -79,73 +116,133 @@ fn no_double_vote_same_target() { fn no_double_vote_distinct_vals() { let v1 = vec![0, 1, 2, 3]; let v2 = vec![4, 5, 6, 7]; - let att1 = indexed_att(v1, 0, 1, 0); - let att2 = indexed_att(v2, 0, 1, 1); - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &hashset! {}, 1); - slasher_test_indiv(&attestations, &hashset! {}, 1000); + for (att1, att2) in [ + (indexed_att(&v1, 0, 1, 0), indexed_att(&v2, 0, 1, 0)), + ( + indexed_att_electra(&v1, 0, 1, 0), + indexed_att_electra(&v2, 0, 1, 1), + ), + ] { + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &hashset! {}, 1); + slasher_test_indiv(&attestations, &hashset! {}, 1000); + } } #[test] fn no_double_vote_repeated() { let v = vec![0, 1, 2, 3, 4]; - let att1 = indexed_att(v, 0, 1, 0); - let att2 = att1.clone(); - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &hashset! {}, 1); - slasher_test_batch(&attestations, &hashset! {}, 1); - parallel_slasher_test(&attestations, hashset! {}, 1); + for att1 in [indexed_att(&v, 0, 1, 0), indexed_att_electra(&v, 0, 1, 0)] { + let att2 = att1.clone(); + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &hashset! {}, 1); + slasher_test_batch(&attestations, &hashset! {}, 1); + parallel_slasher_test(&attestations, hashset! {}, 1); + } } #[test] fn surrounds_existing_single_val_single_chunk() { let v = vec![0]; - let att1 = indexed_att(&v, 1, 2, 0); - let att2 = indexed_att(&v, 0, 3, 0); - let slashings = hashset![att_slashing(&att2, &att1)]; - slasher_test_indiv(&[att1, att2], &slashings, 3); + for (att1, att2) in [ + (indexed_att(&v, 1, 2, 0), indexed_att(&v, 0, 3, 0)), + (indexed_att(&v, 1, 2, 0), indexed_att_electra(&v, 0, 3, 0)), + ( + indexed_att_electra(&v, 1, 2, 0), + indexed_att_electra(&v, 0, 3, 0), + ), + ] { + let slashings = hashset![att_slashing(&att2, &att1)]; + slasher_test_indiv(&[att1, att2], &slashings, 3); + } } #[test] fn surrounds_existing_multi_vals_single_chunk() { let validators = vec![0, 16, 1024, 300_000, 300_001]; - let att1 = indexed_att(validators.clone(), 1, 2, 0); - let att2 = indexed_att(validators, 0, 3, 0); - let slashings = hashset![att_slashing(&att2, &att1)]; - slasher_test_indiv(&[att1, att2], &slashings, 3); + for (att1, att2) in [ + ( + indexed_att(&validators, 1, 2, 0), + indexed_att(&validators, 0, 3, 0), + ), + ( + indexed_att(&validators, 1, 2, 0), + indexed_att_electra(&validators, 0, 3, 0), + ), + ( + indexed_att_electra(&validators, 1, 2, 0), + indexed_att_electra(&validators, 0, 3, 0), + ), + ] { + let slashings = hashset![att_slashing(&att2, &att1)]; + slasher_test_indiv(&[att1, att2], &slashings, 3); + } } #[test] fn surrounds_existing_many_chunks() { let v = vec![0]; let chunk_size = DEFAULT_CHUNK_SIZE as u64; - let att1 = indexed_att(&v, 3 * chunk_size, 3 * chunk_size + 1, 0); - let att2 = indexed_att(&v, 0, 3 * chunk_size + 2, 0); - let slashings = hashset![att_slashing(&att2, &att1)]; - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &slashings, 4 * chunk_size); + for (att1, att2) in [ + ( + indexed_att(&v, 3 * chunk_size, 3 * chunk_size + 1, 0), + indexed_att(&v, 0, 3 * chunk_size + 2, 0), + ), + ( + indexed_att(&v, 3 * chunk_size, 3 * chunk_size + 1, 0), + indexed_att_electra(&v, 0, 3 * chunk_size + 2, 0), + ), + ( + indexed_att_electra(&v, 3 * chunk_size, 3 * chunk_size + 1, 0), + indexed_att_electra(&v, 0, 3 * chunk_size + 2, 0), + ), + ] { + let slashings = hashset![att_slashing(&att2, &att1)]; + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &slashings, 4 * chunk_size); + } } #[test] fn surrounded_by_single_val_single_chunk() { let v = vec![0]; - let att1 = indexed_att(&v, 0, 15, 0); - let att2 = indexed_att(&v, 1, 14, 0); - let slashings = hashset![att_slashing(&att1, &att2)]; - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &slashings, 15); + for (att1, att2) in [ + (indexed_att(&v, 0, 15, 0), indexed_att(&v, 1, 14, 0)), + (indexed_att(&v, 0, 15, 0), indexed_att_electra(&v, 1, 14, 0)), + ( + indexed_att_electra(&v, 0, 15, 0), + indexed_att_electra(&v, 1, 14, 0), + ), + ] { + let slashings = hashset![att_slashing(&att1, &att2)]; + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &slashings, 15); + } } #[test] fn surrounded_by_single_val_multi_chunk() { let v = vec![0]; let chunk_size = DEFAULT_CHUNK_SIZE as u64; - let att1 = indexed_att(&v, 0, 3 * chunk_size, 0); - let att2 = indexed_att(&v, chunk_size, chunk_size + 1, 0); - let slashings = hashset![att_slashing(&att1, &att2)]; - let attestations = vec![att1, att2]; - slasher_test_indiv(&attestations, &slashings, 3 * chunk_size); - slasher_test_indiv(&attestations, &slashings, 4 * chunk_size); + for (att1, att2) in [ + ( + indexed_att(&v, 0, 3 * chunk_size, 0), + indexed_att(&v, chunk_size, chunk_size + 1, 0), + ), + ( + indexed_att(&v, 0, 3 * chunk_size, 0), + indexed_att_electra(&v, chunk_size, chunk_size + 1, 0), + ), + ( + indexed_att_electra(&v, 0, 3 * chunk_size, 0), + indexed_att_electra(&v, chunk_size, chunk_size + 1, 0), + ), + ] { + let slashings = hashset![att_slashing(&att1, &att2)]; + let attestations = vec![att1, att2]; + slasher_test_indiv(&attestations, &slashings, 3 * chunk_size); + slasher_test_indiv(&attestations, &slashings, 4 * chunk_size); + } } // Process each attestation individually, and confirm that the slashings produced are as expected. @@ -174,7 +271,8 @@ fn slasher_test( ) { let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); - let slasher = Slasher::open(config, test_logger()).unwrap(); + let spec = chain_spec(); + let slasher = Slasher::open(config, spec, test_logger()).unwrap(); let current_epoch = Epoch::new(current_epoch); for (i, attestation) in attestations.iter().enumerate() { @@ -203,7 +301,8 @@ fn parallel_slasher_test( ) { let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); - let slasher = Slasher::open(config, test_logger()).unwrap(); + let spec = chain_spec(); + let slasher = Slasher::open(config, spec, test_logger()).unwrap(); let current_epoch = Epoch::new(current_epoch); attestations diff --git a/slasher/tests/proposer_slashings.rs b/slasher/tests/proposer_slashings.rs index 3b7b8ed583..2d2738087d 100644 --- a/slasher/tests/proposer_slashings.rs +++ b/slasher/tests/proposer_slashings.rs @@ -2,7 +2,7 @@ use logging::test_logger; use slasher::{ - test_utils::{block as test_block, E}, + test_utils::{block as test_block, chain_spec, E}, Config, Slasher, }; use tempfile::tempdir; @@ -12,7 +12,8 @@ use types::{Epoch, EthSpec}; fn empty_pruning() { let tempdir = tempdir().unwrap(); let config = Config::new(tempdir.path().into()); - let slasher = Slasher::::open(config, test_logger()).unwrap(); + let spec = chain_spec(); + let slasher = Slasher::::open(config, spec, test_logger()).unwrap(); slasher.prune_database(Epoch::new(0)).unwrap(); } @@ -24,8 +25,9 @@ fn block_pruning() { let mut config = Config::new(tempdir.path().into()); config.chunk_size = 2; config.history_length = 2; + let spec = chain_spec(); - let slasher = Slasher::::open(config.clone(), test_logger()).unwrap(); + let slasher = Slasher::::open(config.clone(), spec, test_logger()).unwrap(); let current_epoch = Epoch::from(2 * config.history_length); // Pruning the empty database should be safe. diff --git a/slasher/tests/random.rs b/slasher/tests/random.rs index ce0e42df1d..ebfe0ef4e9 100644 --- a/slasher/tests/random.rs +++ b/slasher/tests/random.rs @@ -4,7 +4,7 @@ use logging::test_logger; use rand::prelude::*; use slasher::{ test_utils::{ - block, indexed_att, slashed_validators_from_attestations, + block, chain_spec, indexed_att, slashed_validators_from_attestations, slashed_validators_from_slashings, E, }, Config, Slasher, @@ -49,7 +49,9 @@ fn random_test(seed: u64, test_config: TestConfig) { config.chunk_size = 1 << chunk_size_exponent; config.history_length = 1 << rng.gen_range(chunk_size_exponent..chunk_size_exponent + 3); - let slasher = Slasher::::open(config.clone(), test_logger()).unwrap(); + let spec = chain_spec(); + + let slasher = Slasher::::open(config.clone(), spec, test_logger()).unwrap(); let validators = (0..num_validators as u64).collect::>(); diff --git a/slasher/tests/wrap_around.rs b/slasher/tests/wrap_around.rs index d2c876d363..9a42aeb60b 100644 --- a/slasher/tests/wrap_around.rs +++ b/slasher/tests/wrap_around.rs @@ -1,7 +1,10 @@ #![cfg(any(feature = "mdbx", feature = "lmdb"))] use logging::test_logger; -use slasher::{test_utils::indexed_att, Config, Slasher}; +use slasher::{ + test_utils::{chain_spec, indexed_att}, + Config, Slasher, +}; use tempfile::tempdir; use types::Epoch; @@ -9,11 +12,12 @@ use types::Epoch; fn attestation_pruning_empty_wrap_around() { let tempdir = tempdir().unwrap(); let mut config = Config::new(tempdir.path().into()); + let spec = chain_spec(); config.validator_chunk_size = 1; config.chunk_size = 16; config.history_length = 16; - let slasher = Slasher::open(config.clone(), test_logger()).unwrap(); + let slasher = Slasher::open(config.clone(), spec, test_logger()).unwrap(); let v = vec![0]; let history_length = config.history_length as u64; diff --git a/testing/ef_tests/check_all_files_accessed.py b/testing/ef_tests/check_all_files_accessed.py index 7629d61827..5f425ab98b 100755 --- a/testing/ef_tests/check_all_files_accessed.py +++ b/testing/ef_tests/check_all_files_accessed.py @@ -42,7 +42,9 @@ excluded_paths = [ "bls12-381-tests/deserialization_G2", "bls12-381-tests/hash_to_G2", "tests/.*/eip6110", - "tests/.*/whisk" + "tests/.*/whisk", + # TODO(electra): re-enable in https://github.com/sigp/lighthouse/pull/5758 + "tests/.*/.*/ssz_static/IndexedAttestation" ] diff --git a/testing/ef_tests/src/cases/fork_choice.rs b/testing/ef_tests/src/cases/fork_choice.rs index bd8cc79156..2a2cc067e5 100644 --- a/testing/ef_tests/src/cases/fork_choice.rs +++ b/testing/ef_tests/src/cases/fork_choice.rs @@ -24,9 +24,9 @@ use std::future::Future; use std::sync::Arc; use std::time::Duration; use types::{ - Attestation, AttesterSlashing, BeaconBlock, BeaconState, BlobSidecar, BlobsList, - BlockImportSource, Checkpoint, ExecutionBlockHash, Hash256, IndexedAttestation, KzgProof, - ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, + Attestation, AttestationRef, AttesterSlashing, AttesterSlashingRef, BeaconBlock, BeaconState, + BlobSidecar, BlobsList, BlockImportSource, Checkpoint, ExecutionBlockHash, Hash256, + IndexedAttestation, KzgProof, ProposerPreparationData, SignedBeaconBlock, Slot, Uint256, }; #[derive(Default, Debug, PartialEq, Clone, Deserialize, Decode)] @@ -180,12 +180,32 @@ impl LoadCase for ForkChoiceTest { }) } Step::Attestation { attestation } => { - ssz_decode_file(&path.join(format!("{}.ssz_snappy", attestation))) - .map(|attestation| Step::Attestation { attestation }) + if fork_name.electra_enabled() { + ssz_decode_file(&path.join(format!("{}.ssz_snappy", attestation))).map( + |attestation| Step::Attestation { + attestation: Attestation::Electra(attestation), + }, + ) + } else { + ssz_decode_file(&path.join(format!("{}.ssz_snappy", attestation))).map( + |attestation| Step::Attestation { + attestation: Attestation::Base(attestation), + }, + ) + } } Step::AttesterSlashing { attester_slashing } => { - ssz_decode_file(&path.join(format!("{}.ssz_snappy", attester_slashing))) - .map(|attester_slashing| Step::AttesterSlashing { attester_slashing }) + if fork_name.electra_enabled() { + ssz_decode_file(&path.join(format!("{}.ssz_snappy", attester_slashing))) + .map(|attester_slashing| Step::AttesterSlashing { + attester_slashing: AttesterSlashing::Electra(attester_slashing), + }) + } else { + ssz_decode_file(&path.join(format!("{}.ssz_snappy", attester_slashing))) + .map(|attester_slashing| Step::AttesterSlashing { + attester_slashing: AttesterSlashing::Base(attester_slashing), + }) + } } Step::PowBlock { pow_block } => { ssz_decode_file(&path.join(format!("{}.ssz_snappy", pow_block))) @@ -249,7 +269,7 @@ impl Case for ForkChoiceTest { } => tester.process_block(block.clone(), blobs.clone(), proofs.clone(), *valid)?, Step::Attestation { attestation } => tester.process_attestation(attestation)?, Step::AttesterSlashing { attester_slashing } => { - tester.process_attester_slashing(attester_slashing) + tester.process_attester_slashing(attester_slashing.to_ref()) } Step::PowBlock { pow_block } => tester.process_pow_block(pow_block), Step::OnPayloadInfo { @@ -575,11 +595,11 @@ impl Tester { } pub fn process_attestation(&self, attestation: &Attestation) -> Result<(), Error> { - let (indexed_attestation, _) = - obtain_indexed_attestation_and_committees_per_slot(&self.harness.chain, attestation) - .map_err(|e| { - Error::InternalError(format!("attestation indexing failed with {:?}", e)) - })?; + let (indexed_attestation, _) = obtain_indexed_attestation_and_committees_per_slot( + &self.harness.chain, + attestation.to_ref(), + ) + .map_err(|e| Error::InternalError(format!("attestation indexing failed with {:?}", e)))?; let verified_attestation: ManuallyVerifiedAttestation> = ManuallyVerifiedAttestation { attestation, @@ -592,7 +612,7 @@ impl Tester { .map_err(|e| Error::InternalError(format!("attestation import failed with {:?}", e))) } - pub fn process_attester_slashing(&self, attester_slashing: &AttesterSlashing) { + pub fn process_attester_slashing(&self, attester_slashing: AttesterSlashingRef) { self.harness .chain .canonical_head @@ -851,8 +871,8 @@ pub struct ManuallyVerifiedAttestation<'a, T: BeaconChainTypes> { } impl<'a, T: BeaconChainTypes> VerifiedAttestation for ManuallyVerifiedAttestation<'a, T> { - fn attestation(&self) -> &Attestation { - self.attestation + fn attestation(&self) -> AttestationRef { + self.attestation.to_ref() } fn indexed_attestation(&self) -> &IndexedAttestation { diff --git a/testing/ef_tests/src/cases/operations.rs b/testing/ef_tests/src/cases/operations.rs index 158f2334dc..d87770a4df 100644 --- a/testing/ef_tests/src/cases/operations.rs +++ b/testing/ef_tests/src/cases/operations.rs @@ -78,8 +78,12 @@ impl Operation for Attestation { "attestation".into() } - fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { - ssz_decode_file(path) + fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { + if fork_name < ForkName::Electra { + Ok(Self::Base(ssz_decode_file(path)?)) + } else { + Ok(Self::Electra(ssz_decode_file(path)?)) + } } fn apply_to( @@ -93,7 +97,7 @@ impl Operation for Attestation { match state { BeaconState::Base(_) => base::process_attestations( state, - &[self.clone()], + [self.clone().to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, spec, @@ -106,7 +110,7 @@ impl Operation for Attestation { initialize_progressive_balances_cache(state, spec)?; altair_deneb::process_attestation( state, - self, + self.to_ref(), 0, &mut ctxt, VerifySignatures::True, @@ -122,8 +126,15 @@ impl Operation for AttesterSlashing { "attester_slashing".into() } - fn decode(path: &Path, _fork_name: ForkName, _spec: &ChainSpec) -> Result { - ssz_decode_file(path) + fn decode(path: &Path, fork_name: ForkName, _spec: &ChainSpec) -> Result { + Ok(match fork_name { + ForkName::Base + | ForkName::Altair + | ForkName::Bellatrix + | ForkName::Capella + | ForkName::Deneb => Self::Base(ssz_decode_file(path)?), + ForkName::Electra => Self::Electra(ssz_decode_file(path)?), + }) } fn apply_to( @@ -136,7 +147,7 @@ impl Operation for AttesterSlashing { initialize_progressive_balances_cache(state, spec)?; process_attester_slashings( state, - &[self.clone()], + [self.clone().to_ref()].into_iter(), VerifySignatures::True, &mut ctxt, spec, diff --git a/testing/ef_tests/src/handler.rs b/testing/ef_tests/src/handler.rs index 2d5ea4149e..c1e7386bc4 100644 --- a/testing/ef_tests/src/handler.rs +++ b/testing/ef_tests/src/handler.rs @@ -229,6 +229,10 @@ impl SszStaticHandler { Self::for_forks(vec![ForkName::Deneb]) } + pub fn electra_only() -> Self { + Self::for_forks(vec![ForkName::Electra]) + } + pub fn altair_and_later() -> Self { Self::for_forks(ForkName::list_all()[1..].to_vec()) } @@ -240,6 +244,10 @@ impl SszStaticHandler { pub fn capella_and_later() -> Self { Self::for_forks(ForkName::list_all()[3..].to_vec()) } + + pub fn pre_electra() -> Self { + Self::for_forks(ForkName::list_all()[0..5].to_vec()) + } } /// Handler for SSZ types that implement `CachedTreeHash`. diff --git a/testing/ef_tests/src/type_name.rs b/testing/ef_tests/src/type_name.rs index 49ebbe8190..f886431b40 100644 --- a/testing/ef_tests/src/type_name.rs +++ b/testing/ef_tests/src/type_name.rs @@ -37,11 +37,16 @@ macro_rules! type_name_generic { type_name!(MinimalEthSpec, "minimal"); type_name!(MainnetEthSpec, "mainnet"); - type_name_generic!(AggregateAndProof); +type_name_generic!(AggregateAndProofBase, "AggregateAndProof"); +type_name_generic!(AggregateAndProofElectra, "AggregateAndProof"); type_name_generic!(Attestation); +type_name_generic!(AttestationBase, "Attestation"); +type_name_generic!(AttestationElectra, "Attestation"); type_name!(AttestationData); type_name_generic!(AttesterSlashing); +type_name_generic!(AttesterSlashingBase, "AttesterSlashing"); +type_name_generic!(AttesterSlashingElectra, "AttesterSlashing"); type_name_generic!(BeaconBlock); type_name_generic!(BeaconBlockBody); type_name_generic!(BeaconBlockBodyBase, "BeaconBlockBody"); @@ -73,6 +78,8 @@ type_name!(Fork); type_name!(ForkData); type_name_generic!(HistoricalBatch); type_name_generic!(IndexedAttestation); +type_name_generic!(IndexedAttestationBase, "IndexedAttestation"); +type_name_generic!(IndexedAttestationElectra, "IndexedAttestation"); type_name_generic!(LightClientBootstrap); type_name_generic!(LightClientBootstrapAltair, "LightClientBootstrap"); type_name_generic!(LightClientBootstrapCapella, "LightClientBootstrap"); @@ -108,6 +115,8 @@ type_name_generic!(LightClientUpdateDeneb, "LightClientUpdate"); type_name_generic!(PendingAttestation); type_name!(ProposerSlashing); type_name_generic!(SignedAggregateAndProof); +type_name_generic!(SignedAggregateAndProofBase, "SignedAggregateAndProof"); +type_name_generic!(SignedAggregateAndProofElectra, "SignedAggregateAndProof"); type_name_generic!(SignedBeaconBlock); type_name!(SignedBeaconBlockHeader); type_name_generic!(SignedContributionAndProof); diff --git a/testing/ef_tests/tests/tests.rs b/testing/ef_tests/tests/tests.rs index 5226c7ac2b..6068514010 100644 --- a/testing/ef_tests/tests/tests.rs +++ b/testing/ef_tests/tests/tests.rs @@ -217,12 +217,9 @@ mod ssz_static { use ef_tests::{Handler, SszStaticHandler, SszStaticTHCHandler, SszStaticWithSpecHandler}; use types::blob_sidecar::BlobIdentifier; use types::historical_summary::HistoricalSummary; - use types::{LightClientBootstrapAltair, *}; + use types::{AttesterSlashingBase, AttesterSlashingElectra, LightClientBootstrapAltair, *}; - ssz_static_test!(aggregate_and_proof, AggregateAndProof<_>); - ssz_static_test!(attestation, Attestation<_>); ssz_static_test!(attestation_data, AttestationData); - ssz_static_test!(attester_slashing, AttesterSlashing<_>); ssz_static_test!(beacon_block, SszStaticWithSpecHandler, BeaconBlock<_>); ssz_static_test!(beacon_block_header, BeaconBlockHeader); ssz_static_test!(beacon_state, SszStaticTHCHandler, BeaconState<_>); @@ -235,10 +232,8 @@ mod ssz_static { ssz_static_test!(fork, Fork); ssz_static_test!(fork_data, ForkData); ssz_static_test!(historical_batch, HistoricalBatch<_>); - ssz_static_test!(indexed_attestation, IndexedAttestation<_>); ssz_static_test!(pending_attestation, PendingAttestation<_>); ssz_static_test!(proposer_slashing, ProposerSlashing); - ssz_static_test!(signed_aggregate_and_proof, SignedAggregateAndProof<_>); ssz_static_test!( signed_beacon_block, SszStaticWithSpecHandler, @@ -249,6 +244,59 @@ mod ssz_static { ssz_static_test!(signing_data, SigningData); ssz_static_test!(validator, Validator); ssz_static_test!(voluntary_exit, VoluntaryExit); + + #[test] + fn attestation() { + SszStaticHandler::, MinimalEthSpec>::pre_electra().run(); + SszStaticHandler::, MainnetEthSpec>::pre_electra().run(); + SszStaticHandler::, MinimalEthSpec>::electra_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::electra_only() + .run(); + } + + #[test] + fn attester_slashing() { + SszStaticHandler::, MinimalEthSpec>::pre_electra() + .run(); + SszStaticHandler::, MainnetEthSpec>::pre_electra() + .run(); + SszStaticHandler::, MinimalEthSpec>::electra_only() + .run(); + SszStaticHandler::, MainnetEthSpec>::electra_only() + .run(); + } + + #[test] + fn signed_aggregate_and_proof() { + SszStaticHandler::, MinimalEthSpec>::pre_electra( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::pre_electra( + ) + .run(); + SszStaticHandler::, MinimalEthSpec>::electra_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::electra_only( + ) + .run(); + } + + #[test] + fn aggregate_and_proof() { + SszStaticHandler::, MinimalEthSpec>::pre_electra() + .run(); + SszStaticHandler::, MainnetEthSpec>::pre_electra() + .run(); + SszStaticHandler::, MinimalEthSpec>::electra_only( + ) + .run(); + SszStaticHandler::, MainnetEthSpec>::electra_only( + ) + .run(); + } + // BeaconBlockBody has no internal indicator of which fork it is for, so we test it separately. #[test] fn beacon_block_body() { diff --git a/testing/web3signer_tests/src/lib.rs b/testing/web3signer_tests/src/lib.rs index 911704e751..4187844cec 100644 --- a/testing/web3signer_tests/src/lib.rs +++ b/testing/web3signer_tests/src/lib.rs @@ -39,7 +39,7 @@ mod tests { use tempfile::{tempdir, TempDir}; use tokio::sync::OnceCell; use tokio::time::sleep; - use types::*; + use types::{attestation::AttestationBase, *}; use url::Url; use validator_client::{ initialized_validators::{ @@ -542,7 +542,7 @@ mod tests { /// Get a generic, arbitrary attestation for signing. fn get_attestation() -> Attestation { - Attestation { + Attestation::Base(AttestationBase { aggregation_bits: BitList::with_capacity(1).unwrap(), data: AttestationData { slot: <_>::default(), @@ -558,7 +558,7 @@ mod tests { }, }, signature: AggregateSignature::empty(), - } + }) } fn get_validator_registration(pubkey: PublicKeyBytes) -> ValidatorRegistrationData { @@ -778,28 +778,28 @@ mod tests { let first_attestation = || { let mut attestation = get_attestation(); - attestation.data.source.epoch = Epoch::new(1); - attestation.data.target.epoch = Epoch::new(4); + attestation.data_mut().source.epoch = Epoch::new(1); + attestation.data_mut().target.epoch = Epoch::new(4); attestation }; let double_vote_attestation = || { let mut attestation = first_attestation(); - attestation.data.beacon_block_root = Hash256::from_low_u64_be(1); + attestation.data_mut().beacon_block_root = Hash256::from_low_u64_be(1); attestation }; let surrounding_attestation = || { let mut attestation = first_attestation(); - attestation.data.source.epoch = Epoch::new(0); - attestation.data.target.epoch = Epoch::new(5); + attestation.data_mut().source.epoch = Epoch::new(0); + attestation.data_mut().target.epoch = Epoch::new(5); attestation }; let surrounded_attestation = || { let mut attestation = first_attestation(); - attestation.data.source.epoch = Epoch::new(2); - attestation.data.target.epoch = Epoch::new(3); + attestation.data_mut().source.epoch = Epoch::new(2); + attestation.data_mut().target.epoch = Epoch::new(3); attestation }; diff --git a/validator_client/src/attestation_service.rs b/validator_client/src/attestation_service.rs index 1c6b60addb..0cff39546d 100644 --- a/validator_client/src/attestation_service.rs +++ b/validator_client/src/attestation_service.rs @@ -14,10 +14,7 @@ use std::ops::Deref; use std::sync::Arc; use tokio::time::{sleep, sleep_until, Duration, Instant}; use tree_hash::TreeHash; -use types::{ - AggregateSignature, Attestation, AttestationData, BitList, ChainSpec, CommitteeIndex, EthSpec, - Slot, -}; +use types::{Attestation, AttestationData, ChainSpec, CommitteeIndex, EthSpec, Slot}; /// Builds an `AttestationService`. pub struct AttestationServiceBuilder { @@ -363,9 +360,7 @@ impl AttestationService { let attestation_data = attestation_data_ref; // Ensure that the attestation matches the duties. - #[allow(clippy::suspicious_operation_groupings)] - if duty.slot != attestation_data.slot || duty.committee_index != attestation_data.index - { + if !duty.match_attestation_data::(attestation_data, &self.context.eth2_config.spec) { crit!( log, "Inconsistent validator duties during signing"; @@ -378,10 +373,26 @@ impl AttestationService { return None; } - let mut attestation = Attestation { - aggregation_bits: BitList::with_capacity(duty.committee_length as usize).unwrap(), - data: attestation_data.clone(), - signature: AggregateSignature::infinity(), + let mut attestation = match Attestation::::empty_for_signing( + duty.committee_index, + duty.committee_length as usize, + attestation_data.slot, + attestation_data.beacon_block_root, + attestation_data.source, + attestation_data.target, + &self.context.eth2_config.spec, + ) { + Ok(attestation) => attestation, + Err(err) => { + crit!( + log, + "Invalid validator duties during signing"; + "validator" => ?duty.pubkey, + "duty" => ?duty, + "err" => ?err, + ); + return None; + } }; match self @@ -536,10 +547,7 @@ impl AttestationService { let duty = &duty_and_proof.duty; let selection_proof = duty_and_proof.selection_proof.as_ref()?; - let slot = attestation_data.slot; - let committee_index = attestation_data.index; - - if duty.slot != slot || duty.committee_index != committee_index { + if !duty.match_attestation_data::(attestation_data, &self.context.eth2_config.spec) { crit!(log, "Inconsistent validator duties during signing"); return None; } @@ -605,29 +613,29 @@ impl AttestationService { { Ok(()) => { for signed_aggregate_and_proof in signed_aggregate_and_proofs { - let attestation = &signed_aggregate_and_proof.message.aggregate; + let attestation = signed_aggregate_and_proof.message().aggregate(); info!( log, "Successfully published attestation"; - "aggregator" => signed_aggregate_and_proof.message.aggregator_index, - "signatures" => attestation.aggregation_bits.num_set_bits(), - "head_block" => format!("{:?}", attestation.data.beacon_block_root), - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), + "aggregator" => signed_aggregate_and_proof.message().aggregator_index(), + "signatures" => attestation.num_set_aggregation_bits(), + "head_block" => format!("{:?}", attestation.data().beacon_block_root), + "committee_index" => attestation.committee_index(), + "slot" => attestation.data().slot.as_u64(), "type" => "aggregated", ); } } Err(e) => { for signed_aggregate_and_proof in signed_aggregate_and_proofs { - let attestation = &signed_aggregate_and_proof.message.aggregate; + let attestation = &signed_aggregate_and_proof.message().aggregate(); crit!( log, "Failed to publish attestation"; "error" => %e, - "aggregator" => signed_aggregate_and_proof.message.aggregator_index, - "committee_index" => attestation.data.index, - "slot" => attestation.data.slot.as_u64(), + "aggregator" => signed_aggregate_and_proof.message().aggregator_index(), + "committee_index" => attestation.committee_index(), + "slot" => attestation.data().slot.as_u64(), "type" => "aggregated", ); } diff --git a/validator_client/src/block_service.rs b/validator_client/src/block_service.rs index 06d484a52b..cb3636135c 100644 --- a/validator_client/src/block_service.rs +++ b/validator_client/src/block_service.rs @@ -919,8 +919,8 @@ impl SignedBlock { } pub fn num_attestations(&self) -> usize { match self { - SignedBlock::Full(block) => block.signed_block().message().body().attestations().len(), - SignedBlock::Blinded(block) => block.message().body().attestations().len(), + SignedBlock::Full(block) => block.signed_block().message().body().attestations_len(), + SignedBlock::Blinded(block) => block.message().body().attestations_len(), } } } diff --git a/validator_client/src/http_api/tests/keystores.rs b/validator_client/src/http_api/tests/keystores.rs index fe58393bb8..b6923d1c78 100644 --- a/validator_client/src/http_api/tests/keystores.rs +++ b/validator_client/src/http_api/tests/keystores.rs @@ -13,7 +13,7 @@ use rand::{rngs::SmallRng, Rng, SeedableRng}; use slashing_protection::interchange::{Interchange, InterchangeMetadata}; use std::{collections::HashMap, path::Path}; use tokio::runtime::Handle; -use types::Address; +use types::{attestation::AttestationBase, Address}; fn new_keystore(password: ZeroizeString) -> Keystore { let keypair = Keypair::random(); @@ -1094,7 +1094,7 @@ async fn generic_migration_test( // Sign attestations on VC1. for (validator_index, mut attestation) in first_vc_attestations { let public_key = keystore_pubkey(&keystores[validator_index]); - let current_epoch = attestation.data.target.epoch; + let current_epoch = attestation.data().target.epoch; tester1 .validator_store .sign_attestation(public_key, 0, &mut attestation, current_epoch) @@ -1170,7 +1170,7 @@ async fn generic_migration_test( // Sign attestations on the second VC. for (validator_index, mut attestation, should_succeed) in second_vc_attestations { let public_key = keystore_pubkey(&keystores[validator_index]); - let current_epoch = attestation.data.target.epoch; + let current_epoch = attestation.data().target.epoch; match tester2 .validator_store .sign_attestation(public_key, 0, &mut attestation, current_epoch) @@ -1236,7 +1236,7 @@ async fn delete_nonexistent_keystores() { } fn make_attestation(source_epoch: u64, target_epoch: u64) -> Attestation { - Attestation { + Attestation::Base(AttestationBase { aggregation_bits: BitList::with_capacity( ::MaxValidatorsPerCommittee::to_usize(), ) @@ -1253,7 +1253,7 @@ fn make_attestation(source_epoch: u64, target_epoch: u64) -> Attestation { ..AttestationData::default() }, signature: AggregateSignature::empty(), - } + }) } #[tokio::test] diff --git a/validator_client/src/signing_method.rs b/validator_client/src/signing_method.rs index 8c11027b8d..d89c9b8229 100644 --- a/validator_client/src/signing_method.rs +++ b/validator_client/src/signing_method.rs @@ -38,7 +38,7 @@ pub enum SignableMessage<'a, E: EthSpec, Payload: AbstractExecPayload = FullP RandaoReveal(Epoch), BeaconBlock(&'a BeaconBlock), AttestationData(&'a AttestationData), - SignedAggregateAndProof(&'a AggregateAndProof), + SignedAggregateAndProof(AggregateAndProofRef<'a, E>), SelectionProof(Slot), SyncSelectionProof(&'a SyncAggregatorSelectionData), SyncCommitteeSignature { diff --git a/validator_client/src/signing_method/web3signer.rs b/validator_client/src/signing_method/web3signer.rs index 8ad37a1620..86e7015ad3 100644 --- a/validator_client/src/signing_method/web3signer.rs +++ b/validator_client/src/signing_method/web3signer.rs @@ -43,7 +43,7 @@ pub enum Web3SignerObject<'a, E: EthSpec, Payload: AbstractExecPayload> { AggregationSlot { slot: Slot, }, - AggregateAndProof(&'a AggregateAndProof), + AggregateAndProof(AggregateAndProofRef<'a, E>), Attestation(&'a AttestationData), BeaconBlock { version: ForkName, diff --git a/validator_client/src/validator_store.rs b/validator_client/src/validator_store.rs index f3bdc2c0f6..e6e19b6e06 100644 --- a/validator_client/src/validator_store.rs +++ b/validator_client/src/validator_store.rs @@ -647,9 +647,9 @@ impl ValidatorStore { 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 { + if attestation.data().target.epoch > current_epoch { return Err(Error::GreaterThanCurrentEpoch { - epoch: attestation.data.target.epoch, + epoch: attestation.data().target.epoch, current_epoch, }); } @@ -658,7 +658,7 @@ impl ValidatorStore { let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; // Checking for slashing conditions. - let signing_epoch = attestation.data.target.epoch; + 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 = if signing_method @@ -666,7 +666,7 @@ impl ValidatorStore { { self.slashing_protection.check_and_insert_attestation( &validator_pubkey, - &attestation.data, + attestation.data(), domain_hash, ) } else { @@ -678,7 +678,7 @@ impl ValidatorStore { Ok(Safe::Valid) => { let signature = signing_method .get_signature::>( - SignableMessage::AttestationData(&attestation.data), + SignableMessage::AttestationData(attestation.data()), signing_context, &self.spec, &self.task_executor, @@ -720,7 +720,7 @@ impl ValidatorStore { crit!( self.log, "Not signing slashable attestation"; - "attestation" => format!("{:?}", attestation.data), + "attestation" => format!("{:?}", attestation.data()), "error" => format!("{:?}", e) ); metrics::inc_counter_vec( @@ -798,19 +798,16 @@ impl ValidatorStore { aggregate: Attestation, selection_proof: SelectionProof, ) -> Result, Error> { - let signing_epoch = aggregate.data.target.epoch; + 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 message = + AggregateAndProof::from_attestation(aggregator_index, aggregate, selection_proof); let signing_method = self.doppelganger_checked_signing_method(validator_pubkey)?; let signature = signing_method .get_signature::>( - SignableMessage::SignedAggregateAndProof(&message), + SignableMessage::SignedAggregateAndProof(message.to_ref()), signing_context, &self.spec, &self.task_executor, @@ -819,7 +816,9 @@ impl ValidatorStore { metrics::inc_counter_vec(&metrics::SIGNED_AGGREGATES_TOTAL, &[metrics::SUCCESS]); - Ok(SignedAggregateAndProof { message, signature }) + Ok(SignedAggregateAndProof::from_aggregate_and_proof( + message, signature, + )) } /// Produces a `SelectionProof` for the `slot`, signed by with corresponding secret key to diff --git a/watch/src/database/mod.rs b/watch/src/database/mod.rs index 315fcbc835..b31583c629 100644 --- a/watch/src/database/mod.rs +++ b/watch/src/database/mod.rs @@ -141,7 +141,7 @@ pub fn insert_beacon_block( let parent_root = WatchHash::from_hash(block.parent_root()); let proposer_index = block_message.proposer_index() as i32; let graffiti = block_message.body().graffiti().as_utf8_lossy(); - let attestation_count = block_message.body().attestations().len() as i32; + let attestation_count = block_message.body().attestations_len() as i32; let full_payload = block_message.execution_payload().ok();