mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-03 00:31:50 +00:00
Fix handling of cross-fork messages in op pool
This commit is contained in:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -2041,7 +2041,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub fn verify_voluntary_exit_for_gossip(
|
||||
&self,
|
||||
exit: SignedVoluntaryExit,
|
||||
) -> Result<ObservationOutcome<SignedVoluntaryExit>, Error> {
|
||||
) -> Result<ObservationOutcome<SignedVoluntaryExit, T::EthSpec>, 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<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
|
||||
/// Accept a pre-verified exit and queue it for inclusion in an appropriate block.
|
||||
pub fn import_voluntary_exit(&self, exit: SigVerifiedOp<SignedVoluntaryExit>) {
|
||||
pub fn import_voluntary_exit(&self, exit: SigVerifiedOp<SignedVoluntaryExit, T::EthSpec>) {
|
||||
if self.eth1_chain.is_some() {
|
||||
self.op_pool.insert_voluntary_exit(exit)
|
||||
}
|
||||
@@ -2072,7 +2072,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub fn verify_proposer_slashing_for_gossip(
|
||||
&self,
|
||||
proposer_slashing: ProposerSlashing,
|
||||
) -> Result<ObservationOutcome<ProposerSlashing>, Error> {
|
||||
) -> Result<ObservationOutcome<ProposerSlashing, T::EthSpec>, 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<T: BeaconChainTypes> BeaconChain<T> {
|
||||
}
|
||||
|
||||
/// Accept some proposer slashing and queue it for inclusion in an appropriate block.
|
||||
pub fn import_proposer_slashing(&self, proposer_slashing: SigVerifiedOp<ProposerSlashing>) {
|
||||
pub fn import_proposer_slashing(
|
||||
&self,
|
||||
proposer_slashing: SigVerifiedOp<ProposerSlashing, T::EthSpec>,
|
||||
) {
|
||||
if self.eth1_chain.is_some() {
|
||||
self.op_pool.insert_proposer_slashing(proposer_slashing)
|
||||
}
|
||||
@@ -2092,7 +2095,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
pub fn verify_attester_slashing_for_gossip(
|
||||
&self,
|
||||
attester_slashing: AttesterSlashing<T::EthSpec>,
|
||||
) -> Result<ObservationOutcome<AttesterSlashing<T::EthSpec>>, Error> {
|
||||
) -> Result<ObservationOutcome<AttesterSlashing<T::EthSpec>, 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<T: BeaconChainTypes> BeaconChain<T> {
|
||||
/// 2. Add it to the op pool.
|
||||
pub fn import_attester_slashing(
|
||||
&self,
|
||||
attester_slashing: SigVerifiedOp<AttesterSlashing<T::EthSpec>>,
|
||||
attester_slashing: SigVerifiedOp<AttesterSlashing<T::EthSpec>, T::EthSpec>,
|
||||
) {
|
||||
// Add to fork choice.
|
||||
self.canonical_head
|
||||
@@ -2116,10 +2119,7 @@ impl<T: BeaconChainTypes> BeaconChain<T> {
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T: ObservableOperation<E>, E: EthSpec> {
|
||||
|
||||
/// Was the observed operation new and valid for further processing, or a useless duplicate?
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum ObservationOutcome<T> {
|
||||
New(SigVerifiedOp<T>),
|
||||
pub enum ObservationOutcome<T: Encode + Decode, E: EthSpec> {
|
||||
New(SigVerifiedOp<T, E>),
|
||||
AlreadyKnown,
|
||||
}
|
||||
|
||||
@@ -81,7 +82,7 @@ impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
|
||||
op: T,
|
||||
head_state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<ObservationOutcome<T>, T::Error> {
|
||||
) -> Result<ObservationOutcome<T, E>, T::Error> {
|
||||
let observed_validator_indices = &mut self.observed_validator_indices;
|
||||
let new_validator_indices = op.observed_validators();
|
||||
|
||||
@@ -95,6 +96,8 @@ impl<T: ObservableOperation<E>, E: EthSpec> ObservedOperations<T, E> {
|
||||
.iter()
|
||||
.all(|index| observed_validator_indices.contains(index))
|
||||
{
|
||||
// FIXME(sproul): consider verifying that those already-observed slashings are
|
||||
// still valid.
|
||||
return Ok(ObservationOutcome::AlreadyKnown);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,13 @@ pub fn upgrade_to_v12<T: BeaconChainTypes>(
|
||||
"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<T: BeaconChainTypes>(
|
||||
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)])
|
||||
}
|
||||
|
||||
@@ -1175,6 +1175,19 @@ where
|
||||
}
|
||||
|
||||
pub fn make_attester_slashing(&self, validator_indices: Vec<u64>) -> AttesterSlashing<E> {
|
||||
self.make_attester_slashing_with_epochs(validator_indices, None, None, None, None)
|
||||
}
|
||||
|
||||
pub fn make_attester_slashing_with_epochs(
|
||||
&self,
|
||||
validator_indices: Vec<u64>,
|
||||
source1: Option<Epoch>,
|
||||
target1: Option<Epoch>,
|
||||
source2: Option<Epoch>,
|
||||
target2: Option<Epoch>,
|
||||
) -> AttesterSlashing<E> {
|
||||
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<Slot>,
|
||||
) -> 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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<T> = RwLock<HashMap<SyncAggregateId, Vec<SyncCommitteeContribution<T>>>>;
|
||||
@@ -45,11 +44,11 @@ pub struct OperationPool<T: EthSpec + Default> {
|
||||
/// Map from sync aggregate ID to the best `SyncCommitteeContribution`s seen for that ID.
|
||||
sync_contributions: SyncContributions<T>,
|
||||
/// Set of attester slashings, and the fork version they were verified against.
|
||||
attester_slashings: RwLock<HashSet<(AttesterSlashing<T>, ForkVersion)>>,
|
||||
attester_slashings: RwLock<HashSet<SigVerifiedOp<AttesterSlashing<T>, T>>>,
|
||||
/// Map from proposer index to slashing.
|
||||
proposer_slashings: RwLock<HashMap<u64, ProposerSlashing>>,
|
||||
proposer_slashings: RwLock<HashMap<u64, SigVerifiedOp<ProposerSlashing, T>>>,
|
||||
/// Map from exiting validator to their exit data.
|
||||
voluntary_exits: RwLock<HashMap<u64, SignedVoluntaryExit>>,
|
||||
voluntary_exits: RwLock<HashMap<u64, SigVerifiedOp<SignedVoluntaryExit, T>>>,
|
||||
/// Reward cache for accelerating attestation packing.
|
||||
reward_cache: RwLock<RewardCache>,
|
||||
_phantom: PhantomData<T>,
|
||||
@@ -335,23 +334,20 @@ impl<T: EthSpec> OperationPool<T> {
|
||||
/// Insert a proposer slashing into the pool.
|
||||
pub fn insert_proposer_slashing(
|
||||
&self,
|
||||
verified_proposer_slashing: SigVerifiedOp<ProposerSlashing>,
|
||||
verified_proposer_slashing: SigVerifiedOp<ProposerSlashing, T>,
|
||||
) {
|
||||
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<AttesterSlashing<T>>,
|
||||
fork: Fork,
|
||||
verified_slashing: SigVerifiedOp<AttesterSlashing<T>, 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<T: EthSpec> OperationPool<T> {
|
||||
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<T: EthSpec> OperationPool<T> {
|
||||
// slashings.
|
||||
let mut to_be_slashed = proposer_slashings
|
||||
.iter()
|
||||
.map(|s| s.signed_header_1.message.proposer_index)
|
||||
.collect::<HashSet<_>>();
|
||||
.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<T>,
|
||||
to_be_slashed: &mut HashSet<u64>,
|
||||
) -> Vec<AttesterSlashing<T>> {
|
||||
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<T: EthSpec> OperationPool<T> {
|
||||
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<T: EthSpec> OperationPool<T> {
|
||||
/// Prune attester slashings for all slashed or withdrawn validators, or attestations on another
|
||||
/// fork.
|
||||
pub fn prune_attester_slashings(&self, head_state: &BeaconState<T>) {
|
||||
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<T: EthSpec> OperationPool<T> {
|
||||
}
|
||||
|
||||
/// Insert a voluntary exit that has previously been checked elsewhere.
|
||||
pub fn insert_voluntary_exit(&self, verified_exit: SigVerifiedOp<SignedVoluntaryExit>) {
|
||||
let exit = verified_exit.into_inner();
|
||||
pub fn insert_voluntary_exit(&self, exit: SigVerifiedOp<SignedVoluntaryExit, T>) {
|
||||
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<T: EthSpec> OperationPool<T> {
|
||||
{
|
||||
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<T: EthSpec> OperationPool<T> {
|
||||
self.attester_slashings
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(slashing, _)| slashing.clone())
|
||||
.map(|slashing| slashing.as_inner().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -562,7 +568,7 @@ impl<T: EthSpec> OperationPool<T> {
|
||||
self.proposer_slashings
|
||||
.read()
|
||||
.iter()
|
||||
.map(|(_, slashing)| slashing.clone())
|
||||
.map(|(_, slashing)| slashing.as_inner().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -573,23 +579,29 @@ impl<T: EthSpec> OperationPool<T> {
|
||||
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<T>
|
||||
fn filter_limit_operations<'a, T: 'a, V: 'a, I, F, G>(
|
||||
operations: I,
|
||||
filter: F,
|
||||
mapping: G,
|
||||
limit: usize,
|
||||
) -> Vec<V>
|
||||
where
|
||||
I: IntoIterator<Item = &'a T>,
|
||||
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<T, F, E: EthSpec>(
|
||||
map: &mut HashMap<u64, T>,
|
||||
map: &mut HashMap<u64, SigVerifiedOp<T, E>>,
|
||||
prune_if: F,
|
||||
head_state: &BeaconState<E>,
|
||||
) where
|
||||
F: Fn(&Validator) -> bool,
|
||||
T: VerifyOperation<E>,
|
||||
{
|
||||
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::<MainnetEthSpec>::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<E: EthSpec>() -> (BeaconChainHarness<EphemeralHarnessType<E>>, ChainSpec)
|
||||
{
|
||||
let mut spec = test_spec::<E>();
|
||||
|
||||
// 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::<E>(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::<MainnetEthSpec>();
|
||||
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::<MainnetEthSpec>::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::<MainnetEthSpec>();
|
||||
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::<MainnetEthSpec>::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::<MainnetEthSpec>();
|
||||
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::<MainnetEthSpec>::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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T> = Vec<(SyncAggregateId, Vec<SyncCommitteeCont
|
||||
#[superstruct(
|
||||
variants(V5, V12),
|
||||
variant_attributes(
|
||||
derive(Derivative, PartialEq, Debug, Serialize, Deserialize, Encode, Decode),
|
||||
serde(bound = "T: EthSpec", deny_unknown_fields),
|
||||
derive(Derivative, PartialEq, Debug, Encode, Decode),
|
||||
derivative(Clone),
|
||||
),
|
||||
partial_getter_error(ty = "OpPoolError", expr = "OpPoolError::IncorrectOpPoolVariant")
|
||||
)]
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Encode)]
|
||||
#[serde(untagged)]
|
||||
#[serde(bound = "T: EthSpec")]
|
||||
#[derive(PartialEq, Debug, Encode)]
|
||||
#[ssz(enum_behaviour = "transparent")]
|
||||
pub struct PersistedOperationPool<T: EthSpec> {
|
||||
/// [DEPRECATED] Mapping from attestation ID to attestation mappings.
|
||||
@@ -39,12 +36,24 @@ pub struct PersistedOperationPool<T: EthSpec> {
|
||||
pub attestations: Vec<(Attestation<T>, Vec<u64>)>,
|
||||
/// Mapping from sync contribution ID to sync contributions and aggregate.
|
||||
pub sync_contributions: PersistedSyncContributions<T>,
|
||||
/// [DEPRECATED] Attester slashings.
|
||||
#[superstruct(only(V5))]
|
||||
pub attester_slashings_v5: Vec<(AttesterSlashing<T>, ForkVersion)>,
|
||||
/// Attester slashings.
|
||||
pub attester_slashings: Vec<(AttesterSlashing<T>, ForkVersion)>,
|
||||
/// Proposer slashings.
|
||||
pub proposer_slashings: Vec<ProposerSlashing>,
|
||||
/// Voluntary exits.
|
||||
pub voluntary_exits: Vec<SignedVoluntaryExit>,
|
||||
#[superstruct(only(V12))]
|
||||
pub attester_slashings: Vec<SigVerifiedOp<AttesterSlashing<T>, T>>,
|
||||
/// [DEPRECATED] Proposer slashings.
|
||||
#[superstruct(only(V5))]
|
||||
pub proposer_slashings_v5: Vec<ProposerSlashing>,
|
||||
/// Proposer slashings with fork information.
|
||||
#[superstruct(only(V12))]
|
||||
pub proposer_slashings: Vec<SigVerifiedOp<ProposerSlashing, T>>,
|
||||
/// [DEPRECATED] Voluntary exits.
|
||||
#[superstruct(only(V5))]
|
||||
pub voluntary_exits_v5: Vec<SignedVoluntaryExit>,
|
||||
/// Voluntary exits with fork information.
|
||||
#[superstruct(only(V12))]
|
||||
pub voluntary_exits: Vec<SigVerifiedOp<SignedVoluntaryExit, T>>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> PersistedOperationPool<T> {
|
||||
@@ -101,19 +110,19 @@ impl<T: EthSpec> PersistedOperationPool<T> {
|
||||
|
||||
/// Reconstruct an `OperationPool`.
|
||||
pub fn into_operation_pool(self) -> Result<OperationPool<T>, 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());
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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>(T);
|
||||
#[derive(Derivative, Debug, Clone, Encode, Decode)]
|
||||
#[derivative(
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash(bound = "T: Encode + Decode + std::hash::Hash, E: EthSpec")
|
||||
)]
|
||||
pub struct SigVerifiedOp<T: Encode + Decode, E: EthSpec> {
|
||||
op: T,
|
||||
verified_against: VerifiedAgainst,
|
||||
#[ssz(skip_serializing, skip_deserializing)]
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
/// 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<T, E> SigVerifiedOp<T, E>
|
||||
where
|
||||
T: VerifyOperation<E>,
|
||||
E: EthSpec,
|
||||
{
|
||||
/// This function must be private because it assumes that `op` has already been verified.
|
||||
fn new(op: T, state: &BeaconState<E>) -> 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<T> SigVerifiedOp<T> {
|
||||
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<E: EthSpec>: Sized {
|
||||
pub trait VerifyOperation<E: EthSpec>: Encode + Decode + Sized {
|
||||
type Error;
|
||||
|
||||
fn validate(
|
||||
self,
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<SigVerifiedOp<Self>, Self::Error>;
|
||||
) -> Result<SigVerifiedOp<Self, E>, 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<E: EthSpec> VerifyOperation<E> for SignedVoluntaryExit {
|
||||
@@ -44,9 +120,13 @@ impl<E: EthSpec> VerifyOperation<E> for SignedVoluntaryExit {
|
||||
self,
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<SigVerifiedOp<Self>, Self::Error> {
|
||||
) -> Result<SigVerifiedOp<Self, E>, 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<E: EthSpec> VerifyOperation<E> for AttesterSlashing<E> {
|
||||
self,
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<SigVerifiedOp<Self>, Self::Error> {
|
||||
) -> Result<SigVerifiedOp<Self, E>, 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<E: EthSpec> VerifyOperation<E> for ProposerSlashing {
|
||||
self,
|
||||
state: &BeaconState<E>,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<SigVerifiedOp<Self>, Self::Error> {
|
||||
) -> Result<SigVerifiedOp<Self, E>, 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())]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user