From 52bb1840ae5c4356a8fc3a51e5df23ed65ed2c7f Mon Sep 17 00:00:00 2001 From: Michael Sproul Date: Fri, 19 Aug 2022 18:00:52 +1000 Subject: [PATCH] Fix handling of cross-fork messages in op pool --- Cargo.lock | 3 + beacon_node/beacon_chain/src/beacon_chain.rs | 20 +- .../beacon_chain/src/observed_operations.rs | 9 +- .../src/schema_change/migration_schema_v12.rs | 13 +- beacon_node/beacon_chain/src/test_utils.rs | 31 +- beacon_node/operation_pool/Cargo.toml | 1 + beacon_node/operation_pool/src/lib.rs | 539 +++++++++++++----- beacon_node/operation_pool/src/persistence.rs | 41 +- consensus/state_processing/Cargo.toml | 2 + .../state_processing/src/verify_operation.rs | 126 +++- consensus/types/src/proposer_slashing.rs | 7 + 11 files changed, 593 insertions(+), 199 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e6a157323..91778cb2c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4394,6 +4394,7 @@ dependencies = [ "itertools", "lazy_static", "lighthouse_metrics", + "maplit", "parking_lot 0.12.1", "rayon", "serde", @@ -6246,9 +6247,11 @@ dependencies = [ "arbitrary", "beacon_chain", "bls", + "derivative", "env_logger 0.9.0", "eth2_hashing", "eth2_ssz", + "eth2_ssz_derive", "eth2_ssz_types", "int_to_bytes", "integer-sqrt", diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index aa2aebddda..4ce8395a2f 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -2041,7 +2041,7 @@ impl BeaconChain { pub fn verify_voluntary_exit_for_gossip( &self, exit: SignedVoluntaryExit, - ) -> Result, Error> { + ) -> Result, Error> { // NOTE: this could be more efficient if it avoided cloning the head state let wall_clock_state = self.wall_clock_state()?; Ok(self @@ -2062,7 +2062,7 @@ impl BeaconChain { } /// Accept a pre-verified exit and queue it for inclusion in an appropriate block. - pub fn import_voluntary_exit(&self, exit: SigVerifiedOp) { + pub fn import_voluntary_exit(&self, exit: SigVerifiedOp) { if self.eth1_chain.is_some() { self.op_pool.insert_voluntary_exit(exit) } @@ -2072,7 +2072,7 @@ impl BeaconChain { pub fn verify_proposer_slashing_for_gossip( &self, proposer_slashing: ProposerSlashing, - ) -> Result, Error> { + ) -> Result, Error> { let wall_clock_state = self.wall_clock_state()?; Ok(self.observed_proposer_slashings.lock().verify_and_observe( proposer_slashing, @@ -2082,7 +2082,10 @@ impl BeaconChain { } /// Accept some proposer slashing and queue it for inclusion in an appropriate block. - pub fn import_proposer_slashing(&self, proposer_slashing: SigVerifiedOp) { + pub fn import_proposer_slashing( + &self, + proposer_slashing: SigVerifiedOp, + ) { if self.eth1_chain.is_some() { self.op_pool.insert_proposer_slashing(proposer_slashing) } @@ -2092,7 +2095,7 @@ impl BeaconChain { pub fn verify_attester_slashing_for_gossip( &self, attester_slashing: AttesterSlashing, - ) -> Result>, Error> { + ) -> Result, T::EthSpec>, Error> { let wall_clock_state = self.wall_clock_state()?; Ok(self.observed_attester_slashings.lock().verify_and_observe( attester_slashing, @@ -2107,7 +2110,7 @@ impl BeaconChain { /// 2. Add it to the op pool. pub fn import_attester_slashing( &self, - attester_slashing: SigVerifiedOp>, + attester_slashing: SigVerifiedOp, T::EthSpec>, ) { // Add to fork choice. self.canonical_head @@ -2116,10 +2119,7 @@ impl BeaconChain { // Add to the op pool (if we have the ability to propose blocks). if self.eth1_chain.is_some() { - self.op_pool.insert_attester_slashing( - attester_slashing, - self.canonical_head.cached_head().head_fork(), - ) + self.op_pool.insert_attester_slashing(attester_slashing) } } diff --git a/beacon_node/beacon_chain/src/observed_operations.rs b/beacon_node/beacon_chain/src/observed_operations.rs index f1eb996a54..2c48b253f4 100644 --- a/beacon_node/beacon_chain/src/observed_operations.rs +++ b/beacon_node/beacon_chain/src/observed_operations.rs @@ -1,5 +1,6 @@ use derivative::Derivative; use smallvec::SmallVec; +use ssz::{Decode, Encode}; use state_processing::{SigVerifiedOp, VerifyOperation}; use std::collections::HashSet; use std::marker::PhantomData; @@ -29,8 +30,8 @@ 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 { - New(SigVerifiedOp), +pub enum ObservationOutcome { + New(SigVerifiedOp), AlreadyKnown, } @@ -81,7 +82,7 @@ impl, E: EthSpec> ObservedOperations { op: T, head_state: &BeaconState, spec: &ChainSpec, - ) -> Result, T::Error> { + ) -> Result, T::Error> { let observed_validator_indices = &mut self.observed_validator_indices; let new_validator_indices = op.observed_validators(); @@ -95,6 +96,8 @@ impl, E: EthSpec> ObservedOperations { .iter() .all(|index| observed_validator_indices.contains(index)) { + // FIXME(sproul): consider verifying that those already-observed slashings are + // still valid. return Ok(ObservationOutcome::AlreadyKnown); } diff --git a/beacon_node/beacon_chain/src/schema_change/migration_schema_v12.rs b/beacon_node/beacon_chain/src/schema_change/migration_schema_v12.rs index ab37b457ca..1fc3687804 100644 --- a/beacon_node/beacon_chain/src/schema_change/migration_schema_v12.rs +++ b/beacon_node/beacon_chain/src/schema_change/migration_schema_v12.rs @@ -23,12 +23,13 @@ pub fn upgrade_to_v12( "count" => v5.attestations_v5.len(), ); + // FIXME(sproul): actually transfer the operations across let v12 = PersistedOperationPool::V12(PersistedOperationPoolV12 { attestations: vec![], sync_contributions: v5.sync_contributions, - attester_slashings: v5.attester_slashings, - proposer_slashings: v5.proposer_slashings, - voluntary_exits: v5.voluntary_exits, + attester_slashings: vec![], + proposer_slashings: vec![], + voluntary_exits: vec![], }); Ok(vec![v12.as_kv_store_op(OP_POOL_DB_KEY)]) } @@ -55,9 +56,9 @@ pub fn downgrade_from_v12( let v5 = PersistedOperationPoolV5 { attestations_v5: vec![], sync_contributions: v12.sync_contributions, - attester_slashings: v12.attester_slashings, - proposer_slashings: v12.proposer_slashings, - voluntary_exits: v12.voluntary_exits, + attester_slashings_v5: vec![], + proposer_slashings_v5: vec![], + voluntary_exits_v5: vec![], }; Ok(vec![v5.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 54a195a8e5..a62608202e 100644 --- a/beacon_node/beacon_chain/src/test_utils.rs +++ b/beacon_node/beacon_chain/src/test_utils.rs @@ -1175,6 +1175,19 @@ where } pub fn make_attester_slashing(&self, validator_indices: Vec) -> AttesterSlashing { + self.make_attester_slashing_with_epochs(validator_indices, None, None, None, None) + } + + pub fn make_attester_slashing_with_epochs( + &self, + validator_indices: Vec, + source1: Option, + target1: Option, + source2: Option, + target2: Option, + ) -> 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 { @@ -1183,11 +1196,11 @@ where beacon_block_root: Hash256::zero(), target: Checkpoint { root: Hash256::zero(), - epoch: Epoch::new(0), + epoch: target1.unwrap_or(fork.epoch), }, source: Checkpoint { root: Hash256::zero(), - epoch: Epoch::new(0), + epoch: source1.unwrap_or(Epoch::new(0)), }, }, signature: AggregateSignature::infinity(), @@ -1195,8 +1208,9 @@ where 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); - 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; @@ -1280,8 +1294,19 @@ where } pub fn make_proposer_slashing(&self, validator_index: u64) -> ProposerSlashing { + self.make_proposer_slashing_at_slot(validator_index, None) + } + + pub fn make_proposer_slashing_at_slot( + &self, + validator_index: u64, + slot_override: Option, + ) -> ProposerSlashing { let mut block_header_1 = self.chain.head_beacon_block().message().block_header(); block_header_1.proposer_index = validator_index; + if let Some(slot) = slot_override { + block_header_1.slot = slot; + } let mut block_header_2 = block_header_1.clone(); block_header_2.state_root = Hash256::zero(); diff --git a/beacon_node/operation_pool/Cargo.toml b/beacon_node/operation_pool/Cargo.toml index edb0df22b0..1d67ecdccc 100644 --- a/beacon_node/operation_pool/Cargo.toml +++ b/beacon_node/operation_pool/Cargo.toml @@ -23,3 +23,4 @@ bitvec = "1" [dev-dependencies] beacon_chain = { path = "../beacon_chain" } tokio = { version = "1.14.0", features = ["rt-multi-thread"] } +maplit = "1.0.2" diff --git a/beacon_node/operation_pool/src/lib.rs b/beacon_node/operation_pool/src/lib.rs index 821a77c004..837cb83a51 100644 --- a/beacon_node/operation_pool/src/lib.rs +++ b/beacon_node/operation_pool/src/lib.rs @@ -25,15 +25,14 @@ use state_processing::per_block_processing::errors::AttestationValidationError; use state_processing::per_block_processing::{ get_slashable_indices_modular, verify_exit, VerifySignatures, }; -use state_processing::SigVerifiedOp; +use state_processing::{SigVerifiedOp, VerifyOperation}; use std::collections::{hash_map::Entry, HashMap, HashSet}; use std::marker::PhantomData; use std::ptr; use types::{ sync_aggregate::Error as SyncAggregateError, typenum::Unsigned, Attestation, AttestationData, - AttesterSlashing, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, - ProposerSlashing, SignedVoluntaryExit, Slot, SyncAggregate, SyncCommitteeContribution, - Validator, + AttesterSlashing, BeaconState, BeaconStateError, ChainSpec, Epoch, EthSpec, ProposerSlashing, + SignedVoluntaryExit, Slot, SyncAggregate, SyncCommitteeContribution, Validator, }; type SyncContributions = RwLock>>>; @@ -45,11 +44,11 @@ pub struct OperationPool { /// Map from sync aggregate ID to the best `SyncCommitteeContribution`s seen for that ID. sync_contributions: SyncContributions, /// Set of attester slashings, and the fork version they were verified against. - attester_slashings: RwLock, ForkVersion)>>, + attester_slashings: RwLock, T>>>, /// Map from proposer index to slashing. - proposer_slashings: RwLock>, + proposer_slashings: RwLock>>, /// Map from exiting validator to their exit data. - voluntary_exits: RwLock>, + voluntary_exits: RwLock>>, /// Reward cache for accelerating attestation packing. reward_cache: RwLock, _phantom: PhantomData, @@ -335,23 +334,20 @@ impl OperationPool { /// Insert a proposer slashing into the pool. pub fn insert_proposer_slashing( &self, - verified_proposer_slashing: SigVerifiedOp, + verified_proposer_slashing: SigVerifiedOp, ) { - let slashing = verified_proposer_slashing.into_inner(); - self.proposer_slashings - .write() - .insert(slashing.signed_header_1.message.proposer_index, slashing); + self.proposer_slashings.write().insert( + verified_proposer_slashing.as_inner().proposer_index(), + verified_proposer_slashing, + ); } /// Insert an attester slashing into the pool. pub fn insert_attester_slashing( &self, - verified_slashing: SigVerifiedOp>, - fork: Fork, + verified_slashing: SigVerifiedOp, T>, ) { - self.attester_slashings - .write() - .insert((verified_slashing.into_inner(), fork.current_version)); + self.attester_slashings.write().insert(verified_slashing); } /// Get proposer and attester slashings for inclusion in a block. @@ -371,11 +367,13 @@ impl OperationPool { let proposer_slashings = filter_limit_operations( self.proposer_slashings.read().values(), |slashing| { - state - .validators() - .get(slashing.signed_header_1.message.proposer_index as usize) - .map_or(false, |validator| !validator.slashed) + slashing.signature_is_still_valid(&state.fork()) + && state + .validators() + .get(slashing.as_inner().signed_header_1.message.proposer_index as usize) + .map_or(false, |validator| !validator.slashed) }, + |slashing| slashing.as_inner().clone(), T::MaxProposerSlashings::to_usize(), ); @@ -383,20 +381,39 @@ impl OperationPool { // slashings. let mut to_be_slashed = proposer_slashings .iter() - .map(|s| s.signed_header_1.message.proposer_index) - .collect::>(); + .map(|s| s.proposer_index()) + .collect(); + let attester_slashings = self.get_attester_slashings(state, &mut to_be_slashed); + + let voluntary_exits = self.get_voluntary_exits( + state, + |exit| !to_be_slashed.contains(&exit.message.validator_index), + spec, + ); + + (proposer_slashings, attester_slashings, voluntary_exits) + } + + /// Get attester slashings taking into account already slashed validators. + /// + /// This function *must* remain private. + fn get_attester_slashings( + &self, + state: &BeaconState, + to_be_slashed: &mut HashSet, + ) -> Vec> { let reader = self.attester_slashings.read(); - let relevant_attester_slashings = reader.iter().flat_map(|(slashing, fork)| { - if *fork == state.fork().previous_version || *fork == state.fork().current_version { - AttesterSlashingMaxCover::new(slashing, &to_be_slashed, state) + 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) } else { None } }); - let attester_slashings = maximum_cover( + maximum_cover( relevant_attester_slashings, T::MaxAttesterSlashings::to_usize(), "attester_slashings", @@ -406,15 +423,7 @@ impl OperationPool { to_be_slashed.extend(cover.covering_set().keys()); cover.intermediate().clone() }) - .collect(); - - let voluntary_exits = self.get_voluntary_exits( - state, - |exit| !to_be_slashed.contains(&exit.message.validator_index), - spec, - ); - - (proposer_slashings, attester_slashings, voluntary_exits) + .collect() } /// Prune proposer slashings for validators which are exited in the finalized epoch. @@ -429,30 +438,23 @@ impl OperationPool { /// Prune attester slashings for all slashed or withdrawn validators, or attestations on another /// fork. pub fn prune_attester_slashings(&self, head_state: &BeaconState) { - self.attester_slashings - .write() - .retain(|(slashing, fork_version)| { - let previous_fork_is_finalized = - head_state.finalized_checkpoint().epoch >= head_state.fork().epoch; - // Prune any slashings which don't match the current fork version, or the previous - // fork version if it is not finalized yet. - let fork_ok = (*fork_version == head_state.fork().current_version) - || (*fork_version == head_state.fork().previous_version - && !previous_fork_is_finalized); - // Slashings that don't slash any validators can also be dropped. - let slashing_ok = - get_slashable_indices_modular(head_state, slashing, |_, 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()); + self.attester_slashings.write().retain(|slashing| { + // 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| { + // 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()); - fork_ok && slashing_ok - }); + signature_ok && slashing_ok + }); } /// Total number of attester slashings in the pool. @@ -466,11 +468,10 @@ impl OperationPool { } /// Insert a voluntary exit that has previously been checked elsewhere. - pub fn insert_voluntary_exit(&self, verified_exit: SigVerifiedOp) { - let exit = verified_exit.into_inner(); + pub fn insert_voluntary_exit(&self, exit: SigVerifiedOp) { self.voluntary_exits .write() - .insert(exit.message.validator_index, exit); + .insert(exit.as_inner().message.validator_index, exit); } /// Get a list of voluntary exits for inclusion in a block. @@ -485,7 +486,12 @@ impl OperationPool { { filter_limit_operations( self.voluntary_exits.read().values(), - |exit| filter(exit) && verify_exit(state, exit, VerifySignatures::False, spec).is_ok(), + |exit| { + filter(exit.as_inner()) + && exit.signature_is_still_valid(&state.fork()) + && verify_exit(state, exit.as_inner(), VerifySignatures::False, spec).is_ok() + }, + |exit| exit.as_inner().clone(), T::MaxVoluntaryExits::to_usize(), ) } @@ -551,7 +557,7 @@ impl OperationPool { self.attester_slashings .read() .iter() - .map(|(slashing, _)| slashing.clone()) + .map(|slashing| slashing.as_inner().clone()) .collect() } @@ -562,7 +568,7 @@ impl OperationPool { self.proposer_slashings .read() .iter() - .map(|(_, slashing)| slashing.clone()) + .map(|(_, slashing)| slashing.as_inner().clone()) .collect() } @@ -573,23 +579,29 @@ impl OperationPool { self.voluntary_exits .read() .iter() - .map(|(_, exit)| exit.clone()) + .map(|(_, exit)| exit.as_inner().clone()) .collect() } } /// Filter up to a maximum number of operations out of an iterator. -fn filter_limit_operations<'a, T: 'a, I, F>(operations: I, filter: F, limit: usize) -> Vec +fn filter_limit_operations<'a, T: 'a, V: 'a, I, F, G>( + operations: I, + filter: F, + mapping: G, + limit: usize, +) -> Vec where I: IntoIterator, F: Fn(&T) -> bool, + G: Fn(&T) -> V, T: Clone, { operations .into_iter() .filter(|x| filter(*x)) .take(limit) - .cloned() + .map(mapping) .collect() } @@ -599,17 +611,19 @@ where /// in the state's validator registry and then passed to `prune_if`. /// Entries for unknown validators will be kept. fn prune_validator_hash_map( - map: &mut HashMap, + map: &mut HashMap>, prune_if: F, head_state: &BeaconState, ) where F: Fn(&Validator) -> bool, + T: VerifyOperation, { - map.retain(|&validator_index, _| { - head_state - .validators() - .get(validator_index as usize) - .map_or(true, |validator| !prune_if(validator)) + map.retain(|&validator_index, op| { + op.signature_is_still_valid(&head_state.fork()) + && head_state + .validators() + .get(validator_index as usize) + .map_or(true, |validator| !prune_if(validator)) }); } @@ -635,6 +649,7 @@ mod release_tests { test_spec, BeaconChainHarness, EphemeralHarnessType, RelativeSyncCommittee, }; use lazy_static::lazy_static; + use maplit::hashset; use state_processing::{common::get_attesting_indices_from_state, VerifyOperation}; use std::collections::BTreeSet; use types::consts::altair::SYNC_COMMITTEE_SUBNET_COUNT; @@ -655,6 +670,7 @@ mod release_tests { .spec_or_default(spec) .keypairs(KEYPAIRS[0..validator_count].to_vec()) .fresh_ephemeral_store() + .mock_execution_layer() .build(); harness.advance_slot(); @@ -1264,10 +1280,7 @@ mod release_tests { let op_pool = OperationPool::::new(); let slashing = harness.make_attester_slashing(vec![1, 3, 5, 7, 9]); - op_pool.insert_attester_slashing( - slashing.clone().validate(&state, spec).unwrap(), - state.fork(), - ); + op_pool.insert_attester_slashing(slashing.clone().validate(&state, spec).unwrap()); op_pool.prune_attester_slashings(&state); assert_eq!( op_pool.get_slashings_and_exits(&state, &harness.spec).1, @@ -1288,22 +1301,10 @@ mod release_tests { let slashing_3 = harness.make_attester_slashing(vec![4, 5, 6]); let slashing_4 = harness.make_attester_slashing(vec![7, 8, 9, 10]); - op_pool.insert_attester_slashing( - slashing_1.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_2.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_3.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_4.clone().validate(&state, spec).unwrap(), - state.fork(), - ); + op_pool.insert_attester_slashing(slashing_1.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_2.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_3.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_4.clone().validate(&state, spec).unwrap()); let best_slashings = op_pool.get_slashings_and_exits(&state, &harness.spec); assert_eq!(best_slashings.1, vec![slashing_4, slashing_3]); @@ -1322,22 +1323,10 @@ mod release_tests { let slashing_3 = harness.make_attester_slashing(vec![5, 6]); let slashing_4 = harness.make_attester_slashing(vec![6]); - op_pool.insert_attester_slashing( - slashing_1.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_2.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_3.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_4.clone().validate(&state, spec).unwrap(), - state.fork(), - ); + op_pool.insert_attester_slashing(slashing_1.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_2.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_3.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_4.clone().validate(&state, spec).unwrap()); let best_slashings = op_pool.get_slashings_and_exits(&state, &harness.spec); assert_eq!(best_slashings.1, vec![slashing_1, slashing_3]); @@ -1357,18 +1346,9 @@ mod release_tests { let a_slashing_3 = harness.make_attester_slashing(vec![5, 6]); op_pool.insert_proposer_slashing(p_slashing.clone().validate(&state, spec).unwrap()); - op_pool.insert_attester_slashing( - a_slashing_1.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - a_slashing_2.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - a_slashing_3.clone().validate(&state, spec).unwrap(), - state.fork(), - ); + op_pool.insert_attester_slashing(a_slashing_1.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(a_slashing_2.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(a_slashing_3.clone().validate(&state, spec).unwrap()); let best_slashings = op_pool.get_slashings_and_exits(&state, &harness.spec); assert_eq!(best_slashings.1, vec![a_slashing_1, a_slashing_3]); @@ -1389,18 +1369,9 @@ mod release_tests { let slashing_2 = harness.make_attester_slashing(vec![5, 6]); let slashing_3 = harness.make_attester_slashing(vec![1, 2, 3]); - op_pool.insert_attester_slashing( - slashing_1.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_2.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_3.clone().validate(&state, spec).unwrap(), - state.fork(), - ); + op_pool.insert_attester_slashing(slashing_1.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_2.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_3.clone().validate(&state, spec).unwrap()); let best_slashings = op_pool.get_slashings_and_exits(&state, &harness.spec); assert_eq!(best_slashings.1, vec![slashing_1, slashing_3]); @@ -1421,18 +1392,9 @@ mod release_tests { let slashing_2 = harness.make_attester_slashing(vec![4, 5, 6]); let slashing_3 = harness.make_attester_slashing(vec![7, 8]); - op_pool.insert_attester_slashing( - slashing_1.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_2.clone().validate(&state, spec).unwrap(), - state.fork(), - ); - op_pool.insert_attester_slashing( - slashing_3.clone().validate(&state, spec).unwrap(), - state.fork(), - ); + op_pool.insert_attester_slashing(slashing_1.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_2.clone().validate(&state, spec).unwrap()); + op_pool.insert_attester_slashing(slashing_3.clone().validate(&state, spec).unwrap()); let best_slashings = op_pool.get_slashings_and_exits(&state, &harness.spec); assert_eq!(best_slashings.1, vec![slashing_2, slashing_3]); @@ -1701,4 +1663,289 @@ mod release_tests { expected_bits ); } + + fn cross_fork_harness() -> (BeaconChainHarness>, ChainSpec) + { + let mut spec = test_spec::(); + + // Give some room to sign surround slashings. + spec.altair_fork_epoch = Some(Epoch::new(3)); + spec.bellatrix_fork_epoch = Some(Epoch::new(6)); + + // To make exits immediately valid. + spec.shard_committee_period = 0; + + let num_validators = 32; + + let harness = get_harness::(num_validators, Some(spec.clone())); + (harness, spec) + } + + /// Test several cross-fork voluntary exits: + /// + /// - phase0 exit (not valid after Bellatrix) + /// - phase0 exit signed with Altair fork version (only valid after Bellatrix) + #[tokio::test] + async fn cross_fork_exits() { + let (harness, spec) = cross_fork_harness::(); + let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); + let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); + let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); + + let op_pool = OperationPool::::new(); + + // Sign an exit in phase0 with a phase0 epoch. + let exit1 = harness.make_voluntary_exit(0, Epoch::new(0)); + + // Advance to Altair. + harness + .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) + .await; + let altair_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); + + // Add exit 1 to the op pool during Altair. It's still valid at this point and should be + // returned. + let verified_exit1 = exit1 + .clone() + .validate(&altair_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_voluntary_exit(verified_exit1); + let exits = + op_pool.get_voluntary_exits(&altair_head.beacon_state, |_| true, &harness.chain.spec); + assert!(exits.contains(&exit1)); + assert_eq!(exits.len(), 1); + + // Advance to Bellatrix. + harness + .extend_to_slot(bellatrix_fork_epoch.start_slot(slots_per_epoch)) + .await; + let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!( + bellatrix_head.beacon_state.current_epoch(), + bellatrix_fork_epoch + ); + + // Sign an exit with the Altair domain and a phase0 epoch. This is a weird type of exit + // that is valid because after the Bellatrix fork we'll use the Altair fork domain to verify + // all prior epochs. + let exit2 = harness.make_voluntary_exit(2, Epoch::new(0)); + let verified_exit2 = exit2 + .clone() + .validate(&bellatrix_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_voluntary_exit(verified_exit2); + + // Attempting to fetch exit1 now should fail, despite it still being in the pool. + // exit2 should still be valid, because it was signed with the Altair fork domain. + assert_eq!(op_pool.voluntary_exits.read().len(), 2); + let exits = + op_pool.get_voluntary_exits(&bellatrix_head.beacon_state, |_| true, &harness.spec); + assert_eq!(&exits, &[exit2]); + } + + /// Test several cross-fork proposer slashings: + /// + /// - phase0 slashing (not valid after Bellatrix) + /// - Bellatrix signed with Altair fork version (not valid after Bellatrix) + /// - phase0 exit signed with Altair fork version (only valid after Bellatrix) + #[tokio::test] + async fn cross_fork_proposer_slashings() { + let (harness, spec) = cross_fork_harness::(); + let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); + let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); + let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); + let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(slots_per_epoch); + + let op_pool = OperationPool::::new(); + + // Sign a proposer slashing in phase0 with a phase0 epoch. + let slashing1 = harness.make_proposer_slashing_at_slot(0, Some(Slot::new(1))); + + // Advance to Altair. + harness + .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) + .await; + let altair_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); + + // Add slashing1 to the op pool during Altair. It's still valid at this point and should be + // returned. + let verified_slashing1 = slashing1 + .clone() + .validate(&altair_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_proposer_slashing(verified_slashing1); + let (proposer_slashings, _, _) = + op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + assert!(proposer_slashings.contains(&slashing1)); + assert_eq!(proposer_slashings.len(), 1); + + // Sign a proposer slashing with a Bellatrix slot using the Altair fork domain. + // + // This slashing is valid only before the Bellatrix fork epoch. + let slashing2 = harness.make_proposer_slashing_at_slot(1, Some(bellatrix_fork_slot)); + let verified_slashing2 = slashing2 + .clone() + .validate(&altair_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_proposer_slashing(verified_slashing2); + let (proposer_slashings, _, _) = + op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + assert!(proposer_slashings.contains(&slashing1)); + assert!(proposer_slashings.contains(&slashing2)); + assert_eq!(proposer_slashings.len(), 2); + + // Advance to Bellatrix. + harness.extend_to_slot(bellatrix_fork_slot).await; + let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!( + bellatrix_head.beacon_state.current_epoch(), + bellatrix_fork_epoch + ); + + // Sign a proposer slashing with the Altair domain and a phase0 slot. This is a weird type + // of slashing that is only valid after the Bellatrix fork because we'll use the Altair fork + // domain to verify all prior epochs. + let slashing3 = harness.make_proposer_slashing_at_slot(2, Some(Slot::new(1))); + let verified_slashing3 = slashing3 + .clone() + .validate(&bellatrix_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_proposer_slashing(verified_slashing3); + + // Attempting to fetch slashing1 now should fail, despite it still being in the pool. + // Likewise slashing2 is also invalid now because it should be signed with the + // Bellatrix fork version. + // slashing3 should still be valid, because it was signed with the Altair fork domain. + assert_eq!(op_pool.proposer_slashings.read().len(), 3); + let (proposer_slashings, _, _) = + op_pool.get_slashings_and_exits(&bellatrix_head.beacon_state, &harness.spec); + assert!(proposer_slashings.contains(&slashing3)); + assert_eq!(proposer_slashings.len(), 1); + } + + /// Test several cross-fork attester slashings: + /// + /// - both target epochs in phase0 (not valid after Bellatrix) + /// - both target epochs in Bellatrix but signed with Altair domain (not valid after Bellatrix) + /// - Altair attestation that surrounds a phase0 attestation (not valid after Bellatrix) + /// - both target epochs in phase0 but signed with Altair domain (only valid after Bellatrix) + #[tokio::test] + async fn cross_fork_attester_slashings() { + let (harness, spec) = cross_fork_harness::(); + let slots_per_epoch = MainnetEthSpec::slots_per_epoch(); + let zero_epoch = Epoch::new(0); + let altair_fork_epoch = spec.altair_fork_epoch.unwrap(); + let bellatrix_fork_epoch = spec.bellatrix_fork_epoch.unwrap(); + let bellatrix_fork_slot = bellatrix_fork_epoch.start_slot(slots_per_epoch); + + let op_pool = OperationPool::::new(); + + // Sign an attester slashing with the phase0 fork version, with both target epochs in phase0. + let slashing1 = harness.make_attester_slashing_with_epochs( + vec![0], + None, + Some(zero_epoch), + None, + Some(zero_epoch), + ); + + // Advance to Altair. + harness + .extend_to_slot(altair_fork_epoch.start_slot(slots_per_epoch)) + .await; + let altair_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!(altair_head.beacon_state.current_epoch(), altair_fork_epoch); + + // Add slashing1 to the op pool during Altair. It's still valid at this point and should be + // returned. + let verified_slashing1 = slashing1 + .clone() + .validate(&altair_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_attester_slashing(verified_slashing1); + + // Sign an attester slashing with two Bellatrix epochs using the Altair fork domain. + // + // This slashing is valid only before the Bellatrix fork epoch. + let slashing2 = harness.make_attester_slashing_with_epochs( + vec![1], + None, + Some(bellatrix_fork_epoch), + None, + Some(bellatrix_fork_epoch), + ); + let verified_slashing2 = slashing2 + .clone() + .validate(&altair_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_attester_slashing(verified_slashing2); + let (_, attester_slashings, _) = + op_pool.get_slashings_and_exits(&altair_head.beacon_state, &harness.chain.spec); + assert!(attester_slashings.contains(&slashing1)); + assert!(attester_slashings.contains(&slashing2)); + assert_eq!(attester_slashings.len(), 2); + + // Sign an attester slashing where an Altair attestation surrounds a phase0 one. + // + // This slashing is valid only before the Bellatrix fork epoch. + let slashing3 = harness.make_attester_slashing_with_epochs( + vec![2], + Some(Epoch::new(0)), + Some(altair_fork_epoch), + Some(Epoch::new(1)), + Some(altair_fork_epoch - 1), + ); + let verified_slashing3 = slashing3 + .clone() + .validate(&altair_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_attester_slashing(verified_slashing3); + + // All three slashings should be valid and returned from the pool at this point. + // Seeing as we can only extract 2 at time we'll just pretend that validator 0 is already + // slashed. + let mut to_be_slashed = hashset! {0}; + let attester_slashings = + op_pool.get_attester_slashings(&altair_head.beacon_state, &mut to_be_slashed); + assert!(attester_slashings.contains(&slashing2)); + assert!(attester_slashings.contains(&slashing3)); + assert_eq!(attester_slashings.len(), 2); + + // Advance to Bellatrix. + harness.extend_to_slot(bellatrix_fork_slot).await; + let bellatrix_head = harness.chain.canonical_head.cached_head().snapshot; + assert_eq!( + bellatrix_head.beacon_state.current_epoch(), + bellatrix_fork_epoch + ); + + // Sign an attester slashing with the Altair domain and phase0 epochs. This is a weird type + // of slashing that is only valid after the Bellatrix fork because we'll use the Altair fork + // domain to verify all prior epochs. + let slashing4 = harness.make_attester_slashing_with_epochs( + vec![3], + Some(Epoch::new(0)), + Some(altair_fork_epoch - 1), + Some(Epoch::new(0)), + Some(altair_fork_epoch - 1), + ); + let verified_slashing4 = slashing4 + .clone() + .validate(&bellatrix_head.beacon_state, &harness.chain.spec) + .unwrap(); + op_pool.insert_attester_slashing(verified_slashing4); + + // All slashings except slashing4 are now invalid (despite being present in the pool). + assert_eq!(op_pool.attester_slashings.read().len(), 4); + let (_, attester_slashings, _) = + op_pool.get_slashings_and_exits(&bellatrix_head.beacon_state, &harness.spec); + assert!(attester_slashings.contains(&slashing4)); + assert_eq!(attester_slashings.len(), 1); + + // Pruning the attester slashings should remove all but slashing4. + op_pool.prune_attester_slashings(&bellatrix_head.beacon_state); + assert_eq!(op_pool.attester_slashings.read().len(), 1); + } } diff --git a/beacon_node/operation_pool/src/persistence.rs b/beacon_node/operation_pool/src/persistence.rs index 9d7d154e68..ed15369df7 100644 --- a/beacon_node/operation_pool/src/persistence.rs +++ b/beacon_node/operation_pool/src/persistence.rs @@ -5,9 +5,9 @@ use crate::OpPoolError; use crate::OperationPool; use derivative::Derivative; use parking_lot::RwLock; -use serde_derive::{Deserialize, Serialize}; use ssz::{Decode, Encode}; use ssz_derive::{Decode, Encode}; +use state_processing::SigVerifiedOp; use store::{DBColumn, Error as StoreError, StoreItem}; use types::*; @@ -20,15 +20,12 @@ type PersistedSyncContributions = Vec<(SyncAggregateId, Vec { /// [DEPRECATED] Mapping from attestation ID to attestation mappings. @@ -39,12 +36,24 @@ pub struct PersistedOperationPool { pub attestations: Vec<(Attestation, 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)>, /// Attester slashings. - pub attester_slashings: Vec<(AttesterSlashing, ForkVersion)>, - /// Proposer slashings. - pub proposer_slashings: Vec, - /// Voluntary exits. - pub voluntary_exits: Vec, + #[superstruct(only(V12))] + pub attester_slashings: Vec, T>>, + /// [DEPRECATED] Proposer slashings. + #[superstruct(only(V5))] + pub proposer_slashings_v5: Vec, + /// Proposer slashings with fork information. + #[superstruct(only(V12))] + pub proposer_slashings: Vec>, + /// [DEPRECATED] Voluntary exits. + #[superstruct(only(V5))] + pub voluntary_exits_v5: Vec, + /// Voluntary exits with fork information. + #[superstruct(only(V12))] + pub voluntary_exits: Vec>, } impl PersistedOperationPool { @@ -101,19 +110,19 @@ impl PersistedOperationPool { /// Reconstruct an `OperationPool`. pub fn into_operation_pool(self) -> Result, OpPoolError> { - let attester_slashings = RwLock::new(self.attester_slashings().iter().cloned().collect()); + let attester_slashings = RwLock::new(self.attester_slashings()?.iter().cloned().collect()); let proposer_slashings = RwLock::new( - self.proposer_slashings() + self.proposer_slashings()? .iter() .cloned() - .map(|slashing| (slashing.signed_header_1.message.proposer_index, slashing)) + .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.message.validator_index, exit)) + .map(|exit| (exit.as_inner().message.validator_index, exit)) .collect(), ); let sync_contributions = RwLock::new(self.sync_contributions().iter().cloned().collect()); diff --git a/consensus/state_processing/Cargo.toml b/consensus/state_processing/Cargo.toml index c7ed4b308d..46ac2bae57 100644 --- a/consensus/state_processing/Cargo.toml +++ b/consensus/state_processing/Cargo.toml @@ -14,6 +14,7 @@ bls = { path = "../../crypto/bls" } integer-sqrt = "0.1.5" itertools = "0.10.0" eth2_ssz = "0.4.1" +eth2_ssz_derive = "0.3.0" eth2_ssz_types = "0.2.2" merkle_proof = { path = "../merkle_proof" } safe_arith = { path = "../safe_arith" } @@ -26,6 +27,7 @@ smallvec = "1.6.1" arbitrary = { version = "1.0", features = ["derive"], optional = true } lighthouse_metrics = { path = "../../common/lighthouse_metrics", optional = true } lazy_static = { version = "1.4.0", optional = true } +derivative = "2.1.1" [features] default = ["legacy-arith", "metrics"] diff --git a/consensus/state_processing/src/verify_operation.rs b/consensus/state_processing/src/verify_operation.rs index 25c2839edd..b3f9166039 100644 --- a/consensus/state_processing/src/verify_operation.rs +++ b/consensus/state_processing/src/verify_operation.rs @@ -5,36 +5,112 @@ use crate::per_block_processing::{ verify_attester_slashing, verify_exit, verify_proposer_slashing, }; use crate::VerifySignatures; +use derivative::Derivative; +use smallvec::{smallvec, SmallVec}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode, Encode}; +use std::marker::PhantomData; use types::{ - AttesterSlashing, BeaconState, ChainSpec, EthSpec, ProposerSlashing, SignedVoluntaryExit, + AttesterSlashing, BeaconState, ChainSpec, Epoch, EthSpec, Fork, ForkVersion, ProposerSlashing, + SignedVoluntaryExit, }; +const MAX_FORKS_VERIFIED_AGAINST: usize = 2; + /// Wrapper around an operation type that acts as proof that its signature has been checked. /// -/// The inner field is private, meaning instances of this type can only be constructed +/// The inner `op` field is private, meaning instances of this type can only be constructed /// by calling `validate`. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SigVerifiedOp(T); +#[derive(Derivative, Debug, Clone, Encode, Decode)] +#[derivative( + PartialEq, + Eq, + Hash(bound = "T: Encode + Decode + std::hash::Hash, E: EthSpec") +)] +pub struct SigVerifiedOp { + op: T, + verified_against: VerifiedAgainst, + #[ssz(skip_serializing, skip_deserializing)] + _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 +/// will continue to be valid in the presence of a changing `state.fork()`. The reason for this +/// is that the fork versions that the message's epochs map to might change. +/// +/// For example a proposer slashing at a phase0 slot verified against an Altair state will use +/// the phase0 fork version, but will become invalid once the Bellatrix fork occurs because that +/// slot will start to map to the Altair fork version. This is because `Fork::get_fork_version` only +/// remembers the most recent two forks. +/// +/// In the other direction, a proposer slashing at a Bellatrix slot verified against an Altair state +/// will use the Altair fork version, but will become invalid once the Bellatrix fork occurs because +/// that slot will start to map to the Bellatrix fork version. +/// +/// We need to store multiple `ForkVersion`s because attester slashings contain two indexed +/// attestations which may be signed using different versions. +#[derive(Debug, PartialEq, Eq, Clone, Hash, Encode, Decode)] +pub struct VerifiedAgainst { + fork_versions: SmallVec<[ForkVersion; MAX_FORKS_VERIFIED_AGAINST]>, +} + +impl SigVerifiedOp +where + T: VerifyOperation, + E: EthSpec, +{ + /// This function must be private because it assumes that `op` has already been verified. + fn new(op: T, state: &BeaconState) -> Self { + let verified_against = VerifiedAgainst { + fork_versions: op + .verification_epochs() + .into_iter() + .map(|epoch| state.fork().get_fork_version(epoch)) + .collect(), + }; + + SigVerifiedOp { + op, + verified_against, + _phantom: PhantomData, + } + } -impl SigVerifiedOp { pub fn into_inner(self) -> T { - self.0 + self.op } pub fn as_inner(&self) -> &T { - &self.0 + &self.op + } + + pub fn signature_is_still_valid(&self, current_fork: &Fork) -> bool { + self.as_inner() + .verification_epochs() + .into_iter() + .zip(self.verified_against.fork_versions.iter()) + .all(|(epoch, verified_fork_version)| { + current_fork.get_fork_version(epoch) == *verified_fork_version + }) } } /// Trait for operations that can be verified and transformed into a `SigVerifiedOp`. -pub trait VerifyOperation: Sized { +pub trait VerifyOperation: Encode + Decode + Sized { type Error; fn validate( self, state: &BeaconState, spec: &ChainSpec, - ) -> Result, Self::Error>; + ) -> Result, Self::Error>; + + /// Return the epochs at which parts of this message were verified. + /// + /// These need to map 1-to-1 to the `SigVerifiedOp::verified_against` for this type. + fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]>; } impl VerifyOperation for SignedVoluntaryExit { @@ -44,9 +120,13 @@ impl VerifyOperation for SignedVoluntaryExit { self, state: &BeaconState, spec: &ChainSpec, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { verify_exit(state, &self, VerifySignatures::True, spec)?; - Ok(SigVerifiedOp(self)) + Ok(SigVerifiedOp::new(self, state)) + } + + fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { + smallvec![self.message.epoch] } } @@ -57,9 +137,16 @@ impl VerifyOperation for AttesterSlashing { self, state: &BeaconState, spec: &ChainSpec, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { verify_attester_slashing(state, &self, VerifySignatures::True, spec)?; - Ok(SigVerifiedOp(self)) + Ok(SigVerifiedOp::new(self, state)) + } + + fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { + smallvec![ + self.attestation_1.data.target.epoch, + self.attestation_2.data.target.epoch + ] } } @@ -70,8 +157,17 @@ impl VerifyOperation for ProposerSlashing { self, state: &BeaconState, spec: &ChainSpec, - ) -> Result, Self::Error> { + ) -> Result, Self::Error> { verify_proposer_slashing(&self, state, VerifySignatures::True, spec)?; - Ok(SigVerifiedOp(self)) + Ok(SigVerifiedOp::new(self, state)) + } + + fn verification_epochs(&self) -> SmallVec<[Epoch; MAX_FORKS_VERIFIED_AGAINST]> { + // Only need a single epoch because the slots of the two headers must be equal. + smallvec![self + .signed_header_1 + .message + .slot + .epoch(E::slots_per_epoch())] } } diff --git a/consensus/types/src/proposer_slashing.rs b/consensus/types/src/proposer_slashing.rs index ff12b0611a..ca048b149a 100644 --- a/consensus/types/src/proposer_slashing.rs +++ b/consensus/types/src/proposer_slashing.rs @@ -18,6 +18,13 @@ pub struct ProposerSlashing { pub signed_header_2: SignedBeaconBlockHeader, } +impl ProposerSlashing { + /// Get proposer index, assuming slashing validity has already been checked. + pub fn proposer_index(&self) -> u64 { + self.signed_header_1.message.proposer_index + } +} + #[cfg(test)] mod tests { use super::*;