Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Implement slashing protection interchange format #1544

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
5 changes: 5 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
120 changes: 120 additions & 0 deletions account_manager/src/validator/slashing_protection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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: {:?}", e))?;

Ok(())
michaelsproul marked this conversation as resolved.
Show resolved Hide resolved
}
(EXPORT_CMD, Some(matches)) => {
let export_filename: PathBuf = clap_utils::parse_required(&matches, EXPORT_FILE_ARG)?;

michaelsproul marked this conversation as resolved.
Show resolved Hide resolved
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))?;

Ok(())
michaelsproul marked this conversation as resolved.
Show resolved Hide resolved
}
("", _) => Err("No subcommand provided, see --help for options".to_string()),
(command, _) => Err(format!("No such subcommand `{}`", command)),
}
}
Loading