Implement Slashing Protection (#1116)

* Implement slashing protection

Roll-up of #588 with some conflicts resolved

* WIP improvements

* Require slot uniqueness for blocks (rather than epochs)
* Native DB support for Slot and Epoch
* Simplify surrounding/surrounded-by queries

* Implement unified slashing protection database

A single SQL database saves on open file descriptors.

* Make slashing protection concurrency safe.

Revive tests, add parallel tests.

* Some simplifications

* Auto-registration, test clean-ups

* More tests, clean-ups, hardening

* Fix comments in BLS

* Optimise bulk validator registration

* Delete outdated tests

* Use bundled SQLite in slashing protection

* Auto-register validators in simulation

* Use real signing_root in slashing protection

* Update book for --auto-register

* Refine log messages and help flags

* Correct typo in Cargo.toml authors

* Fix merge conflicts

* Safer error handling in sqlite slot/epoch

* Address review comments

* Add attestation test mutating block root

Co-authored-by: pscott <scottpiriou@gmail.com>
This commit is contained in:
Michael Sproul
2020-05-18 16:25:16 +10:00
committed by GitHub
parent 90b3953dda
commit 2d8e2dd7f5
30 changed files with 1720 additions and 52 deletions

View File

@@ -1,8 +1,10 @@
use crate::config::SLASHING_PROTECTION_FILENAME;
use crate::fork_service::ForkService;
use crate::validator_directory::{ValidatorDirectory, ValidatorDirectoryBuilder};
use parking_lot::RwLock;
use rayon::prelude::*;
use slog::{error, Logger};
use slashing_protection::{NotSafe, Safe, SlashingDatabase};
use slog::{crit, error, warn, Logger};
use slot_clock::SlotClock;
use std::collections::HashMap;
use std::fs::read_dir;
@@ -19,6 +21,7 @@ use types::{
#[derive(Clone)]
pub struct ValidatorStore<T, E: EthSpec> {
validators: Arc<RwLock<HashMap<PublicKey, ValidatorDirectory>>>,
slashing_protection: SlashingDatabase,
genesis_validators_root: Hash256,
spec: Arc<ChainSpec>,
log: Logger,
@@ -35,6 +38,15 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
fork_service: ForkService<T, E>,
log: Logger,
) -> Result<Self, String> {
let slashing_db_path = base_dir.join(SLASHING_PROTECTION_FILENAME);
let slashing_protection =
SlashingDatabase::open_or_create(&slashing_db_path).map_err(|e| {
format!(
"Failed to open or create slashing protection database: {:?}",
e
)
})?;
let validator_key_values = read_dir(&base_dir)
.map_err(|e| format!("Failed to read base directory {:?}: {:?}", base_dir, e))?
.collect::<Vec<_>>()
@@ -68,6 +80,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
Ok(Self {
validators: Arc::new(RwLock::new(HashMap::from_par_iter(validator_key_values))),
slashing_protection,
genesis_validators_root,
spec: Arc::new(spec),
log,
@@ -88,6 +101,10 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
.map_err(|e| format!("Unable to create temp dir: {:?}", e))?;
let data_dir = PathBuf::from(temp_dir.path());
let slashing_db_path = data_dir.join(SLASHING_PROTECTION_FILENAME);
let slashing_protection = SlashingDatabase::create(&slashing_db_path)
.map_err(|e| format!("Failed to create slashing protection database: {:?}", e))?;
let validators = validator_indices
.par_iter()
.map(|index| {
@@ -111,6 +128,7 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
Ok(Self {
validators: Arc::new(RwLock::new(HashMap::from_iter(validators))),
slashing_protection,
genesis_validators_root,
spec: Arc::new(spec),
log,
@@ -120,6 +138,16 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
})
}
/// Register all known validators with the slashing protection database.
///
/// Registration is required to protect against a lost or missing slashing database,
/// such as when relocating validator keys to a new machine.
pub fn register_all_validators_for_slashing_protection(&self) -> Result<(), String> {
self.slashing_protection
.register_validators(self.validators.read().keys())
.map_err(|e| format!("Error while registering validators: {:?}", e))
}
pub fn voting_pubkeys(&self) -> Vec<PublicKey> {
self.validators
.read()
@@ -165,20 +193,73 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
&self,
validator_pubkey: &PublicKey,
block: BeaconBlock<E>,
current_slot: Slot,
) -> Option<SignedBeaconBlock<E>> {
// TODO: check for slashing.
self.validators
.read()
.get(validator_pubkey)
.and_then(|validator_dir| {
let voting_keypair = validator_dir.voting_keypair.as_ref()?;
// Make sure the block slot is not higher than the current slot to avoid potential attacks.
if block.slot > current_slot {
warn!(
self.log,
"Not signing block with slot greater than current slot";
"block_slot" => block.slot.as_u64(),
"current_slot" => current_slot.as_u64()
);
return None;
}
// Check for slashing conditions.
let fork = self.fork()?;
let domain = self.spec.get_domain(
block.epoch(),
Domain::BeaconProposer,
&fork,
self.genesis_validators_root,
);
let slashing_status = self.slashing_protection.check_and_insert_block_proposal(
validator_pubkey,
&block.block_header(),
domain,
);
match slashing_status {
// We can safely sign this block.
Ok(Safe::Valid) => {
let validators = self.validators.read();
let validator = validators.get(validator_pubkey)?;
let voting_keypair = validator.voting_keypair.as_ref()?;
Some(block.sign(
&voting_keypair.sk,
&self.fork()?,
&fork,
self.genesis_validators_root,
&self.spec,
))
})
}
Ok(Safe::SameData) => {
warn!(
self.log,
"Skipping signing of previously signed block";
);
None
}
Err(NotSafe::UnregisteredValidator(pk)) => {
warn!(
self.log,
"Not signing block for unregistered validator";
"msg" => "Carefully consider running with --auto-register (see --help)",
"public_key" => format!("{:?}", pk)
);
None
}
Err(e) => {
crit!(
self.log,
"Not signing slashable block";
"error" => format!("{:?}", e)
);
None
}
}
}
pub fn sign_attestation(
@@ -186,19 +267,40 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
validator_pubkey: &PublicKey,
validator_committee_position: usize,
attestation: &mut Attestation<E>,
current_epoch: Epoch,
) -> Option<()> {
// TODO: check for slashing.
self.validators
.read()
.get(validator_pubkey)
.and_then(|validator_dir| {
let voting_keypair = validator_dir.voting_keypair.as_ref()?;
// Make sure the target epoch is not higher than the current epoch to avoid potential attacks.
if attestation.data.target.epoch > current_epoch {
return None;
}
// Checking for slashing conditions.
let fork = self.fork()?;
let domain = self.spec.get_domain(
attestation.data.target.epoch,
Domain::BeaconAttester,
&fork,
self.genesis_validators_root,
);
let slashing_status = self.slashing_protection.check_and_insert_attestation(
validator_pubkey,
&attestation.data,
domain,
);
match slashing_status {
// We can safely sign this attestation.
Ok(Safe::Valid) => {
let validators = self.validators.read();
let validator = validators.get(validator_pubkey)?;
let voting_keypair = validator.voting_keypair.as_ref()?;
attestation
.sign(
&voting_keypair.sk,
validator_committee_position,
&self.fork()?,
&fork,
self.genesis_validators_root,
&self.spec,
)
@@ -212,7 +314,33 @@ impl<T: SlotClock + 'static, E: EthSpec> ValidatorStore<T, E> {
.ok()?;
Some(())
})
}
Ok(Safe::SameData) => {
warn!(
self.log,
"Skipping signing of previously signed attestation"
);
None
}
Err(NotSafe::UnregisteredValidator(pk)) => {
warn!(
self.log,
"Not signing attestation for unregistered validator";
"msg" => "Carefully consider running with --auto-register (see --help)",
"public_key" => format!("{:?}", pk)
);
None
}
Err(e) => {
crit!(
self.log,
"Not signing slashable attestation";
"attestation" => format!("{:?}", attestation.data),
"error" => format!("{:?}", e)
);
None
}
}
}
/// Signs an `AggregateAndProof` for a given validator.