Implement slashing protection interchange format (#1544)
## 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
This commit is contained in:
parent
22aedda1be
commit
1d278aaa83
2
.gitignore
vendored
2
.gitignore
vendored
@ -6,4 +6,4 @@ target/
|
|||||||
flamegraph.svg
|
flamegraph.svg
|
||||||
perf.data*
|
perf.data*
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
bin/
|
/bin
|
||||||
|
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -23,6 +23,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"rand 0.7.3",
|
"rand 0.7.3",
|
||||||
"rayon",
|
"rayon",
|
||||||
|
"slashing_protection",
|
||||||
"slog",
|
"slog",
|
||||||
"slog-async",
|
"slog-async",
|
||||||
"slog-term",
|
"slog-term",
|
||||||
@ -3057,6 +3058,7 @@ dependencies = [
|
|||||||
"futures 0.3.5",
|
"futures 0.3.5",
|
||||||
"lighthouse_version",
|
"lighthouse_version",
|
||||||
"logging",
|
"logging",
|
||||||
|
"slashing_protection",
|
||||||
"slog",
|
"slog",
|
||||||
"slog-async",
|
"slog-async",
|
||||||
"slog-term",
|
"slog-term",
|
||||||
@ -4935,6 +4937,10 @@ dependencies = [
|
|||||||
"r2d2_sqlite",
|
"r2d2_sqlite",
|
||||||
"rayon",
|
"rayon",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"serde_utils",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tree_hash",
|
"tree_hash",
|
||||||
"types",
|
"types",
|
||||||
|
@ -32,3 +32,4 @@ validator_dir = { path = "../common/validator_dir" }
|
|||||||
tokio = { version = "0.2.21", features = ["full"] }
|
tokio = { version = "0.2.21", features = ["full"] }
|
||||||
eth2_keystore = { path = "../crypto/eth2_keystore" }
|
eth2_keystore = { path = "../crypto/eth2_keystore" }
|
||||||
account_utils = { path = "../common/account_utils" }
|
account_utils = { path = "../common/account_utils" }
|
||||||
|
slashing_protection = { path = "../validator_client/slashing_protection" }
|
||||||
|
@ -10,6 +10,7 @@ use directory::{
|
|||||||
};
|
};
|
||||||
use environment::Environment;
|
use environment::Environment;
|
||||||
use eth2_wallet_manager::WalletManager;
|
use eth2_wallet_manager::WalletManager;
|
||||||
|
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@ -178,6 +179,16 @@ pub fn cli_run<T: EthSpec>(
|
|||||||
.wallet_by_name(&wallet_name)
|
.wallet_by_name(&wallet_name)
|
||||||
.map_err(|e| format!("Unable to open wallet: {:?}", e))?;
|
.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 {
|
for i in 0..n {
|
||||||
let voting_password = random_password();
|
let voting_password = random_password();
|
||||||
let withdrawal_password = random_password();
|
let withdrawal_password = random_password();
|
||||||
@ -190,7 +201,22 @@ pub fn cli_run<T: EthSpec>(
|
|||||||
)
|
)
|
||||||
.map_err(|e| format!("Unable to create validator keys: {:?}", e))?;
|
.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())
|
ValidatorDirBuilder::new(validator_dir.clone(), secrets_dir.clone())
|
||||||
.voting_keystore(keystores.voting, voting_password.as_bytes())
|
.voting_keystore(keystores.voting, voting_password.as_bytes())
|
||||||
@ -200,7 +226,7 @@ pub fn cli_run<T: EthSpec>(
|
|||||||
.build()
|
.build()
|
||||||
.map_err(|e| format!("Unable to build validator directory: {:?}", e))?;
|
.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(())
|
Ok(())
|
||||||
@ -208,14 +234,18 @@ pub fn cli_run<T: EthSpec>(
|
|||||||
|
|
||||||
/// Returns the number of validators that exist in the given `validator_dir`.
|
/// 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,
|
/// 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
|
/// and slashing protection database are validator directories, making it likely to return a higher
|
||||||
/// but never a lower one.
|
/// number than accurate but never a lower one.
|
||||||
fn existing_validator_count<P: AsRef<Path>>(validator_dir: P) -> Result<usize, String> {
|
fn existing_validator_count<P: AsRef<Path>>(validator_dir: P) -> Result<usize, String> {
|
||||||
fs::read_dir(validator_dir.as_ref())
|
fs::read_dir(validator_dir.as_ref())
|
||||||
.map(|iter| {
|
.map(|iter| {
|
||||||
iter.filter_map(|e| e.ok())
|
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()
|
.count()
|
||||||
})
|
})
|
||||||
.map_err(|e| format!("Unable to read {:?}: {}", validator_dir.as_ref(), e))
|
.map_err(|e| format!("Unable to read {:?}: {}", validator_dir.as_ref(), e))
|
||||||
|
@ -9,6 +9,7 @@ use account_utils::{
|
|||||||
ZeroizeString,
|
ZeroizeString,
|
||||||
};
|
};
|
||||||
use clap::{App, Arg, ArgMatches};
|
use clap::{App, Arg, ArgMatches};
|
||||||
|
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
@ -75,6 +76,16 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
|
|||||||
let mut defs = ValidatorDefinitions::open_or_create(&validator_dir)
|
let mut defs = ValidatorDefinitions::open_or_create(&validator_dir)
|
||||||
.map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?;
|
.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.
|
// Collect the paths for the keystores that should be imported.
|
||||||
let keystore_paths = match (keystore, keystores_dir) {
|
let keystore_paths = match (keystore, keystores_dir) {
|
||||||
(Some(keystore), None) => vec![keystore],
|
(Some(keystore), None) => vec![keystore],
|
||||||
@ -105,6 +116,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
|
|||||||
//
|
//
|
||||||
// - Obtain the keystore password, if the user desires.
|
// - Obtain the keystore password, if the user desires.
|
||||||
// - Copy the keystore into the `validator_dir`.
|
// - Copy the keystore into the `validator_dir`.
|
||||||
|
// - Register the voting key with the slashing protection database.
|
||||||
// - Add the keystore to the validator definitions file.
|
// - Add the keystore to the validator definitions file.
|
||||||
//
|
//
|
||||||
// Skip keystores that already exist, but exit early if any operation fails.
|
// Skip keystores that already exist, but exit early if any operation fails.
|
||||||
@ -185,6 +197,20 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin
|
|||||||
fs::copy(&src_keystore, &dest_keystore)
|
fs::copy(&src_keystore, &dest_keystore)
|
||||||
.map_err(|e| format!("Unable to copy keystore: {:?}", e))?;
|
.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.");
|
eprintln!("Successfully imported keystore.");
|
||||||
num_imported_keystores += 1;
|
num_imported_keystores += 1;
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ pub mod deposit;
|
|||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
pub mod recover;
|
pub mod recover;
|
||||||
|
pub mod slashing_protection;
|
||||||
|
|
||||||
use crate::VALIDATOR_DIR_FLAG;
|
use crate::VALIDATOR_DIR_FLAG;
|
||||||
use clap::{App, Arg, ArgMatches};
|
use clap::{App, Arg, ArgMatches};
|
||||||
@ -33,6 +34,7 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
|
|||||||
.subcommand(import::cli_app())
|
.subcommand(import::cli_app())
|
||||||
.subcommand(list::cli_app())
|
.subcommand(list::cli_app())
|
||||||
.subcommand(recover::cli_app())
|
.subcommand(recover::cli_app())
|
||||||
|
.subcommand(slashing_protection::cli_app())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<(), String> {
|
pub fn cli_run<T: EthSpec>(matches: &ArgMatches, env: Environment<T>) -> Result<(), String> {
|
||||||
@ -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),
|
(import::CMD, Some(matches)) => import::cli_run(matches, validator_base_dir),
|
||||||
(list::CMD, Some(_)) => list::cli_run(validator_base_dir),
|
(list::CMD, Some(_)) => list::cli_run(validator_base_dir),
|
||||||
(recover::CMD, Some(matches)) => recover::cli_run(matches, 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!(
|
(unknown, _) => Err(format!(
|
||||||
"{} does not have a {} command. See --help",
|
"{} does not have a {} command. See --help",
|
||||||
CMD, unknown
|
CMD, unknown
|
||||||
|
137
account_manager/src/validator/slashing_protection.rs
Normal file
137
account_manager/src/validator/slashing_protection.rs
Normal file
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
@ -38,3 +38,4 @@ account_utils = { path = "../common/account_utils" }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.1.0"
|
tempfile = "3.1.0"
|
||||||
validator_dir = { path = "../common/validator_dir" }
|
validator_dir = { path = "../common/validator_dir" }
|
||||||
|
slashing_protection = { path = "../validator_client/slashing_protection" }
|
||||||
|
@ -18,6 +18,7 @@ use account_utils::{
|
|||||||
validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions},
|
validator_definitions::{SigningDefinition, ValidatorDefinition, ValidatorDefinitions},
|
||||||
ZeroizeString,
|
ZeroizeString,
|
||||||
};
|
};
|
||||||
|
use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{BufRead, BufReader, Write};
|
use std::io::{BufRead, BufReader, Write};
|
||||||
@ -25,7 +26,7 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::{Command, Output, Stdio};
|
use std::process::{Command, Output, Stdio};
|
||||||
use std::str::from_utf8;
|
use std::str::from_utf8;
|
||||||
use tempfile::{tempdir, TempDir};
|
use tempfile::{tempdir, TempDir};
|
||||||
use types::Keypair;
|
use types::{Keypair, PublicKey};
|
||||||
use validator_dir::ValidatorDir;
|
use validator_dir::ValidatorDir;
|
||||||
|
|
||||||
// TODO: create tests for the `lighthouse account validator deposit` command. This involves getting
|
// TODO: create tests for the `lighthouse account validator deposit` command. This involves getting
|
||||||
@ -69,6 +70,23 @@ fn dir_child_count<P: AsRef<Path>>(dir: P) -> usize {
|
|||||||
fs::read_dir(dir).expect("should read dir").count()
|
fs::read_dir(dir).expect("should read dir").count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the number of 0x-prefixed children in a directory
|
||||||
|
/// i.e. validators in the validators dir.
|
||||||
|
fn dir_validator_count<P: AsRef<Path>>(dir: P) -> usize {
|
||||||
|
fs::read_dir(dir)
|
||||||
|
.unwrap()
|
||||||
|
.filter(|c| {
|
||||||
|
c.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.path()
|
||||||
|
.file_name()
|
||||||
|
.unwrap()
|
||||||
|
.to_string_lossy()
|
||||||
|
.starts_with("0x")
|
||||||
|
})
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
|
||||||
/// Uses `lighthouse account wallet list` to list all wallets.
|
/// Uses `lighthouse account wallet list` to list all wallets.
|
||||||
fn list_wallets<P: AsRef<Path>>(base_dir: P) -> Vec<String> {
|
fn list_wallets<P: AsRef<Path>>(base_dir: P) -> Vec<String> {
|
||||||
let output = output_result(
|
let output = output_result(
|
||||||
@ -328,19 +346,30 @@ fn validator_create() {
|
|||||||
let wallet = TestWallet::new(base_dir.path(), "wally");
|
let wallet = TestWallet::new(base_dir.path(), "wally");
|
||||||
wallet.create_expect_success();
|
wallet.create_expect_success();
|
||||||
|
|
||||||
assert_eq!(dir_child_count(validator_dir.path()), 0);
|
assert_eq!(dir_validator_count(validator_dir.path()), 0);
|
||||||
|
|
||||||
let validator = TestValidator::new(validator_dir.path(), secrets_dir.path(), wallet);
|
let validator = TestValidator::new(validator_dir.path(), secrets_dir.path(), wallet);
|
||||||
|
|
||||||
// Create a validator _without_ storing the withdraw key.
|
// Create a validator _without_ storing the withdraw key.
|
||||||
validator.create_expect_success(COUNT_FLAG, 1, false);
|
let created_validators = validator.create_expect_success(COUNT_FLAG, 1, false);
|
||||||
|
|
||||||
assert_eq!(dir_child_count(validator_dir.path()), 1);
|
// Validator should be registered with slashing protection.
|
||||||
|
check_slashing_protection(
|
||||||
|
&validator_dir,
|
||||||
|
created_validators
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.voting_keypair(&secrets_dir).unwrap().pk),
|
||||||
|
);
|
||||||
|
drop(created_validators);
|
||||||
|
|
||||||
|
// Number of dir entries should be #validators + 1 for the slashing protection DB
|
||||||
|
assert_eq!(dir_validator_count(validator_dir.path()), 1);
|
||||||
|
assert_eq!(dir_child_count(validator_dir.path()), 2);
|
||||||
|
|
||||||
// Create a validator storing the withdraw key.
|
// Create a validator storing the withdraw key.
|
||||||
validator.create_expect_success(COUNT_FLAG, 1, true);
|
validator.create_expect_success(COUNT_FLAG, 1, true);
|
||||||
|
|
||||||
assert_eq!(dir_child_count(validator_dir.path()), 2);
|
assert_eq!(dir_validator_count(validator_dir.path()), 2);
|
||||||
|
|
||||||
// Use the at-most flag with less validators then are in the directory.
|
// Use the at-most flag with less validators then are in the directory.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -348,7 +377,7 @@ fn validator_create() {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(dir_child_count(validator_dir.path()), 2);
|
assert_eq!(dir_validator_count(validator_dir.path()), 2);
|
||||||
|
|
||||||
// Use the at-most flag with the same number of validators that are in the directory.
|
// Use the at-most flag with the same number of validators that are in the directory.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -356,7 +385,7 @@ fn validator_create() {
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(dir_child_count(validator_dir.path()), 2);
|
assert_eq!(dir_validator_count(validator_dir.path()), 2);
|
||||||
|
|
||||||
// Use the at-most flag with two more number of validators than are in the directory.
|
// Use the at-most flag with two more number of validators than are in the directory.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -364,7 +393,7 @@ fn validator_create() {
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(dir_child_count(validator_dir.path()), 4);
|
assert_eq!(dir_validator_count(validator_dir.path()), 4);
|
||||||
|
|
||||||
// Create multiple validators with the count flag.
|
// Create multiple validators with the count flag.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@ -372,7 +401,7 @@ fn validator_create() {
|
|||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(dir_child_count(validator_dir.path()), 6);
|
assert_eq!(dir_validator_count(validator_dir.path()), 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -445,6 +474,9 @@ fn validator_import_launchpad() {
|
|||||||
"not-keystore should not be present in dst dir"
|
"not-keystore should not be present in dst dir"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Validator should be registered with slashing protection.
|
||||||
|
check_slashing_protection(&dst_dir, std::iter::once(keystore.public_key().unwrap()));
|
||||||
|
|
||||||
let defs = ValidatorDefinitions::open(&dst_dir).unwrap();
|
let defs = ValidatorDefinitions::open(&dst_dir).unwrap();
|
||||||
|
|
||||||
let expected_def = ValidatorDefinition {
|
let expected_def = ValidatorDefinition {
|
||||||
@ -462,3 +494,12 @@ fn validator_import_launchpad() {
|
|||||||
"validator defs file should be accurate"
|
"validator defs file should be accurate"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that all of the given pubkeys have been registered with slashing protection.
|
||||||
|
fn check_slashing_protection(validator_dir: &TempDir, pubkeys: impl Iterator<Item = PublicKey>) {
|
||||||
|
let slashing_db_path = validator_dir.path().join(SLASHING_PROTECTION_FILENAME);
|
||||||
|
let slashing_db = SlashingDatabase::open(&slashing_db_path).unwrap();
|
||||||
|
for validator_pk in pubkeys {
|
||||||
|
slashing_db.get_validator_id(&validator_pk).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2
validator_client/slashing_protection/.gitignore
vendored
Normal file
2
validator_client/slashing_protection/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
interchange-tests
|
||||||
|
generated-tests
|
@ -12,6 +12,10 @@ rusqlite = { version = "0.23.1", features = ["bundled"] }
|
|||||||
r2d2 = "0.8.8"
|
r2d2 = "0.8.8"
|
||||||
r2d2_sqlite = "0.16.0"
|
r2d2_sqlite = "0.16.0"
|
||||||
parking_lot = "0.11.0"
|
parking_lot = "0.11.0"
|
||||||
|
serde = "1.0.110"
|
||||||
|
serde_derive = "1.0.110"
|
||||||
|
serde_json = "1.0.52"
|
||||||
|
serde_utils = { path = "../../consensus/serde_utils" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rayon = "1.3.0"
|
rayon = "1.3.0"
|
||||||
|
28
validator_client/slashing_protection/Makefile
Normal file
28
validator_client/slashing_protection/Makefile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
TESTS_TAG := ac393b815b356c95569c028c215232b512df583d
|
||||||
|
GENERATE_DIR := generated-tests
|
||||||
|
OUTPUT_DIR := interchange-tests
|
||||||
|
TARBALL := $(OUTPUT_DIR)-$(TESTS_TAG).tar.gz
|
||||||
|
ARCHIVE_URL := https://github.com/eth2-clients/slashing-protection-interchange-tests/tarball/$(TESTS_TAG)
|
||||||
|
|
||||||
|
$(OUTPUT_DIR): $(TARBALL)
|
||||||
|
rm -rf $@
|
||||||
|
mkdir $@
|
||||||
|
tar --strip-components=1 -xzf $^ -C $@
|
||||||
|
|
||||||
|
$(TARBALL):
|
||||||
|
wget $(ARCHIVE_URL) -O $@
|
||||||
|
|
||||||
|
clean-test-files:
|
||||||
|
rm -rf $(OUTPUT_DIR)
|
||||||
|
|
||||||
|
clean-archives:
|
||||||
|
rm -f $(TARBALL)
|
||||||
|
|
||||||
|
generate:
|
||||||
|
rm -rf $(GENERATE_DIR)
|
||||||
|
cargo run --release --bin test_generator -- $(GENERATE_DIR)
|
||||||
|
|
||||||
|
clean: clean-test-files clean-archives
|
||||||
|
|
||||||
|
.PHONY: clean clean-archives clean-test-files generate
|
||||||
|
|
7
validator_client/slashing_protection/build.rs
Normal file
7
validator_client/slashing_protection/build.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fn main() {
|
||||||
|
let exit_status = std::process::Command::new("make")
|
||||||
|
.current_dir(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||||
|
.status()
|
||||||
|
.unwrap();
|
||||||
|
assert!(exit_status.success());
|
||||||
|
}
|
128
validator_client/slashing_protection/src/bin/test_generator.rs
Normal file
128
validator_client/slashing_protection/src/bin/test_generator.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
use slashing_protection::interchange::{
|
||||||
|
CompleteInterchangeData, Interchange, InterchangeFormat, InterchangeMetadata,
|
||||||
|
SignedAttestation, SignedBlock,
|
||||||
|
};
|
||||||
|
use slashing_protection::interchange_test::TestCase;
|
||||||
|
use slashing_protection::test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT};
|
||||||
|
use slashing_protection::SUPPORTED_INTERCHANGE_FORMAT_VERSION;
|
||||||
|
use std::fs::{self, File};
|
||||||
|
use std::path::Path;
|
||||||
|
use types::{Epoch, Hash256, Slot};
|
||||||
|
|
||||||
|
fn metadata(genesis_validators_root: Hash256) -> InterchangeMetadata {
|
||||||
|
InterchangeMetadata {
|
||||||
|
interchange_format: InterchangeFormat::Complete,
|
||||||
|
interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION,
|
||||||
|
genesis_validators_root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn interchange(data: Vec<(usize, Vec<u64>, Vec<(u64, u64)>)>) -> Interchange {
|
||||||
|
let data = data
|
||||||
|
.into_iter()
|
||||||
|
.map(|(pk, blocks, attestations)| CompleteInterchangeData {
|
||||||
|
pubkey: pubkey(pk),
|
||||||
|
signed_blocks: blocks
|
||||||
|
.into_iter()
|
||||||
|
.map(|slot| SignedBlock {
|
||||||
|
slot: Slot::new(slot),
|
||||||
|
signing_root: None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
signed_attestations: attestations
|
||||||
|
.into_iter()
|
||||||
|
.map(|(source, target)| SignedAttestation {
|
||||||
|
source_epoch: Epoch::new(source),
|
||||||
|
target_epoch: Epoch::new(target),
|
||||||
|
signing_root: None,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Interchange {
|
||||||
|
metadata: metadata(DEFAULT_GENESIS_VALIDATORS_ROOT),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let single_validator_blocks =
|
||||||
|
vec![(0, 32, false), (0, 33, true), (0, 31, false), (0, 1, false)];
|
||||||
|
let single_validator_attestations = vec![
|
||||||
|
(0, 3, 4, false),
|
||||||
|
(0, 14, 19, false),
|
||||||
|
(0, 15, 20, false),
|
||||||
|
(0, 16, 20, false),
|
||||||
|
(0, 15, 21, true),
|
||||||
|
];
|
||||||
|
|
||||||
|
let tests = vec![
|
||||||
|
TestCase::new(
|
||||||
|
"single_validator_import_only",
|
||||||
|
interchange(vec![(0, vec![22], vec![(0, 2)])]),
|
||||||
|
),
|
||||||
|
TestCase::new(
|
||||||
|
"single_validator_single_block",
|
||||||
|
interchange(vec![(0, vec![32], vec![])]),
|
||||||
|
)
|
||||||
|
.with_blocks(single_validator_blocks.clone()),
|
||||||
|
TestCase::new(
|
||||||
|
"single_validator_single_attestation",
|
||||||
|
interchange(vec![(0, vec![], vec![(15, 20)])]),
|
||||||
|
)
|
||||||
|
.with_attestations(single_validator_attestations.clone()),
|
||||||
|
TestCase::new(
|
||||||
|
"single_validator_single_block_and_attestation",
|
||||||
|
interchange(vec![(0, vec![32], vec![(15, 20)])]),
|
||||||
|
)
|
||||||
|
.with_blocks(single_validator_blocks)
|
||||||
|
.with_attestations(single_validator_attestations),
|
||||||
|
TestCase::new(
|
||||||
|
"single_validator_genesis_attestation",
|
||||||
|
interchange(vec![(0, vec![], vec![(0, 0)])]),
|
||||||
|
)
|
||||||
|
.with_attestations(vec![(0, 0, 0, false)]),
|
||||||
|
TestCase::new(
|
||||||
|
"single_validator_multiple_blocks_and_attestations",
|
||||||
|
interchange(vec![(
|
||||||
|
0,
|
||||||
|
vec![2, 3, 10, 1200],
|
||||||
|
vec![(10, 11), (12, 13), (20, 24)],
|
||||||
|
)]),
|
||||||
|
)
|
||||||
|
.with_blocks(vec![
|
||||||
|
(0, 1, false),
|
||||||
|
(0, 2, false),
|
||||||
|
(0, 3, false),
|
||||||
|
(0, 10, false),
|
||||||
|
(0, 1200, false),
|
||||||
|
(0, 4, true),
|
||||||
|
(0, 256, true),
|
||||||
|
(0, 1201, true),
|
||||||
|
])
|
||||||
|
.with_attestations(vec![
|
||||||
|
(0, 9, 10, false),
|
||||||
|
(0, 12, 13, false),
|
||||||
|
(0, 11, 14, false),
|
||||||
|
(0, 21, 22, false),
|
||||||
|
(0, 10, 24, false),
|
||||||
|
(0, 11, 12, true),
|
||||||
|
(0, 20, 25, true),
|
||||||
|
]),
|
||||||
|
TestCase::new("wrong_genesis_validators_root", interchange(vec![]))
|
||||||
|
.gvr(Hash256::from_low_u64_be(1))
|
||||||
|
.should_fail(),
|
||||||
|
];
|
||||||
|
// TODO: multi-validator test
|
||||||
|
|
||||||
|
let args = std::env::args().collect::<Vec<_>>();
|
||||||
|
let output_dir = Path::new(&args[1]);
|
||||||
|
fs::create_dir_all(output_dir).unwrap();
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
test.run();
|
||||||
|
let f = File::create(output_dir.join(format!("{}.json", test.name))).unwrap();
|
||||||
|
serde_json::to_writer(f, &test).unwrap();
|
||||||
|
}
|
||||||
|
}
|
84
validator_client/slashing_protection/src/interchange.rs
Normal file
84
validator_client/slashing_protection/src/interchange.rs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
use types::{Epoch, Hash256, PublicKey, Slot};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum InterchangeFormat {
|
||||||
|
Complete,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct InterchangeMetadata {
|
||||||
|
pub interchange_format: InterchangeFormat,
|
||||||
|
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
|
||||||
|
pub interchange_format_version: u64,
|
||||||
|
pub genesis_validators_root: Hash256,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct CompleteInterchangeData {
|
||||||
|
pub pubkey: PublicKey,
|
||||||
|
pub signed_blocks: Vec<SignedBlock>,
|
||||||
|
pub signed_attestations: Vec<SignedAttestation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct SignedBlock {
|
||||||
|
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
|
||||||
|
pub slot: Slot,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub signing_root: Option<Hash256>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct SignedAttestation {
|
||||||
|
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
|
||||||
|
pub source_epoch: Epoch,
|
||||||
|
#[serde(with = "serde_utils::quoted_u64::require_quotes")]
|
||||||
|
pub target_epoch: Epoch,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub signing_root: Option<Hash256>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct Interchange {
|
||||||
|
pub metadata: InterchangeMetadata,
|
||||||
|
pub data: Vec<CompleteInterchangeData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interchange {
|
||||||
|
pub fn from_json_str(json: &str) -> Result<Self, serde_json::Error> {
|
||||||
|
serde_json::from_str(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_json_reader(reader: impl std::io::Read) -> Result<Self, serde_json::Error> {
|
||||||
|
serde_json::from_reader(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_to(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
|
||||||
|
serde_json::to_writer(writer, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Do these two `Interchange`s contain the same data (ignoring ordering)?
|
||||||
|
pub fn equiv(&self, other: &Self) -> bool {
|
||||||
|
let self_set = HashSet::<_>::from_iter(self.data.iter());
|
||||||
|
let other_set = HashSet::<_>::from_iter(other.data.iter());
|
||||||
|
self.metadata == other.metadata && self_set == other_set
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of entries in `data`.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.data.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the `data` part of the interchange completely empty?
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
}
|
151
validator_client/slashing_protection/src/interchange_test.rs
Normal file
151
validator_client/slashing_protection/src/interchange_test.rs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
use crate::{
|
||||||
|
interchange::Interchange,
|
||||||
|
test_utils::{pubkey, DEFAULT_GENESIS_VALIDATORS_ROOT},
|
||||||
|
SlashingDatabase,
|
||||||
|
};
|
||||||
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
use tempfile::tempdir;
|
||||||
|
use types::{Epoch, Hash256, PublicKey, Slot};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct TestCase {
|
||||||
|
pub name: String,
|
||||||
|
pub should_succeed: bool,
|
||||||
|
pub genesis_validators_root: Hash256,
|
||||||
|
pub interchange: Interchange,
|
||||||
|
pub blocks: Vec<TestBlock>,
|
||||||
|
pub attestations: Vec<TestAttestation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct TestBlock {
|
||||||
|
pub pubkey: PublicKey,
|
||||||
|
pub slot: Slot,
|
||||||
|
pub should_succeed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct TestAttestation {
|
||||||
|
pub pubkey: PublicKey,
|
||||||
|
pub source_epoch: Epoch,
|
||||||
|
pub target_epoch: Epoch,
|
||||||
|
pub should_succeed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestCase {
|
||||||
|
pub fn new(name: &str, interchange: Interchange) -> Self {
|
||||||
|
TestCase {
|
||||||
|
name: name.into(),
|
||||||
|
should_succeed: true,
|
||||||
|
genesis_validators_root: DEFAULT_GENESIS_VALIDATORS_ROOT,
|
||||||
|
interchange,
|
||||||
|
blocks: vec![],
|
||||||
|
attestations: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gvr(mut self, genesis_validators_root: Hash256) -> Self {
|
||||||
|
self.genesis_validators_root = genesis_validators_root;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn should_fail(mut self) -> Self {
|
||||||
|
self.should_succeed = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_blocks(mut self, blocks: impl IntoIterator<Item = (usize, u64, bool)>) -> Self {
|
||||||
|
self.blocks.extend(
|
||||||
|
blocks
|
||||||
|
.into_iter()
|
||||||
|
.map(|(pk, slot, should_succeed)| TestBlock {
|
||||||
|
pubkey: pubkey(pk),
|
||||||
|
slot: Slot::new(slot),
|
||||||
|
should_succeed,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_attestations(
|
||||||
|
mut self,
|
||||||
|
attestations: impl IntoIterator<Item = (usize, u64, u64, bool)>,
|
||||||
|
) -> Self {
|
||||||
|
self.attestations.extend(attestations.into_iter().map(
|
||||||
|
|(pk, source, target, should_succeed)| TestAttestation {
|
||||||
|
pubkey: pubkey(pk),
|
||||||
|
source_epoch: Epoch::new(source),
|
||||||
|
target_epoch: Epoch::new(target),
|
||||||
|
should_succeed,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self) {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
|
||||||
|
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
|
||||||
|
|
||||||
|
match slashing_db.import_interchange_info(&self.interchange, self.genesis_validators_root) {
|
||||||
|
Ok(()) if !self.should_succeed => {
|
||||||
|
panic!(
|
||||||
|
"test `{}` succeeded on import when it should have failed",
|
||||||
|
self.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) if self.should_succeed => {
|
||||||
|
panic!(
|
||||||
|
"test `{}` failed on import when it should have succeeded, error: {:?}",
|
||||||
|
self.name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, block) in self.blocks.iter().enumerate() {
|
||||||
|
match slashing_db.check_and_insert_block_signing_root(
|
||||||
|
&block.pubkey,
|
||||||
|
block.slot,
|
||||||
|
Hash256::random(),
|
||||||
|
) {
|
||||||
|
Ok(safe) if !block.should_succeed => {
|
||||||
|
panic!(
|
||||||
|
"block {} from `{}` succeeded when it should have failed: {:?}",
|
||||||
|
i, self.name, safe
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) if block.should_succeed => {
|
||||||
|
panic!(
|
||||||
|
"block {} from `{}` failed when it should have succeeded: {:?}",
|
||||||
|
i, self.name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, att) in self.attestations.iter().enumerate() {
|
||||||
|
match slashing_db.check_and_insert_attestation_signing_root(
|
||||||
|
&att.pubkey,
|
||||||
|
att.source_epoch,
|
||||||
|
att.target_epoch,
|
||||||
|
Hash256::random(),
|
||||||
|
) {
|
||||||
|
Ok(safe) if !att.should_succeed => {
|
||||||
|
panic!(
|
||||||
|
"attestation {} from `{}` succeeded when it should have failed: {:?}",
|
||||||
|
i, self.name, safe
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) if att.should_succeed => {
|
||||||
|
panic!(
|
||||||
|
"attestation {} from `{}` failed when it should have succeeded: {:?}",
|
||||||
|
i, self.name, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,25 @@
|
|||||||
mod attestation_tests;
|
mod attestation_tests;
|
||||||
mod block_tests;
|
mod block_tests;
|
||||||
|
pub mod interchange;
|
||||||
|
pub mod interchange_test;
|
||||||
mod parallel_tests;
|
mod parallel_tests;
|
||||||
|
mod registration_tests;
|
||||||
mod signed_attestation;
|
mod signed_attestation;
|
||||||
mod signed_block;
|
mod signed_block;
|
||||||
mod slashing_database;
|
mod slashing_database;
|
||||||
mod test_utils;
|
pub mod test_utils;
|
||||||
|
|
||||||
pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation};
|
pub use crate::signed_attestation::{InvalidAttestation, SignedAttestation};
|
||||||
pub use crate::signed_block::{InvalidBlock, SignedBlock};
|
pub use crate::signed_block::{InvalidBlock, SignedBlock};
|
||||||
pub use crate::slashing_database::SlashingDatabase;
|
pub use crate::slashing_database::{SlashingDatabase, SUPPORTED_INTERCHANGE_FORMAT_VERSION};
|
||||||
use rusqlite::Error as SQLError;
|
use rusqlite::Error as SQLError;
|
||||||
use std::io::{Error as IOError, ErrorKind};
|
use std::io::{Error as IOError, ErrorKind};
|
||||||
use std::string::ToString;
|
use std::string::ToString;
|
||||||
use types::{Hash256, PublicKey};
|
use types::{Hash256, PublicKey};
|
||||||
|
|
||||||
|
/// The filename within the `validators` directory that contains the slashing protection DB.
|
||||||
|
pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite";
|
||||||
|
|
||||||
/// The attestation or block is not safe to sign.
|
/// The attestation or block is not safe to sign.
|
||||||
///
|
///
|
||||||
/// This could be because it's slashable, or because an error occurred.
|
/// This could be because it's slashable, or because an error occurred.
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
#![cfg(test)]
|
||||||
|
|
||||||
|
use crate::test_utils::*;
|
||||||
|
use crate::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn double_register_validators() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let slashing_db_file = dir.path().join("slashing_protection.sqlite");
|
||||||
|
let slashing_db = SlashingDatabase::create(&slashing_db_file).unwrap();
|
||||||
|
|
||||||
|
let num_validators = 100u32;
|
||||||
|
let pubkeys = (0..num_validators as usize).map(pubkey).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let get_validator_ids = || {
|
||||||
|
pubkeys
|
||||||
|
.iter()
|
||||||
|
.map(|pk| slashing_db.get_validator_id(pk).unwrap())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(slashing_db.num_validator_rows().unwrap(), 0);
|
||||||
|
|
||||||
|
slashing_db.register_validators(pubkeys.iter()).unwrap();
|
||||||
|
assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators);
|
||||||
|
let validator_ids = get_validator_ids();
|
||||||
|
|
||||||
|
slashing_db.register_validators(pubkeys.iter()).unwrap();
|
||||||
|
assert_eq!(slashing_db.num_validator_rows().unwrap(), num_validators);
|
||||||
|
assert_eq!(validator_ids, get_validator_ids());
|
||||||
|
}
|
@ -20,6 +20,18 @@ pub enum InvalidAttestation {
|
|||||||
PrevSurroundsNew { prev: SignedAttestation },
|
PrevSurroundsNew { prev: SignedAttestation },
|
||||||
/// The attestation is invalid because its source epoch is greater than its target epoch.
|
/// The attestation is invalid because its source epoch is greater than its target epoch.
|
||||||
SourceExceedsTarget,
|
SourceExceedsTarget,
|
||||||
|
/// The attestation is invalid because its source epoch is less than the lower bound on source
|
||||||
|
/// epochs for this validator.
|
||||||
|
SourceLessThanLowerBound {
|
||||||
|
source_epoch: Epoch,
|
||||||
|
bound_epoch: Epoch,
|
||||||
|
},
|
||||||
|
/// The attestation is invalid because its target epoch is less than or equal to the lower
|
||||||
|
/// bound on target epochs for this validator.
|
||||||
|
TargetLessThanOrEqLowerBound {
|
||||||
|
target_epoch: Epoch,
|
||||||
|
bound_epoch: Epoch,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignedAttestation {
|
impl SignedAttestation {
|
||||||
|
@ -12,6 +12,7 @@ pub struct SignedBlock {
|
|||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
pub enum InvalidBlock {
|
pub enum InvalidBlock {
|
||||||
DoubleBlockProposal(SignedBlock),
|
DoubleBlockProposal(SignedBlock),
|
||||||
|
SlotViolatesLowerBound { block_slot: Slot, bound_slot: Slot },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignedBlock {
|
impl SignedBlock {
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
|
use crate::interchange::{
|
||||||
|
CompleteInterchangeData, Interchange, InterchangeFormat, InterchangeMetadata,
|
||||||
|
SignedAttestation as InterchangeAttestation, SignedBlock as InterchangeBlock,
|
||||||
|
};
|
||||||
use crate::signed_attestation::InvalidAttestation;
|
use crate::signed_attestation::InvalidAttestation;
|
||||||
use crate::signed_block::InvalidBlock;
|
use crate::signed_block::InvalidBlock;
|
||||||
use crate::{NotSafe, Safe, SignedAttestation, SignedBlock};
|
use crate::{hash256_from_row, NotSafe, Safe, SignedAttestation, SignedBlock};
|
||||||
use r2d2_sqlite::SqliteConnectionManager;
|
use r2d2_sqlite::SqliteConnectionManager;
|
||||||
use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior};
|
use rusqlite::{params, OptionalExtension, Transaction, TransactionBehavior};
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use types::{AttestationData, BeaconBlockHeader, Hash256, PublicKey, SignedRoot};
|
use types::{AttestationData, BeaconBlockHeader, Epoch, Hash256, PublicKey, SignedRoot, Slot};
|
||||||
|
|
||||||
type Pool = r2d2::Pool<SqliteConnectionManager>;
|
type Pool = r2d2::Pool<SqliteConnectionManager>;
|
||||||
|
|
||||||
@ -20,6 +24,9 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100);
|
pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
|
|
||||||
|
/// Supported version of the interchange format.
|
||||||
|
pub const SUPPORTED_INTERCHANGE_FORMAT_VERSION: u64 = 4;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SlashingDatabase {
|
pub struct SlashingDatabase {
|
||||||
conn_pool: Pool,
|
conn_pool: Pool,
|
||||||
@ -52,7 +59,7 @@ impl SlashingDatabase {
|
|||||||
conn.execute(
|
conn.execute(
|
||||||
"CREATE TABLE validators (
|
"CREATE TABLE validators (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
public_key BLOB NOT NULL
|
public_key BLOB NOT NULL UNIQUE
|
||||||
)",
|
)",
|
||||||
params![],
|
params![],
|
||||||
)?;
|
)?;
|
||||||
@ -144,15 +151,25 @@ impl SlashingDatabase {
|
|||||||
) -> Result<(), NotSafe> {
|
) -> Result<(), NotSafe> {
|
||||||
let mut conn = self.conn_pool.get()?;
|
let mut conn = self.conn_pool.get()?;
|
||||||
let txn = conn.transaction()?;
|
let txn = conn.transaction()?;
|
||||||
{
|
self.register_validators_in_txn(public_keys, &txn)?;
|
||||||
let mut stmt = txn.prepare("INSERT INTO validators (public_key) VALUES (?1)")?;
|
txn.commit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
for pubkey in public_keys {
|
/// Register multiple validators inside the given transaction.
|
||||||
|
///
|
||||||
|
/// The caller must commit the transaction for the changes to be persisted.
|
||||||
|
pub fn register_validators_in_txn<'a>(
|
||||||
|
&self,
|
||||||
|
public_keys: impl Iterator<Item = &'a PublicKey>,
|
||||||
|
txn: &Transaction,
|
||||||
|
) -> Result<(), NotSafe> {
|
||||||
|
let mut stmt = txn.prepare("INSERT INTO validators (public_key) VALUES (?1)")?;
|
||||||
|
for pubkey in public_keys {
|
||||||
|
if self.get_validator_id_opt(&txn, pubkey)?.is_none() {
|
||||||
stmt.execute(&[pubkey.to_hex_string()])?;
|
stmt.execute(&[pubkey.to_hex_string()])?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
txn.commit()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,14 +177,34 @@ impl SlashingDatabase {
|
|||||||
///
|
///
|
||||||
/// This is NOT the same as a validator index, and depends on the ordering that validators
|
/// This is NOT the same as a validator index, and depends on the ordering that validators
|
||||||
/// are registered with the slashing protection database (and may vary between machines).
|
/// are registered with the slashing protection database (and may vary between machines).
|
||||||
fn get_validator_id(txn: &Transaction, public_key: &PublicKey) -> Result<i64, NotSafe> {
|
pub fn get_validator_id(&self, public_key: &PublicKey) -> Result<i64, NotSafe> {
|
||||||
txn.query_row(
|
let mut conn = self.conn_pool.get()?;
|
||||||
"SELECT id FROM validators WHERE public_key = ?1",
|
let txn = conn.transaction()?;
|
||||||
params![&public_key.to_hex_string()],
|
self.get_validator_id_in_txn(&txn, public_key)
|
||||||
|row| row.get(0),
|
}
|
||||||
)
|
|
||||||
.optional()?
|
fn get_validator_id_in_txn(
|
||||||
.ok_or_else(|| NotSafe::UnregisteredValidator(public_key.clone()))
|
&self,
|
||||||
|
txn: &Transaction,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
) -> Result<i64, NotSafe> {
|
||||||
|
self.get_validator_id_opt(txn, public_key)?
|
||||||
|
.ok_or_else(|| NotSafe::UnregisteredValidator(public_key.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional version of `get_validator_id`.
|
||||||
|
fn get_validator_id_opt(
|
||||||
|
&self,
|
||||||
|
txn: &Transaction,
|
||||||
|
public_key: &PublicKey,
|
||||||
|
) -> Result<Option<i64>, NotSafe> {
|
||||||
|
Ok(txn
|
||||||
|
.query_row(
|
||||||
|
"SELECT id FROM validators WHERE public_key = ?1",
|
||||||
|
params![&public_key.to_hex_string()],
|
||||||
|
|row| row.get(0),
|
||||||
|
)
|
||||||
|
.optional()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check a block proposal from `validator_pubkey` for slash safety.
|
/// Check a block proposal from `validator_pubkey` for slash safety.
|
||||||
@ -175,10 +212,10 @@ impl SlashingDatabase {
|
|||||||
&self,
|
&self,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
validator_pubkey: &PublicKey,
|
validator_pubkey: &PublicKey,
|
||||||
block_header: &BeaconBlockHeader,
|
slot: Slot,
|
||||||
domain: Hash256,
|
signing_root: Hash256,
|
||||||
) -> Result<Safe, NotSafe> {
|
) -> Result<Safe, NotSafe> {
|
||||||
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
|
let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?;
|
||||||
|
|
||||||
let existing_block = txn
|
let existing_block = txn
|
||||||
.prepare(
|
.prepare(
|
||||||
@ -186,25 +223,37 @@ impl SlashingDatabase {
|
|||||||
FROM signed_blocks
|
FROM signed_blocks
|
||||||
WHERE validator_id = ?1 AND slot = ?2",
|
WHERE validator_id = ?1 AND slot = ?2",
|
||||||
)?
|
)?
|
||||||
.query_row(
|
.query_row(params![validator_id, slot], SignedBlock::from_row)
|
||||||
params![validator_id, block_header.slot],
|
|
||||||
SignedBlock::from_row,
|
|
||||||
)
|
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
||||||
if let Some(existing_block) = existing_block {
|
if let Some(existing_block) = existing_block {
|
||||||
if existing_block.signing_root == block_header.signing_root(domain) {
|
if existing_block.signing_root == signing_root {
|
||||||
// Same slot and same hash -> we're re-broadcasting a previously signed block
|
// Same slot and same hash -> we're re-broadcasting a previously signed block
|
||||||
Ok(Safe::SameData)
|
return Ok(Safe::SameData);
|
||||||
} else {
|
} else {
|
||||||
// Same epoch but not the same hash -> it's a DoubleBlockProposal
|
// Same epoch but not the same hash -> it's a DoubleBlockProposal
|
||||||
Err(NotSafe::InvalidBlock(InvalidBlock::DoubleBlockProposal(
|
return Err(NotSafe::InvalidBlock(InvalidBlock::DoubleBlockProposal(
|
||||||
existing_block,
|
existing_block,
|
||||||
)))
|
)));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Ok(Safe::Valid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let min_slot = txn
|
||||||
|
.prepare("SELECT MIN(slot) FROM signed_blocks WHERE validator_id = ?1")?
|
||||||
|
.query_row(params![validator_id], |row| row.get(0))?;
|
||||||
|
|
||||||
|
if let Some(min_slot) = min_slot {
|
||||||
|
if slot <= min_slot {
|
||||||
|
return Err(NotSafe::InvalidBlock(
|
||||||
|
InvalidBlock::SlotViolatesLowerBound {
|
||||||
|
block_slot: slot,
|
||||||
|
bound_slot: min_slot,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Safe::Valid)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check an attestation from `validator_pubkey` for slash safety.
|
/// Check an attestation from `validator_pubkey` for slash safety.
|
||||||
@ -212,12 +261,10 @@ impl SlashingDatabase {
|
|||||||
&self,
|
&self,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
validator_pubkey: &PublicKey,
|
validator_pubkey: &PublicKey,
|
||||||
attestation: &AttestationData,
|
att_source_epoch: Epoch,
|
||||||
domain: Hash256,
|
att_target_epoch: Epoch,
|
||||||
|
att_signing_root: Hash256,
|
||||||
) -> Result<Safe, NotSafe> {
|
) -> Result<Safe, NotSafe> {
|
||||||
let att_source_epoch = attestation.source.epoch;
|
|
||||||
let att_target_epoch = attestation.target.epoch;
|
|
||||||
|
|
||||||
// Although it's not required to avoid slashing, we disallow attestations
|
// Although it's not required to avoid slashing, we disallow attestations
|
||||||
// which are obviously invalid by virtue of their source epoch exceeding their target.
|
// which are obviously invalid by virtue of their source epoch exceeding their target.
|
||||||
if att_source_epoch > att_target_epoch {
|
if att_source_epoch > att_target_epoch {
|
||||||
@ -226,10 +273,10 @@ impl SlashingDatabase {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
|
let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?;
|
||||||
|
|
||||||
// 1. Check for a double vote. Namely, an existing attestation with the same target epoch,
|
// Check for a double vote. Namely, an existing attestation with the same target epoch,
|
||||||
// and a different signing root.
|
// and a different signing root.
|
||||||
let same_target_att = txn
|
let same_target_att = txn
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT source_epoch, target_epoch, signing_root
|
"SELECT source_epoch, target_epoch, signing_root
|
||||||
@ -245,7 +292,7 @@ impl SlashingDatabase {
|
|||||||
if let Some(existing_attestation) = same_target_att {
|
if let Some(existing_attestation) = same_target_att {
|
||||||
// If the new attestation is identical to the existing attestation, then we already
|
// If the new attestation is identical to the existing attestation, then we already
|
||||||
// know that it is safe, and can return immediately.
|
// know that it is safe, and can return immediately.
|
||||||
if existing_attestation.signing_root == attestation.signing_root(domain) {
|
if existing_attestation.signing_root == att_signing_root {
|
||||||
return Ok(Safe::SameData);
|
return Ok(Safe::SameData);
|
||||||
// Otherwise if the hashes are different, this is a double vote.
|
// Otherwise if the hashes are different, this is a double vote.
|
||||||
} else {
|
} else {
|
||||||
@ -255,7 +302,7 @@ impl SlashingDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check that no previous vote is surrounding `attestation`.
|
// Check that no previous vote is surrounding `attestation`.
|
||||||
// If there is a surrounding attestation, we only return the most recent one.
|
// If there is a surrounding attestation, we only return the most recent one.
|
||||||
let surrounding_attestation = txn
|
let surrounding_attestation = txn
|
||||||
.prepare(
|
.prepare(
|
||||||
@ -277,7 +324,7 @@ impl SlashingDatabase {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check that no previous vote is surrounded by `attestation`.
|
// Check that no previous vote is surrounded by `attestation`.
|
||||||
// If there is a surrounded attestation, we only return the most recent one.
|
// If there is a surrounded attestation, we only return the most recent one.
|
||||||
let surrounded_attestation = txn
|
let surrounded_attestation = txn
|
||||||
.prepare(
|
.prepare(
|
||||||
@ -299,6 +346,39 @@ impl SlashingDatabase {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check lower bounds: ensure that source is greater than or equal to min source,
|
||||||
|
// and target is greater than min target. This allows pruning, and compatibility
|
||||||
|
// with the interchange format.
|
||||||
|
let min_source = txn
|
||||||
|
.prepare("SELECT MIN(source_epoch) FROM signed_attestations WHERE validator_id = ?1")?
|
||||||
|
.query_row(params![validator_id], |row| row.get(0))?;
|
||||||
|
|
||||||
|
if let Some(min_source) = min_source {
|
||||||
|
if att_source_epoch < min_source {
|
||||||
|
return Err(NotSafe::InvalidAttestation(
|
||||||
|
InvalidAttestation::SourceLessThanLowerBound {
|
||||||
|
source_epoch: att_source_epoch,
|
||||||
|
bound_epoch: min_source,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let min_target = txn
|
||||||
|
.prepare("SELECT MIN(target_epoch) FROM signed_attestations WHERE validator_id = ?1")?
|
||||||
|
.query_row(params![validator_id], |row| row.get(0))?;
|
||||||
|
|
||||||
|
if let Some(min_target) = min_target {
|
||||||
|
if att_target_epoch <= min_target {
|
||||||
|
return Err(NotSafe::InvalidAttestation(
|
||||||
|
InvalidAttestation::TargetLessThanOrEqLowerBound {
|
||||||
|
target_epoch: att_target_epoch,
|
||||||
|
bound_epoch: min_target,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Everything has been checked, return Valid
|
// Everything has been checked, return Valid
|
||||||
Ok(Safe::Valid)
|
Ok(Safe::Valid)
|
||||||
}
|
}
|
||||||
@ -311,19 +391,15 @@ impl SlashingDatabase {
|
|||||||
&self,
|
&self,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
validator_pubkey: &PublicKey,
|
validator_pubkey: &PublicKey,
|
||||||
block_header: &BeaconBlockHeader,
|
slot: Slot,
|
||||||
domain: Hash256,
|
signing_root: Hash256,
|
||||||
) -> Result<(), NotSafe> {
|
) -> Result<(), NotSafe> {
|
||||||
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
|
let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?;
|
||||||
|
|
||||||
txn.execute(
|
txn.execute(
|
||||||
"INSERT INTO signed_blocks (validator_id, slot, signing_root)
|
"INSERT INTO signed_blocks (validator_id, slot, signing_root)
|
||||||
VALUES (?1, ?2, ?3)",
|
VALUES (?1, ?2, ?3)",
|
||||||
params![
|
params![validator_id, slot, signing_root.as_bytes()],
|
||||||
validator_id,
|
|
||||||
block_header.slot,
|
|
||||||
block_header.signing_root(domain).as_bytes()
|
|
||||||
],
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -336,19 +412,20 @@ impl SlashingDatabase {
|
|||||||
&self,
|
&self,
|
||||||
txn: &Transaction,
|
txn: &Transaction,
|
||||||
validator_pubkey: &PublicKey,
|
validator_pubkey: &PublicKey,
|
||||||
attestation: &AttestationData,
|
att_source_epoch: Epoch,
|
||||||
domain: Hash256,
|
att_target_epoch: Epoch,
|
||||||
|
att_signing_root: Hash256,
|
||||||
) -> Result<(), NotSafe> {
|
) -> Result<(), NotSafe> {
|
||||||
let validator_id = Self::get_validator_id(txn, validator_pubkey)?;
|
let validator_id = self.get_validator_id_in_txn(txn, validator_pubkey)?;
|
||||||
|
|
||||||
txn.execute(
|
txn.execute(
|
||||||
"INSERT INTO signed_attestations (validator_id, source_epoch, target_epoch, signing_root)
|
"INSERT INTO signed_attestations (validator_id, source_epoch, target_epoch, signing_root)
|
||||||
VALUES (?1, ?2, ?3, ?4)",
|
VALUES (?1, ?2, ?3, ?4)",
|
||||||
params![
|
params![
|
||||||
validator_id,
|
validator_id,
|
||||||
attestation.source.epoch,
|
att_source_epoch,
|
||||||
attestation.target.epoch,
|
att_target_epoch,
|
||||||
attestation.signing_root(domain).as_bytes()
|
att_signing_root.as_bytes()
|
||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -365,17 +442,46 @@ impl SlashingDatabase {
|
|||||||
validator_pubkey: &PublicKey,
|
validator_pubkey: &PublicKey,
|
||||||
block_header: &BeaconBlockHeader,
|
block_header: &BeaconBlockHeader,
|
||||||
domain: Hash256,
|
domain: Hash256,
|
||||||
|
) -> Result<Safe, NotSafe> {
|
||||||
|
self.check_and_insert_block_signing_root(
|
||||||
|
validator_pubkey,
|
||||||
|
block_header.slot,
|
||||||
|
block_header.signing_root(domain),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As for `check_and_insert_block_proposal` but without requiring the whole `BeaconBlockHeader`.
|
||||||
|
pub fn check_and_insert_block_signing_root(
|
||||||
|
&self,
|
||||||
|
validator_pubkey: &PublicKey,
|
||||||
|
slot: Slot,
|
||||||
|
signing_root: Hash256,
|
||||||
) -> Result<Safe, NotSafe> {
|
) -> Result<Safe, NotSafe> {
|
||||||
let mut conn = self.conn_pool.get()?;
|
let mut conn = self.conn_pool.get()?;
|
||||||
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
||||||
|
let safe = self.check_and_insert_block_signing_root_txn(
|
||||||
|
validator_pubkey,
|
||||||
|
slot,
|
||||||
|
signing_root,
|
||||||
|
&txn,
|
||||||
|
)?;
|
||||||
|
txn.commit()?;
|
||||||
|
Ok(safe)
|
||||||
|
}
|
||||||
|
|
||||||
let safe = self.check_block_proposal(&txn, validator_pubkey, block_header, domain)?;
|
/// Transactional variant of `check_and_insert_block_signing_root`.
|
||||||
|
pub fn check_and_insert_block_signing_root_txn(
|
||||||
|
&self,
|
||||||
|
validator_pubkey: &PublicKey,
|
||||||
|
slot: Slot,
|
||||||
|
signing_root: Hash256,
|
||||||
|
txn: &Transaction,
|
||||||
|
) -> Result<Safe, NotSafe> {
|
||||||
|
let safe = self.check_block_proposal(&txn, validator_pubkey, slot, signing_root)?;
|
||||||
|
|
||||||
if safe != Safe::SameData {
|
if safe != Safe::SameData {
|
||||||
self.insert_block_proposal(&txn, validator_pubkey, block_header, domain)?;
|
self.insert_block_proposal(&txn, validator_pubkey, slot, signing_root)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
txn.commit()?;
|
|
||||||
Ok(safe)
|
Ok(safe)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,19 +496,238 @@ impl SlashingDatabase {
|
|||||||
validator_pubkey: &PublicKey,
|
validator_pubkey: &PublicKey,
|
||||||
attestation: &AttestationData,
|
attestation: &AttestationData,
|
||||||
domain: Hash256,
|
domain: Hash256,
|
||||||
|
) -> Result<Safe, NotSafe> {
|
||||||
|
let attestation_signing_root = attestation.signing_root(domain);
|
||||||
|
self.check_and_insert_attestation_signing_root(
|
||||||
|
validator_pubkey,
|
||||||
|
attestation.source.epoch,
|
||||||
|
attestation.target.epoch,
|
||||||
|
attestation_signing_root,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As for `check_and_insert_attestation` but without requiring the whole `AttestationData`.
|
||||||
|
pub fn check_and_insert_attestation_signing_root(
|
||||||
|
&self,
|
||||||
|
validator_pubkey: &PublicKey,
|
||||||
|
att_source_epoch: Epoch,
|
||||||
|
att_target_epoch: Epoch,
|
||||||
|
att_signing_root: Hash256,
|
||||||
) -> Result<Safe, NotSafe> {
|
) -> Result<Safe, NotSafe> {
|
||||||
let mut conn = self.conn_pool.get()?;
|
let mut conn = self.conn_pool.get()?;
|
||||||
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
let txn = conn.transaction_with_behavior(TransactionBehavior::Exclusive)?;
|
||||||
|
let safe = self.check_and_insert_attestation_signing_root_txn(
|
||||||
let safe = self.check_attestation(&txn, validator_pubkey, attestation, domain)?;
|
validator_pubkey,
|
||||||
|
att_source_epoch,
|
||||||
if safe != Safe::SameData {
|
att_target_epoch,
|
||||||
self.insert_attestation(&txn, validator_pubkey, attestation, domain)?;
|
att_signing_root,
|
||||||
}
|
&txn,
|
||||||
|
)?;
|
||||||
txn.commit()?;
|
txn.commit()?;
|
||||||
Ok(safe)
|
Ok(safe)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Transactional variant of `check_and_insert_attestation_signing_root`.
|
||||||
|
fn check_and_insert_attestation_signing_root_txn(
|
||||||
|
&self,
|
||||||
|
validator_pubkey: &PublicKey,
|
||||||
|
att_source_epoch: Epoch,
|
||||||
|
att_target_epoch: Epoch,
|
||||||
|
att_signing_root: Hash256,
|
||||||
|
txn: &Transaction,
|
||||||
|
) -> Result<Safe, NotSafe> {
|
||||||
|
let safe = self.check_attestation(
|
||||||
|
&txn,
|
||||||
|
validator_pubkey,
|
||||||
|
att_source_epoch,
|
||||||
|
att_target_epoch,
|
||||||
|
att_signing_root,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if safe != Safe::SameData {
|
||||||
|
self.insert_attestation(
|
||||||
|
&txn,
|
||||||
|
validator_pubkey,
|
||||||
|
att_source_epoch,
|
||||||
|
att_target_epoch,
|
||||||
|
att_signing_root,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(safe)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import slashing protection from another client in the interchange format.
|
||||||
|
pub fn import_interchange_info(
|
||||||
|
&self,
|
||||||
|
interchange: &Interchange,
|
||||||
|
genesis_validators_root: Hash256,
|
||||||
|
) -> Result<(), InterchangeError> {
|
||||||
|
let version = interchange.metadata.interchange_format_version;
|
||||||
|
if version != SUPPORTED_INTERCHANGE_FORMAT_VERSION {
|
||||||
|
return Err(InterchangeError::UnsupportedVersion(version));
|
||||||
|
}
|
||||||
|
|
||||||
|
if genesis_validators_root != interchange.metadata.genesis_validators_root {
|
||||||
|
return Err(InterchangeError::GenesisValidatorsMismatch {
|
||||||
|
client: genesis_validators_root,
|
||||||
|
interchange_file: interchange.metadata.genesis_validators_root,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import atomically, to prevent registering validators with partial information.
|
||||||
|
let mut conn = self.conn_pool.get()?;
|
||||||
|
let txn = conn.transaction()?;
|
||||||
|
|
||||||
|
for record in &interchange.data {
|
||||||
|
self.register_validators_in_txn(std::iter::once(&record.pubkey), &txn)?;
|
||||||
|
|
||||||
|
// Insert all signed blocks.
|
||||||
|
for block in &record.signed_blocks {
|
||||||
|
self.check_and_insert_block_signing_root_txn(
|
||||||
|
&record.pubkey,
|
||||||
|
block.slot,
|
||||||
|
block.signing_root.unwrap_or_else(Hash256::zero),
|
||||||
|
&txn,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert all signed attestations.
|
||||||
|
for attestation in &record.signed_attestations {
|
||||||
|
self.check_and_insert_attestation_signing_root_txn(
|
||||||
|
&record.pubkey,
|
||||||
|
attestation.source_epoch,
|
||||||
|
attestation.target_epoch,
|
||||||
|
attestation.signing_root.unwrap_or_else(Hash256::zero),
|
||||||
|
&txn,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
txn.commit()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_interchange_info(
|
||||||
|
&self,
|
||||||
|
genesis_validators_root: Hash256,
|
||||||
|
) -> Result<Interchange, InterchangeError> {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
let mut conn = self.conn_pool.get()?;
|
||||||
|
let txn = conn.transaction()?;
|
||||||
|
|
||||||
|
// Map from internal validator pubkey to blocks and attestation for that pubkey.
|
||||||
|
let mut data: BTreeMap<String, (Vec<InterchangeBlock>, Vec<InterchangeAttestation>)> =
|
||||||
|
BTreeMap::new();
|
||||||
|
|
||||||
|
txn.prepare(
|
||||||
|
"SELECT public_key, slot, signing_root
|
||||||
|
FROM signed_blocks, validators
|
||||||
|
WHERE signed_blocks.validator_id = validators.id",
|
||||||
|
)?
|
||||||
|
.query_and_then(params![], |row| {
|
||||||
|
let validator_pubkey: String = row.get(0)?;
|
||||||
|
let slot = row.get(1)?;
|
||||||
|
let signing_root = Some(hash256_from_row(2, &row)?);
|
||||||
|
let signed_block = InterchangeBlock { slot, signing_root };
|
||||||
|
data.entry(validator_pubkey)
|
||||||
|
.or_insert_with(|| (vec![], vec![]))
|
||||||
|
.0
|
||||||
|
.push(signed_block);
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.collect::<Result<_, InterchangeError>>()?;
|
||||||
|
|
||||||
|
txn.prepare(
|
||||||
|
"SELECT public_key, source_epoch, target_epoch, signing_root
|
||||||
|
FROM signed_attestations, validators
|
||||||
|
WHERE signed_attestations.validator_id = validators.id",
|
||||||
|
)?
|
||||||
|
.query_and_then(params![], |row| {
|
||||||
|
let validator_pubkey: String = row.get(0)?;
|
||||||
|
let source_epoch = row.get(1)?;
|
||||||
|
let target_epoch = row.get(2)?;
|
||||||
|
let signing_root = Some(hash256_from_row(3, &row)?);
|
||||||
|
let signed_attestation = InterchangeAttestation {
|
||||||
|
source_epoch,
|
||||||
|
target_epoch,
|
||||||
|
signing_root,
|
||||||
|
};
|
||||||
|
data.entry(validator_pubkey)
|
||||||
|
.or_insert_with(|| (vec![], vec![]))
|
||||||
|
.1
|
||||||
|
.push(signed_attestation);
|
||||||
|
Ok(())
|
||||||
|
})?
|
||||||
|
.collect::<Result<_, InterchangeError>>()?;
|
||||||
|
|
||||||
|
let metadata = InterchangeMetadata {
|
||||||
|
interchange_format: InterchangeFormat::Complete,
|
||||||
|
interchange_format_version: SUPPORTED_INTERCHANGE_FORMAT_VERSION,
|
||||||
|
genesis_validators_root,
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = data
|
||||||
|
.into_iter()
|
||||||
|
.map(|(pubkey, (signed_blocks, signed_attestations))| {
|
||||||
|
Ok(CompleteInterchangeData {
|
||||||
|
pubkey: pubkey.parse().map_err(InterchangeError::InvalidPubkey)?,
|
||||||
|
signed_blocks,
|
||||||
|
signed_attestations,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<_, InterchangeError>>()?;
|
||||||
|
|
||||||
|
Ok(Interchange { metadata, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn num_validator_rows(&self) -> Result<u32, NotSafe> {
|
||||||
|
let mut conn = self.conn_pool.get()?;
|
||||||
|
let txn = conn.transaction()?;
|
||||||
|
let count = txn
|
||||||
|
.prepare("SELECT COALESCE(COUNT(*), 0) FROM validators")?
|
||||||
|
.query_row(params![], |row| row.get(0))?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum InterchangeError {
|
||||||
|
UnsupportedVersion(u64),
|
||||||
|
GenesisValidatorsMismatch {
|
||||||
|
interchange_file: Hash256,
|
||||||
|
client: Hash256,
|
||||||
|
},
|
||||||
|
MinimalAttestationSourceAndTargetInconsistent,
|
||||||
|
SQLError(String),
|
||||||
|
SQLPoolError(r2d2::Error),
|
||||||
|
SerdeJsonError(serde_json::Error),
|
||||||
|
InvalidPubkey(String),
|
||||||
|
NotSafe(NotSafe),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NotSafe> for InterchangeError {
|
||||||
|
fn from(error: NotSafe) -> Self {
|
||||||
|
InterchangeError::NotSafe(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rusqlite::Error> for InterchangeError {
|
||||||
|
fn from(error: rusqlite::Error) -> Self {
|
||||||
|
Self::SQLError(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<r2d2::Error> for InterchangeError {
|
||||||
|
fn from(error: r2d2::Error) -> Self {
|
||||||
|
InterchangeError::SQLPoolError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for InterchangeError {
|
||||||
|
fn from(error: serde_json::Error) -> Self {
|
||||||
|
InterchangeError::SerdeJsonError(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
#![cfg(test)]
|
|
||||||
|
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use tempfile::tempdir;
|
use tempfile::{tempdir, TempDir};
|
||||||
use types::{
|
use types::{
|
||||||
test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader, Hash256,
|
test_utils::generate_deterministic_keypair, AttestationData, BeaconBlockHeader, Hash256,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const DEFAULT_VALIDATOR_INDEX: usize = 0;
|
pub const DEFAULT_VALIDATOR_INDEX: usize = 0;
|
||||||
pub const DEFAULT_DOMAIN: Hash256 = Hash256::zero();
|
pub const DEFAULT_DOMAIN: Hash256 = Hash256::zero();
|
||||||
|
pub const DEFAULT_GENESIS_VALIDATORS_ROOT: Hash256 = Hash256::zero();
|
||||||
|
|
||||||
pub fn pubkey(index: usize) -> PublicKey {
|
pub fn pubkey(index: usize) -> PublicKey {
|
||||||
generate_deterministic_keypair(index).pk
|
generate_deterministic_keypair(index).pk
|
||||||
@ -73,6 +72,16 @@ impl<T> Default for StreamTest<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> StreamTest<T> {
|
||||||
|
/// The number of test cases that are expected to pass processing successfully.
|
||||||
|
fn num_expected_successes(&self) -> usize {
|
||||||
|
self.cases
|
||||||
|
.iter()
|
||||||
|
.filter(|case| case.expected.is_ok())
|
||||||
|
.count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StreamTest<AttestationData> {
|
impl StreamTest<AttestationData> {
|
||||||
pub fn run(&self) {
|
pub fn run(&self) {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
@ -91,6 +100,8 @@ impl StreamTest<AttestationData> {
|
|||||||
i
|
i
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roundtrip_database(&dir, &slashing_db, self.num_expected_successes() == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,5 +123,24 @@ impl StreamTest<BeaconBlockHeader> {
|
|||||||
i
|
i
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
roundtrip_database(&dir, &slashing_db, self.num_expected_successes() == 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn roundtrip_database(dir: &TempDir, db: &SlashingDatabase, is_empty: bool) {
|
||||||
|
let exported = db
|
||||||
|
.export_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT)
|
||||||
|
.unwrap();
|
||||||
|
let new_db =
|
||||||
|
SlashingDatabase::create(&dir.path().join("roundtrip_slashing_protection.sqlite")).unwrap();
|
||||||
|
new_db
|
||||||
|
.import_interchange_info(&exported, DEFAULT_GENESIS_VALIDATORS_ROOT)
|
||||||
|
.unwrap();
|
||||||
|
let reexported = new_db
|
||||||
|
.export_interchange_info(DEFAULT_GENESIS_VALIDATORS_ROOT)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(exported, reexported);
|
||||||
|
assert_eq!(is_empty, exported.is_empty());
|
||||||
|
}
|
||||||
|
23
validator_client/slashing_protection/tests/interop.rs
Normal file
23
validator_client/slashing_protection/tests/interop.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use slashing_protection::interchange_test::TestCase;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn test_root_dir() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("interchange-tests")
|
||||||
|
.join("tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generated() {
|
||||||
|
for entry in test_root_dir()
|
||||||
|
.join("generated")
|
||||||
|
.read_dir()
|
||||||
|
.unwrap()
|
||||||
|
.map(Result::unwrap)
|
||||||
|
{
|
||||||
|
let file = File::open(entry.path()).unwrap();
|
||||||
|
let test_case: TestCase = serde_json::from_reader(&file).unwrap();
|
||||||
|
test_case.run();
|
||||||
|
}
|
||||||
|
}
|
@ -10,8 +10,6 @@ use std::path::PathBuf;
|
|||||||
use types::GRAFFITI_BYTES_LEN;
|
use types::GRAFFITI_BYTES_LEN;
|
||||||
|
|
||||||
pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/";
|
pub const DEFAULT_HTTP_SERVER: &str = "http://localhost:5052/";
|
||||||
/// Path to the slashing protection database within the datadir.
|
|
||||||
pub const SLASHING_PROTECTION_FILENAME: &str = "slashing_protection.sqlite";
|
|
||||||
|
|
||||||
/// Stores the core configuration for this validator instance.
|
/// Stores the core configuration for this validator instance.
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
config::{Config, SLASHING_PROTECTION_FILENAME},
|
config::Config, fork_service::ForkService, initialized_validators::InitializedValidators,
|
||||||
fork_service::ForkService,
|
|
||||||
initialized_validators::InitializedValidators,
|
|
||||||
};
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use slashing_protection::{NotSafe, Safe, SlashingDatabase};
|
use slashing_protection::{NotSafe, Safe, SlashingDatabase, SLASHING_PROTECTION_FILENAME};
|
||||||
use slog::{crit, error, warn, Logger};
|
use slog::{crit, error, warn, Logger};
|
||||||
use slot_clock::SlotClock;
|
use slot_clock::SlotClock;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
Loading…
Reference in New Issue
Block a user