lighthouse/slasher/tests/random.rs
Michael Sproul 92d597ad23 Modularise slasher backend (#3443)
## Proposed Changes

Enable multiple database backends for the slasher, either MDBX (default) or LMDB. The backend can be selected using `--slasher-backend={lmdb,mdbx}`.

## Additional Info

In order to abstract over the two library's different handling of database lifetimes I've used `Box::leak` to give the `Environment` type a `'static` lifetime. This was the only way I could think of using 100% safe code to construct a self-referential struct `SlasherDB`, where the `OpenDatabases` refers to the `Environment`. I think this is OK, as the `Environment` is expected to live for the life of the program, and both database engines leave the database in a consistent state after each write. The memory claimed for memory-mapping will be freed by the OS and appropriately flushed regardless of whether the `Environment` is actually dropped.

We are depending on two `sigp` forks of `libmdbx-rs` and `lmdb-rs`, to give us greater control over MDBX OS support and LMDB's version.
2022-08-15 01:30:56 +00:00

237 lines
6.1 KiB
Rust

#![cfg(any(feature = "mdbx", feature = "lmdb"))]
use logging::test_logger;
use rand::prelude::*;
use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng};
use slasher::{
test_utils::{
block, indexed_att, slashed_validators_from_attestations,
slashed_validators_from_slashings, E,
},
Config, Slasher,
};
use std::cmp::max;
use tempfile::tempdir;
use types::{Epoch, EthSpec};
#[derive(Debug)]
struct TestConfig {
num_validators: usize,
max_attestations: usize,
check_slashings: bool,
add_blocks: bool,
}
impl Default for TestConfig {
fn default() -> Self {
Self {
num_validators: 4,
max_attestations: 50,
check_slashings: false,
add_blocks: false,
}
}
}
fn random_test(seed: u64, test_config: TestConfig) {
let check_slashings = test_config.check_slashings;
let num_validators = test_config.num_validators;
let max_attestations = test_config.max_attestations;
println!("Running with seed {}", seed);
let mut rng = StdRng::seed_from_u64(seed);
let tempdir = tempdir().unwrap();
let mut config = Config::new(tempdir.path().into());
config.validator_chunk_size = 1 << rng.gen_range(1..4);
let chunk_size_exponent = rng.gen_range(1..4);
config.chunk_size = 1 << chunk_size_exponent;
config.history_length = 1 << rng.gen_range(chunk_size_exponent..chunk_size_exponent + 3);
let slasher = Slasher::<E>::open(config.clone(), test_logger()).unwrap();
let validators = (0..num_validators as u64).collect::<Vec<u64>>();
let num_attestations = rng.gen_range(2..max_attestations + 1);
let mut current_epoch = Epoch::new(0);
let mut attestations = vec![];
for _ in 0..num_attestations {
let num_attesters = rng.gen_range(1..num_validators);
let mut attesting_indices = validators
.choose_multiple(&mut rng, num_attesters)
.copied()
.collect::<Vec<u64>>();
attesting_indices.sort_unstable();
// If checking slashings, generate valid attestations in range.
let (source, target) = if check_slashings {
let source = rng.gen_range(
current_epoch
.as_u64()
.saturating_sub(config.history_length as u64 - 1)
..current_epoch.as_u64() + 1,
);
let target = rng.gen_range(source..current_epoch.as_u64() + 1);
(source, target)
} else {
let source = rng.gen_range(0..max(3 * current_epoch.as_u64(), 1));
let target = rng.gen_range(source..max(3 * current_epoch.as_u64(), source + 1));
(source, target)
};
let target_root = rng.gen_range(0..3);
let attestation = indexed_att(&attesting_indices, source, target, target_root);
if check_slashings {
attestations.push(attestation.clone());
}
// Supply to slasher
slasher.accept_attestation(attestation);
// Maybe add a random block too
if test_config.add_blocks && rng.gen_bool(0.1) {
let slot = rng.gen_range(0..1 + 3 * current_epoch.as_u64() * E::slots_per_epoch() / 2);
let proposer = rng.gen_range(0..num_validators as u64);
let block_root = rng.gen_range(0..2);
slasher.accept_block_header(block(slot, proposer, block_root));
}
// Maybe process
if rng.gen_bool(0.1) {
slasher.process_queued(current_epoch).unwrap();
// Maybe prune
if rng.gen_bool(0.1) {
slasher.prune_database(current_epoch).unwrap();
}
}
// Maybe advance to the next epoch
if rng.gen_bool(0.5) {
if check_slashings {
slasher.process_queued(current_epoch).unwrap();
}
current_epoch += 1;
}
}
if !check_slashings {
return;
}
slasher.process_queued(current_epoch).unwrap();
let slashings = slasher.get_attester_slashings();
let slashed_validators = slashed_validators_from_slashings(&slashings);
let expected_slashed_validators = slashed_validators_from_attestations(&attestations);
assert_eq!(slashed_validators, expected_slashed_validators);
}
// Fuzz-like test that runs forever on different seeds looking for crashes.
#[test]
#[ignore]
fn no_crash() {
let mut rng = thread_rng();
loop {
random_test(rng.gen(), TestConfig::default());
}
}
// Fuzz-like test that runs forever on different seeds looking for crashes.
#[test]
#[ignore]
fn no_crash_with_blocks() {
let mut rng = thread_rng();
loop {
random_test(
rng.gen(),
TestConfig {
add_blocks: true,
..TestConfig::default()
},
);
}
}
// Fuzz-like test that runs forever on different seeds looking for missed slashings.
#[test]
#[ignore]
fn check_slashings() {
let mut rng = thread_rng();
loop {
random_test(
rng.gen(),
TestConfig {
check_slashings: true,
..TestConfig::default()
},
);
}
}
#[test]
fn check_slashings_example1() {
random_test(
1,
TestConfig {
check_slashings: true,
..TestConfig::default()
},
);
}
#[test]
fn check_slashings_example2() {
random_test(
2,
TestConfig {
check_slashings: true,
max_attestations: 3,
..TestConfig::default()
},
);
}
#[test]
fn check_slashings_example3() {
random_test(
3,
TestConfig {
check_slashings: true,
max_attestations: 100,
..TestConfig::default()
},
);
}
#[test]
fn no_crash_example1() {
random_test(1, TestConfig::default());
}
#[test]
fn no_crash_example2() {
random_test(2, TestConfig::default());
}
#[test]
fn no_crash_example3() {
random_test(3, TestConfig::default());
}
#[test]
fn no_crash_blocks_example1() {
random_test(
1,
TestConfig {
add_blocks: true,
..TestConfig::default()
},
);
}