59ec6b71b8
## Issue Addressed Closes https://github.com/sigp/lighthouse/issues/2371 ## Proposed Changes Backport some changes from `tree-states` that remove duplicated calculations of the `proposer_index`. With this change the proposer index should be calculated only once for each block, and then plumbed through to every place it is required. ## Additional Info In future I hope to add more data to the consensus context that is cached on a per-epoch basis, like the effective balances of validators and the base rewards. There are some other changes to remove indexing in tests that were also useful for `tree-states` (the `tree-states` types don't implement `Index`).
373 lines
11 KiB
Rust
373 lines
11 KiB
Rust
use super::*;
|
|
use beacon_chain::test_utils::{BeaconChainHarness, EphemeralHarnessType};
|
|
use state_processing::{
|
|
per_block_processing, per_block_processing::errors::ExitInvalid, BlockProcessingError,
|
|
BlockSignatureStrategy, ConsensusContext, VerifyBlockRoot,
|
|
};
|
|
use types::{BeaconBlock, BeaconState, Epoch, EthSpec, SignedBeaconBlock};
|
|
|
|
// Default validator index to exit.
|
|
pub const VALIDATOR_INDEX: u64 = 0;
|
|
// Epoch that the state will be transitioned to by default, equal to SHARD_COMMITTEE_PERIOD.
|
|
pub const STATE_EPOCH: Epoch = Epoch::new(256);
|
|
|
|
struct ExitTest {
|
|
validator_index: u64,
|
|
exit_epoch: Epoch,
|
|
state_epoch: Epoch,
|
|
#[allow(clippy::type_complexity)]
|
|
state_modifier: Box<dyn FnOnce(&mut BeaconState<E>)>,
|
|
#[allow(clippy::type_complexity)]
|
|
block_modifier:
|
|
Box<dyn FnOnce(&BeaconChainHarness<EphemeralHarnessType<E>>, &mut BeaconBlock<E>)>,
|
|
#[allow(dead_code)]
|
|
expected: Result<(), BlockProcessingError>,
|
|
}
|
|
|
|
impl Default for ExitTest {
|
|
fn default() -> Self {
|
|
Self {
|
|
validator_index: VALIDATOR_INDEX,
|
|
exit_epoch: STATE_EPOCH,
|
|
state_epoch: STATE_EPOCH,
|
|
state_modifier: Box::new(|_| ()),
|
|
block_modifier: Box::new(|_, _| ()),
|
|
expected: Ok(()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ExitTest {
|
|
async fn block_and_pre_state(self) -> (SignedBeaconBlock<E>, BeaconState<E>) {
|
|
let harness = get_harness::<E>(
|
|
self.state_epoch.start_slot(E::slots_per_epoch()),
|
|
VALIDATOR_COUNT,
|
|
)
|
|
.await;
|
|
let mut state = harness.get_current_state();
|
|
(self.state_modifier)(&mut state);
|
|
|
|
let block_modifier = self.block_modifier;
|
|
let validator_index = self.validator_index;
|
|
let exit_epoch = self.exit_epoch;
|
|
|
|
let (signed_block, state) = harness
|
|
.make_block_with_modifier(state.clone(), state.slot() + 1, |block| {
|
|
harness.add_voluntary_exit(block, validator_index, exit_epoch);
|
|
block_modifier(&harness, block);
|
|
})
|
|
.await;
|
|
(signed_block, state)
|
|
}
|
|
|
|
fn process(
|
|
block: &SignedBeaconBlock<E>,
|
|
state: &mut BeaconState<E>,
|
|
) -> Result<(), BlockProcessingError> {
|
|
let mut ctxt = ConsensusContext::new(block.slot());
|
|
per_block_processing(
|
|
state,
|
|
block,
|
|
BlockSignatureStrategy::VerifyIndividual,
|
|
VerifyBlockRoot::True,
|
|
&mut ctxt,
|
|
&E::default_spec(),
|
|
)
|
|
}
|
|
|
|
#[cfg(all(test, not(debug_assertions)))]
|
|
async fn run(self) -> BeaconState<E> {
|
|
let spec = &E::default_spec();
|
|
let expected = self.expected.clone();
|
|
assert_eq!(STATE_EPOCH, spec.shard_committee_period);
|
|
|
|
let (block, mut state) = self.block_and_pre_state().await;
|
|
|
|
let result = Self::process(&block, &mut state);
|
|
|
|
assert_eq!(result, expected);
|
|
|
|
state
|
|
}
|
|
|
|
async fn test_vector(self, title: String) -> TestVector {
|
|
let (block, pre_state) = self.block_and_pre_state().await;
|
|
let mut post_state = pre_state.clone();
|
|
let (post_state, error) = match Self::process(&block, &mut post_state) {
|
|
Ok(_) => (Some(post_state), None),
|
|
Err(e) => (None, Some(format!("{:?}", e))),
|
|
};
|
|
|
|
TestVector {
|
|
title,
|
|
pre_state,
|
|
block,
|
|
post_state,
|
|
error,
|
|
}
|
|
}
|
|
}
|
|
|
|
vectors_and_tests!(
|
|
// Ensures we can process a valid exit,
|
|
valid_single_exit,
|
|
ExitTest::default(),
|
|
// Tests three exits in the same block.
|
|
valid_three_exits,
|
|
ExitTest {
|
|
block_modifier: Box::new(|harness, block| {
|
|
harness.add_voluntary_exit(block, 1, STATE_EPOCH);
|
|
harness.add_voluntary_exit(block, 2, STATE_EPOCH);
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Ensures that a validator cannot be exited twice in the same block.
|
|
invalid_duplicate,
|
|
ExitTest {
|
|
block_modifier: Box::new(|_, block| {
|
|
// Duplicate the exit
|
|
let exit = block.body().voluntary_exits().get(0).unwrap().clone();
|
|
block.body_mut().voluntary_exits_mut().push(exit).unwrap();
|
|
}),
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 1,
|
|
reason: ExitInvalid::AlreadyExited(0),
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// validator = state.validators[voluntary_exit.validator_index]
|
|
// ```
|
|
invalid_validator_unknown,
|
|
ExitTest {
|
|
block_modifier: Box::new(|_, block| {
|
|
block
|
|
.body_mut()
|
|
.voluntary_exits_mut()
|
|
.get_mut(0)
|
|
.unwrap()
|
|
.message
|
|
.validator_index = VALIDATOR_COUNT as u64;
|
|
}),
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::ValidatorUnknown(VALIDATOR_COUNT as u64),
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// # Verify exit has not been initiated
|
|
// assert validator.exit_epoch == FAR_FUTURE_EPOCH
|
|
// ```
|
|
invalid_exit_already_initiated,
|
|
ExitTest {
|
|
state_modifier: Box::new(|state| {
|
|
state.validators_mut().get_mut(0).unwrap().exit_epoch = STATE_EPOCH + 1;
|
|
}),
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::AlreadyExited(0),
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// # Verify the validator is active
|
|
// assert is_active_validator(validator, get_current_epoch(state))
|
|
// ```
|
|
invalid_not_active_before_activation_epoch,
|
|
ExitTest {
|
|
state_modifier: Box::new(|state| {
|
|
state.validators_mut().get_mut(0).unwrap().activation_epoch =
|
|
E::default_spec().far_future_epoch;
|
|
}),
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::NotActive(0),
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Also tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// # Verify the validator is active
|
|
// assert is_active_validator(validator, get_current_epoch(state))
|
|
// ```
|
|
invalid_not_active_after_exit_epoch,
|
|
ExitTest {
|
|
state_modifier: Box::new(|state| {
|
|
state.validators_mut().get_mut(0).unwrap().exit_epoch = STATE_EPOCH;
|
|
}),
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::NotActive(0),
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Ensures we can process an exit from genesis.
|
|
valid_genesis_epoch,
|
|
ExitTest {
|
|
exit_epoch: Epoch::new(0),
|
|
..ExitTest::default()
|
|
},
|
|
// Ensures we can process an exit from the previous epoch.
|
|
valid_previous_epoch,
|
|
ExitTest {
|
|
exit_epoch: STATE_EPOCH - 1,
|
|
..ExitTest::default()
|
|
},
|
|
// Tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// # Exits must specify an epoch when they become valid; they are not
|
|
// # valid before then
|
|
// assert get_current_epoch(state) >= voluntary_exit.epoch
|
|
// ```
|
|
invalid_future_exit_epoch,
|
|
ExitTest {
|
|
exit_epoch: STATE_EPOCH + 1,
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::FutureEpoch {
|
|
state: STATE_EPOCH,
|
|
exit: STATE_EPOCH + 1,
|
|
},
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// # Verify the validator has been active long enough
|
|
// assert get_current_epoch(state) >= validator.activation_epoch + PERSISTENT_COMMITTEE_PERIOD
|
|
// ```
|
|
invalid_too_young_by_one_epoch,
|
|
ExitTest {
|
|
state_epoch: STATE_EPOCH - 1,
|
|
exit_epoch: STATE_EPOCH - 1,
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::TooYoungToExit {
|
|
current_epoch: STATE_EPOCH - 1,
|
|
earliest_exit_epoch: STATE_EPOCH,
|
|
},
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Also tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// # Verify the validator has been active long enough
|
|
// assert get_current_epoch(state) >= validator.activation_epoch + PERSISTENT_COMMITTEE_PERIOD
|
|
// ```
|
|
invalid_too_young_by_a_lot,
|
|
ExitTest {
|
|
state_epoch: Epoch::new(0),
|
|
exit_epoch: Epoch::new(0),
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::TooYoungToExit {
|
|
current_epoch: Epoch::new(0),
|
|
earliest_exit_epoch: STATE_EPOCH,
|
|
},
|
|
}),
|
|
..ExitTest::default()
|
|
},
|
|
// Tests the following line of the spec:
|
|
//
|
|
// Spec v0.12.1
|
|
//
|
|
// ```ignore
|
|
// # Verify signature
|
|
// domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT,
|
|
// voluntary_exit.epoch)
|
|
// signing_root = compute_signing_root(voluntary_exit, domain)
|
|
// assert bls.Verify(validator.pubkey, signing_root,
|
|
// signed_voluntary_exit.signature)
|
|
// ```
|
|
invalid_bad_signature,
|
|
ExitTest {
|
|
block_modifier: Box::new(|_, block| {
|
|
// Shift the validator index by 1 so that it's mismatched from the key that was
|
|
// used to sign.
|
|
block
|
|
.body_mut()
|
|
.voluntary_exits_mut()
|
|
.get_mut(0)
|
|
.unwrap()
|
|
.message
|
|
.validator_index = VALIDATOR_INDEX + 1;
|
|
}),
|
|
expected: Err(BlockProcessingError::ExitInvalid {
|
|
index: 0,
|
|
reason: ExitInvalid::BadSignature,
|
|
}),
|
|
..ExitTest::default()
|
|
}
|
|
);
|
|
|
|
#[cfg(all(test, not(debug_assertions)))]
|
|
mod custom_tests {
|
|
use super::*;
|
|
|
|
fn assert_exited(state: &BeaconState<E>, validator_index: usize) {
|
|
let spec = E::default_spec();
|
|
|
|
let validator = &state.validators()[validator_index];
|
|
assert_eq!(
|
|
validator.exit_epoch,
|
|
// This is correct until we exceed the churn limit. If that happens, we
|
|
// need to introduce more complex logic.
|
|
state.current_epoch() + 1 + spec.max_seed_lookahead,
|
|
"exit epoch"
|
|
);
|
|
assert_eq!(
|
|
validator.withdrawable_epoch,
|
|
validator.exit_epoch + E::default_spec().min_validator_withdrawability_delay,
|
|
"withdrawable epoch"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn valid() {
|
|
let state = ExitTest::default().run().await;
|
|
assert_exited(&state, VALIDATOR_INDEX as usize);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn valid_three() {
|
|
let state = ExitTest {
|
|
block_modifier: Box::new(|harness, block| {
|
|
harness.add_voluntary_exit(block, 1, STATE_EPOCH);
|
|
harness.add_voluntary_exit(block, 2, STATE_EPOCH);
|
|
}),
|
|
..ExitTest::default()
|
|
}
|
|
.run()
|
|
.await;
|
|
|
|
for i in &[VALIDATOR_INDEX, 1, 2] {
|
|
assert_exited(&state, *i as usize);
|
|
}
|
|
}
|
|
}
|