Skip to content

Commit

Permalink
Implement slashing protection interchange format (#1544)
Browse files Browse the repository at this point in the history
## Issue Addressed

Implements support for importing and exporting the slashing protection DB interchange format described here:

https://hackmd.io/@sproul/Bk0Y0qdGD

Also closes #1584 

## Proposed Changes

* [x] Support for serializing and deserializing the format
* [x] Support for importing and exporting Lighthouse's database
* [x] CLI commands to invoke import and export
* [x] Export to minimal format (required when a minimal format has been previously imported)
* [x] Tests for export to minimal (utilising mixed importing and attestation signing?)
* [x] Tests for import/export of complete format, and import of minimal format
* [x] ~~Prevent attestations with sources less than our max source (Danny's suggestion). Required for the fake attestation that we put in for the minimal format to block attestations from source 0.~~
* [x] Add the concept of a "low watermark" for compatibility with the minimal format

Bonus!

* [x] A fix to a potentially nasty bug involving validators getting re-registered each time the validator client ran! Thankfully, the ordering of keys meant that the validator IDs used for attestations and blocks remained stable -- otherwise we could have had some slashings on our hands! 😱
* [x] Tests to confirm that this bug is indeed vanquished
  • Loading branch information
michaelsproul committed Oct 2, 2020
1 parent 22aedda commit 1d278aa
Show file tree
Hide file tree
Showing 25 changed files with 1,164 additions and 88 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ target/
flamegraph.svg
perf.data*
*.tar.gz
bin/
/bin
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions account_manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ validator_dir = { path = "../common/validator_dir" }
tokio = { version = "0.2.21", features = ["full"] }
eth2_keystore = { path = "../crypto/eth2_keystore" }
account_utils = { path = "../common/account_utils" }
slashing_protection = { path = "../validator_client/slashing_protection" }
42 changes: 36 additions & 6 deletions account_manager/src/validator/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use directory::{
};
use environment::Environment;
use eth2_wallet_manager::WalletManager;
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -178,6 +179,16 @@ pub fn cli_run<T: EthSpec>(
.wallet_by_name(&wallet_name)
.map_err(|e| format!("Unable to open wallet: {:?}", e))?;

let slashing_protection_path = validator_dir.join(SLASHING_PROTECTION_FILENAME);
let slashing_protection =
SlashingDatabase::open_or_create(&slashing_protection_path).map_err(|e| {
format!(
"Unable to open or create slashing protection database at {}: {:?}",
slashing_protection_path.display(),
e
)
})?;

for i in 0..n {
let voting_password = random_password();
let withdrawal_password = random_password();
Expand All @@ -190,7 +201,22 @@ pub fn cli_run<T: EthSpec>(
)
.map_err(|e| format!("Unable to create validator keys: {:?}", e))?;

let voting_pubkey = keystores.voting.pubkey().to_string();
let voting_pubkey = keystores.voting.public_key().ok_or_else(|| {
format!(
"Keystore public key is invalid: {}",
keystores.voting.pubkey()
)
})?;

slashing_protection
.register_validator(&voting_pubkey)
.map_err(|e| {
format!(
"Error registering validator {}: {:?}",
voting_pubkey.to_hex_string(),
e
)
})?;

ValidatorDirBuilder::new(validator_dir.clone(), secrets_dir.clone())
.voting_keystore(keystores.voting, voting_password.as_bytes())
Expand All @@ -200,22 +226,26 @@ pub fn cli_run<T: EthSpec>(
.build()
.map_err(|e| format!("Unable to build validator directory: {:?}", e))?;

println!("{}/{}\t0x{}", i + 1, n, voting_pubkey);
println!("{}/{}\t{}", i + 1, n, voting_pubkey.to_hex_string());
}

Ok(())
}

/// Returns the number of validators that exist in the given `validator_dir`.
///
/// This function just assumes all files and directories, excluding the validator definitions YAML,
/// are validator directories, making it likely to return a higher number than accurate
/// but never a lower one.
/// This function just assumes all files and directories, excluding the validator definitions YAML
/// and slashing protection database are validator directories, making it likely to return a higher
/// number than accurate but never a lower one.
fn existing_validator_count<P: AsRef<Path>>(validator_dir: P) -> Result<usize, String> {
fs::read_dir(validator_dir.as_ref())
.map(|iter| {
iter.filter_map(|e| e.ok())
.filter(|e| e.file_name() != OsStr::new(validator_definitions::CONFIG_FILENAME))
.filter(|e| {
e.file_name() != OsStr::new(validator_definitions::CONFIG_FILENAME)
&& e.file_name()
!= OsStr::new(slashing_protection::SLASHING_PROTECTION_FILENAME)
})
.count()
})
.map_err(|e| format!("Unable to read {:?}: {}", validator_dir.as_ref(), e))
Expand Down
26 changes: 26 additions & 0 deletions account_manager/src/validator/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use account_utils::{
ZeroizeString,
};
use clap::{App, Arg, ArgMatches};
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
use std::fs;
use std::path::PathBuf;
use std::thread::sleep;
Expand Down Expand Up @@ -75,6 +76,16 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
let mut defs = ValidatorDefinitions::open_or_create(&validator_dir)
.map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?;

let slashing_protection_path = validator_dir.join(SLASHING_PROTECTION_FILENAME);
let slashing_protection =
SlashingDatabase::open_or_create(&slashing_protection_path).map_err(|e| {
format!(
"Unable to open or create slashing protection database at {}: {:?}",
slashing_protection_path.display(),
e
)
})?;

// Collect the paths for the keystores that should be imported.
let keystore_paths = match (keystore, keystores_dir) {
(Some(keystore), None) => vec![keystore],
Expand Down Expand Up @@ -105,6 +116,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
//
// - Obtain the keystore password, if the user desires.
// - Copy the keystore into the `validator_dir`.
// - Register the voting key with the slashing protection database.
// - Add the keystore to the validator definitions file.
//
// Skip keystores that already exist, but exit early if any operation fails.
Expand Down Expand Up @@ -185,6 +197,20 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
fs::copy(&src_keystore, &dest_keystore)
.map_err(|e| format!("Unable to copy keystore: {:?}", e))?;

// Register with slashing protection.
let voting_pubkey = keystore
.public_key()
.ok_or_else(|| format!("Keystore public key is invalid: {}", keystore.pubkey()))?;
slashing_protection
.register_validator(&voting_pubkey)
.map_err(|e| {
format!(
"Error registering validator {}: {:?}",
voting_pubkey.to_hex_string(),
e
)
})?;

eprintln!("Successfully imported keystore.");
num_imported_keystores += 1;

Expand Down
5 changes: 5 additions & 0 deletions account_manager/src/validator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod deposit;
pub mod import;
pub mod list;
pub mod recover;
pub mod slashing_protection;

use crate::VALIDATOR_DIR_FLAG;
use clap::{App, Arg, ArgMatches};
Expand Down Expand Up @@ -33,6 +34,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.subcommand(import::cli_app())
.subcommand(list::cli_app())
.subcommand(recover::cli_app())
.subcommand(slashing_protection::cli_app())
}

pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<(), String> {
Expand All @@ -50,6 +52,9 @@ pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<
(import::CMD, Some(matches)) => import::cli_run(matches, validator_base_dir),
(list::CMD, Some(_)) => list::cli_run(validator_base_dir),
(recover::CMD, Some(matches)) => recover::cli_run(matches, validator_base_dir),
(slashing_protection::CMD, Some(matches)) => {
slashing_protection::cli_run(matches, env, validator_base_dir)
}
(unknown, _) => Err(format!(
"{} does not have a {} command. See --help",
CMD, unknown
Expand Down
137 changes: 137 additions & 0 deletions account_manager/src/validator/slashing_protection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use clap::{App, Arg, ArgMatches};
use environment::Environment;
use slashing_protection::{
interchange::Interchange, SlashingDatabase, SLASHING_PROTECTION_FILENAME,
};
use std::fs::File;
use std::path::PathBuf;
use types::EthSpec;

pub const CMD: &str = "slashing-protection";
pub const IMPORT_CMD: &str = "import";
pub const EXPORT_CMD: &str = "export";

pub const IMPORT_FILE_ARG: &str = "IMPORT-FILE";
pub const EXPORT_FILE_ARG: &str = "EXPORT-FILE";

pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
App::new(CMD)
.about("Import or export slashing protection data to or from another client")
.subcommand(
App::new(IMPORT_CMD)
.about("Import an interchange file")
.arg(
Arg::with_name(IMPORT_FILE_ARG)
.takes_value(true)
.value_name("FILE")
.help("The slashing protection interchange file to import (.json)"),
),
)
.subcommand(
App::new(EXPORT_CMD)
.about("Export an interchange file")
.arg(
Arg::with_name(EXPORT_FILE_ARG)
.takes_value(true)
.value_name("FILE")
.help("The filename to export the interchange file to"),
),
)
}

pub fn cli_run<T: EthSpec>(
matches: &ArgMatches<'_>,
env: Environment<T>,
validator_base_dir: PathBuf,
) -> Result<(), String> {
let slashing_protection_db_path = validator_base_dir.join(SLASHING_PROTECTION_FILENAME);

let genesis_validators_root = env
.testnet
.and_then(|testnet_config| {
Some(
testnet_config
.genesis_state
.as_ref()?
.genesis_validators_root,
)
})
.ok_or_else(|| {
"Unable to get genesis validators root from testnet config, has genesis occurred?"
})?;

match matches.subcommand() {
(IMPORT_CMD, Some(matches)) => {
let import_filename: PathBuf = clap_utils::parse_required(&matches, IMPORT_FILE_ARG)?;
let import_file = File::open(&import_filename).map_err(|e| {
format!(
"Unable to open import file at {}: {:?}",
import_filename.display(),
e
)
})?;

let interchange = Interchange::from_json_reader(&import_file)
.map_err(|e| format!("Error parsing file for import: {:?}", e))?;

let slashing_protection_database =
SlashingDatabase::open_or_create(&slashing_protection_db_path).map_err(|e| {
format!(
"Unable to open database at {}: {:?}",
slashing_protection_db_path.display(),
e
)
})?;

slashing_protection_database
.import_interchange_info(&interchange, genesis_validators_root)
.map_err(|e| {
format!(
"Error during import, no data imported: {:?}\n\
IT IS NOT SAFE TO START VALIDATING",
e
)
})?;

eprintln!("Import completed successfully");

Ok(())
}
(EXPORT_CMD, Some(matches)) => {
let export_filename: PathBuf = clap_utils::parse_required(&matches, EXPORT_FILE_ARG)?;

if !slashing_protection_db_path.exists() {
return Err(format!(
"No slashing protection database exists at: {}",
slashing_protection_db_path.display()
));
}

let slashing_protection_database = SlashingDatabase::open(&slashing_protection_db_path)
.map_err(|e| {
format!(
"Unable to open database at {}: {:?}",
slashing_protection_db_path.display(),
e
)
})?;

let interchange = slashing_protection_database
.export_interchange_info(genesis_validators_root)
.map_err(|e| format!("Error during export: {:?}", e))?;

let output_file = File::create(export_filename)
.map_err(|e| format!("Error creating output file: {:?}", e))?;

interchange
.write_to(&output_file)
.map_err(|e| format!("Error writing output file: {:?}", e))?;

eprintln!("Export completed successfully");

Ok(())
}
("", _) => Err("No subcommand provided, see --help for options".to_string()),
(command, _) => Err(format!("No such subcommand `{}`", command)),
}
}
1 change: 1 addition & 0 deletions lighthouse/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ account_utils = { path = "../common/account_utils" }
[dev-dependencies]
tempfile = "3.1.0"
validator_dir = { path = "../common/validator_dir" }
slashing_protection = { path = "../validator_client/slashing_protection" }
Loading

0 comments on commit 1d278aa

Please sign in to comment.