mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-23 14:54:45 +00:00
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:
@@ -320,6 +320,12 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let current_epoch = self
|
||||
.slot_clock
|
||||
.now()
|
||||
.ok_or_else(|| "Unable to determine current slot from clock".to_string())?
|
||||
.epoch(E::slots_per_epoch());
|
||||
|
||||
let attestation = self
|
||||
.beacon_node
|
||||
.http
|
||||
@@ -366,26 +372,14 @@ impl<T: SlotClock + 'static, E: EthSpec> AttestationService<T, E> {
|
||||
|
||||
let mut attestation = attestation.clone();
|
||||
|
||||
if self
|
||||
.validator_store
|
||||
self.validator_store
|
||||
.sign_attestation(
|
||||
duty.validator_pubkey(),
|
||||
validator_committee_position,
|
||||
&mut attestation,
|
||||
current_epoch,
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
crit!(
|
||||
log,
|
||||
"Attestation signing refused";
|
||||
"validator" => format!("{:?}", duty.validator_pubkey()),
|
||||
"slot" => attestation.data.slot,
|
||||
"index" => attestation.data.index,
|
||||
);
|
||||
None
|
||||
} else {
|
||||
Some(attestation)
|
||||
}
|
||||
.map(|_| attestation)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -210,6 +210,11 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
|
||||
async fn publish_block(self, slot: Slot, validator_pubkey: PublicKey) -> Result<(), String> {
|
||||
let log = &self.context.log;
|
||||
|
||||
let current_slot = self
|
||||
.slot_clock
|
||||
.now()
|
||||
.ok_or_else(|| "Unable to determine current slot from clock".to_string())?;
|
||||
|
||||
let randao_reveal = self
|
||||
.validator_store
|
||||
.randao_reveal(&validator_pubkey, slot.epoch(E::slots_per_epoch()))
|
||||
@@ -225,7 +230,7 @@ impl<T: SlotClock + 'static, E: EthSpec> BlockService<T, E> {
|
||||
|
||||
let signed_block = self
|
||||
.validator_store
|
||||
.sign_block(&validator_pubkey, block)
|
||||
.sign_block(&validator_pubkey, block, current_slot)
|
||||
.ok_or_else(|| "Unable to sign block".to_string())?;
|
||||
|
||||
let publish_status = self
|
||||
|
||||
@@ -17,9 +17,19 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
||||
.arg(
|
||||
Arg::with_name("allow-unsynced")
|
||||
.long("allow-unsynced")
|
||||
.help("If present, the validator client will still poll for duties if the beacon
|
||||
.help("If present, the validator client will still poll for duties if the beacon \
|
||||
node is not synced.")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("auto-register")
|
||||
.long("auto-register")
|
||||
.help("If present, the validator client will register any new signing keys with \
|
||||
the slashing protection database so that they may be used. WARNING: \
|
||||
enabling the same signing key on multiple validator clients WILL lead to \
|
||||
that validator getting slashed. Only use this flag the first time you run \
|
||||
the validator client, or if you're certain there are no other \
|
||||
nodes using the same key.")
|
||||
)
|
||||
/*
|
||||
* The "testnet" sub-command.
|
||||
*
|
||||
|
||||
@@ -4,6 +4,8 @@ use std::path::PathBuf;
|
||||
|
||||
pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/";
|
||||
pub const DEFAULT_DATA_DIR: &str = ".lighthouse/validators";
|
||||
/// Path to the slashing protection database within the datadir.
|
||||
pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite";
|
||||
|
||||
/// Specifies a method for obtaining validator keypairs.
|
||||
#[derive(Clone)]
|
||||
@@ -35,19 +37,22 @@ pub struct Config {
|
||||
/// If true, the validator client will still poll for duties and produce blocks even if the
|
||||
/// beacon node is not synced at startup.
|
||||
pub allow_unsynced_beacon_node: bool,
|
||||
/// If true, register new validator keys with the slashing protection database.
|
||||
pub auto_register: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
/// Build a new configuration from defaults.
|
||||
fn default() -> Self {
|
||||
let mut data_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||
data_dir.push(".lighthouse");
|
||||
data_dir.push("validators");
|
||||
let data_dir = dirs::home_dir()
|
||||
.map(|home| home.join(DEFAULT_DATA_DIR))
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
Self {
|
||||
data_dir,
|
||||
key_source: <_>::default(),
|
||||
http_server: DEFAULT_HTTP_SERVER.to_string(),
|
||||
allow_unsynced_beacon_node: false,
|
||||
auto_register: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +98,7 @@ impl Config {
|
||||
};
|
||||
|
||||
config.allow_unsynced_beacon_node = cli_args.is_present("allow-unsynced");
|
||||
config.auto_register = cli_args.is_present("auto-register");
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ impl TryInto<DutyAndProof> for ValidatorDutyBytes {
|
||||
}
|
||||
|
||||
/// The outcome of inserting some `ValidatorDuty` into the `DutiesStore`.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum InsertOutcome {
|
||||
/// These are the first duties received for this validator.
|
||||
NewValidator,
|
||||
@@ -426,8 +427,6 @@ impl<T: SlotClock + 'static, E: EthSpec> DutiesService<T, E> {
|
||||
|
||||
/// Returns the pubkeys of the validators which are assigned to propose in the given slot.
|
||||
///
|
||||
/// In normal cases, there should be 0 or 1 validators returned. In extreme cases (i.e., deep forking)
|
||||
///
|
||||
/// It is possible that multiple validators have an identical proposal slot, however that is
|
||||
/// likely the result of heavy forking (lol) or inconsistent beacon node connections.
|
||||
pub fn block_producers(&self, slot: Slot) -> Vec<PublicKey> {
|
||||
|
||||
@@ -188,6 +188,12 @@ impl<T: EthSpec> ProductionValidatorClient<T> {
|
||||
"voting_validators" => validator_store.num_voting_validators()
|
||||
);
|
||||
|
||||
if config.auto_register {
|
||||
info!(log, "Registering all validators for slashing protection");
|
||||
validator_store.register_all_validators_for_slashing_protection()?;
|
||||
info!(log, "Validator auto-registration complete");
|
||||
}
|
||||
|
||||
let duties_service = DutiesServiceBuilder::new()
|
||||
.slot_clock(slot_clock.clone())
|
||||
.validator_store(validator_store.clone())
|
||||
|
||||
@@ -35,7 +35,7 @@ fn dir_name(voting_pubkey: &PublicKey) -> String {
|
||||
/// Represents the files/objects for each dedicated lighthouse validator directory.
|
||||
///
|
||||
/// Generally lives in `~/.lighthouse/validators/`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct ValidatorDirectory {
|
||||
pub directory: PathBuf,
|
||||
pub voting_keypair: Option<Keypair>,
|
||||
@@ -147,11 +147,13 @@ pub struct ValidatorDirectoryBuilder {
|
||||
}
|
||||
|
||||
impl ValidatorDirectoryBuilder {
|
||||
/// Set the specification for this validator.
|
||||
pub fn spec(mut self, spec: ChainSpec) -> Self {
|
||||
self.spec = Some(spec);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the `MAX_EFFECTIVE_BALANCE` as this validator's deposit.
|
||||
pub fn full_deposit_amount(mut self) -> Result<Self, String> {
|
||||
let spec = self
|
||||
.spec
|
||||
@@ -161,17 +163,24 @@ impl ValidatorDirectoryBuilder {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Use a validator deposit of `gwei`.
|
||||
pub fn custom_deposit_amount(mut self, gwei: u64) -> Self {
|
||||
self.amount = Some(gwei);
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate keypairs using `Keypair::random()`.
|
||||
pub fn thread_random_keypairs(mut self) -> Self {
|
||||
self.voting_keypair = Some(Keypair::random());
|
||||
self.withdrawal_keypair = Some(Keypair::random());
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate insecure, deterministic keypairs.
|
||||
///
|
||||
///
|
||||
/// ## Warning
|
||||
/// Only for use in testing. Do not store value in these keys.
|
||||
pub fn insecure_keypairs(mut self, index: usize) -> Self {
|
||||
let keypair = generate_deterministic_keypair(index);
|
||||
self.voting_keypair = Some(keypair.clone());
|
||||
@@ -203,6 +212,7 @@ impl ValidatorDirectoryBuilder {
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Write the validators keypairs to disk.
|
||||
pub fn write_keypair_files(self) -> Result<Self, String> {
|
||||
let voting_keypair = self
|
||||
.voting_keypair
|
||||
@@ -285,7 +295,7 @@ impl ValidatorDirectoryBuilder {
|
||||
.directory
|
||||
.as_ref()
|
||||
.map(|directory| directory.join(ETH1_DEPOSIT_DATA_FILE))
|
||||
.ok_or_else(|| "write_eth1_data_filer requires a directory")?;
|
||||
.ok_or_else(|| "write_eth1_data_file requires a directory")?;
|
||||
|
||||
let (deposit_data, _) = self.get_deposit_data()?;
|
||||
|
||||
@@ -328,8 +338,10 @@ impl ValidatorDirectoryBuilder {
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<ValidatorDirectory, String> {
|
||||
let directory = self.directory.ok_or_else(|| "build requires a directory")?;
|
||||
|
||||
Ok(ValidatorDirectory {
|
||||
directory: self.directory.ok_or_else(|| "build requires a directory")?,
|
||||
directory,
|
||||
voting_keypair: self.voting_keypair,
|
||||
withdrawal_keypair: self.withdrawal_keypair,
|
||||
deposit_data: self.deposit_data,
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user