3db9072fee
## Issue Addressed Closes #1889 ## Proposed Changes - Error when passwords which use invalid UTF-8 characters during encryption. - Add some tests ## Additional Info I've decided to error when bad characters are used to create/encrypt a keystore but think we should allow them during decryption since either the keystore was created - with invalid UTF-8 characters (possibly by another client or someone whose password is random bytes) in which case we'd want them to be able to decrypt their keystore using the right key. - without invalid characters then the password checksum would almost certainly fail. Happy to add them to decryption if we want to make the decryption more trigger happy 😋 , it would only be a one line change and would tell the user which character index is causing the issue. See https://eips.ethereum.org/EIPS/eip-2335#password-requirements
378 lines
11 KiB
Rust
378 lines
11 KiB
Rust
#![cfg(not(debug_assertions))]
|
|
|
|
use eth2_keystore::{Keystore, KeystoreBuilder, PlainText};
|
|
use std::fs::{self, File};
|
|
use std::path::Path;
|
|
use tempfile::{tempdir, TempDir};
|
|
use types::{test_utils::generate_deterministic_keypair, EthSpec, Keypair, MainnetEthSpec};
|
|
use validator_dir::{
|
|
Builder, BuilderError, ValidatorDir, ETH1_DEPOSIT_DATA_FILE, ETH1_DEPOSIT_TX_HASH_FILE,
|
|
VOTING_KEYSTORE_FILE, WITHDRAWAL_KEYSTORE_FILE,
|
|
};
|
|
|
|
/// A very weak password with which to encrypt the keystores.
|
|
pub const INSECURE_PASSWORD: &[u8] = &[50; 51];
|
|
|
|
/// Helper struct for configuring tests.
|
|
struct BuildConfig {
|
|
random_voting_keystore: bool,
|
|
random_withdrawal_keystore: bool,
|
|
deposit_amount: Option<u64>,
|
|
store_withdrawal_keystore: bool,
|
|
}
|
|
|
|
impl Default for BuildConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
random_voting_keystore: true,
|
|
random_withdrawal_keystore: true,
|
|
deposit_amount: None,
|
|
store_withdrawal_keystore: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check that a keystore exists and can be decrypted with a password in password_dir
|
|
fn check_keystore<P: AsRef<Path>>(path: P, password_dir: P) -> Keypair {
|
|
let mut file = File::open(path).unwrap();
|
|
let keystore = Keystore::from_json_reader(&mut file).unwrap();
|
|
let pubkey = keystore.pubkey();
|
|
let password_path = password_dir.as_ref().join(format!("0x{}", pubkey));
|
|
let password = fs::read(password_path).unwrap();
|
|
keystore.decrypt_keypair(&password).unwrap()
|
|
}
|
|
|
|
/// Creates a keystore using `generate_deterministic_keypair`.
|
|
pub fn generate_deterministic_keystore(i: usize) -> Result<(Keystore, PlainText), String> {
|
|
let keypair = generate_deterministic_keypair(i);
|
|
|
|
let keystore = KeystoreBuilder::new(&keypair, INSECURE_PASSWORD, "".into())
|
|
.map_err(|e| format!("Unable to create keystore builder: {:?}", e))?
|
|
.build()
|
|
.map_err(|e| format!("Unable to build keystore: {:?}", e))?;
|
|
|
|
Ok((keystore, INSECURE_PASSWORD.to_vec().into()))
|
|
}
|
|
|
|
/// A testing harness for generating validator directories.
|
|
struct Harness {
|
|
validators_dir: TempDir,
|
|
password_dir: TempDir,
|
|
}
|
|
|
|
impl Harness {
|
|
/// Create a new harness using temporary directories.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
validators_dir: tempdir().unwrap(),
|
|
password_dir: tempdir().unwrap(),
|
|
}
|
|
}
|
|
|
|
/// Create a `ValidatorDir` from the `config`, then assert that the `ValidatorDir` was generated
|
|
/// correctly with respect to the `config`.
|
|
pub fn create_and_test(&self, config: &BuildConfig) -> ValidatorDir {
|
|
let spec = MainnetEthSpec::default_spec();
|
|
|
|
/*
|
|
* Build the `ValidatorDir`.
|
|
*/
|
|
|
|
let builder = Builder::new(self.validators_dir.path().into())
|
|
.password_dir(self.password_dir.path())
|
|
// Note: setting the withdrawal keystore here ensure that it can get replaced by
|
|
// further calls to `random_withdrawal_keystore`.
|
|
.store_withdrawal_keystore(config.store_withdrawal_keystore);
|
|
|
|
let builder = if config.random_voting_keystore {
|
|
builder.random_voting_keystore().unwrap()
|
|
} else {
|
|
let (keystore, password) = generate_deterministic_keystore(0).unwrap();
|
|
builder.voting_keystore(keystore, password.as_bytes())
|
|
};
|
|
|
|
let builder = if config.random_withdrawal_keystore {
|
|
builder.random_withdrawal_keystore().unwrap()
|
|
} else {
|
|
let (keystore, password) = generate_deterministic_keystore(1).unwrap();
|
|
builder.withdrawal_keystore(keystore, password.as_bytes())
|
|
};
|
|
|
|
let builder = if let Some(amount) = config.deposit_amount {
|
|
builder.create_eth1_tx_data(amount, &spec)
|
|
} else {
|
|
builder
|
|
};
|
|
|
|
let mut validator = builder.build().unwrap();
|
|
|
|
/*
|
|
* Assert that the dir is consistent with the config.
|
|
*/
|
|
|
|
let withdrawal_keystore_path = validator.dir().join(WITHDRAWAL_KEYSTORE_FILE);
|
|
let password_dir = self.password_dir.path().into();
|
|
|
|
// Ensure the voting keypair exists and can be decrypted.
|
|
let voting_keypair =
|
|
check_keystore(&validator.dir().join(VOTING_KEYSTORE_FILE), &password_dir);
|
|
|
|
if !config.random_voting_keystore {
|
|
assert_eq!(voting_keypair.pk, generate_deterministic_keypair(0).pk)
|
|
}
|
|
|
|
// Use OR here instead of AND so we *always* check for the withdrawal keystores if random
|
|
// keystores were generated.
|
|
if config.random_withdrawal_keystore || config.store_withdrawal_keystore {
|
|
// Ensure the withdrawal keypair exists and can be decrypted.
|
|
let withdrawal_keypair = check_keystore(&withdrawal_keystore_path, &password_dir);
|
|
|
|
if !config.random_withdrawal_keystore {
|
|
assert_eq!(withdrawal_keypair.pk, generate_deterministic_keypair(1).pk)
|
|
}
|
|
|
|
// The withdrawal keys should be distinct from the voting keypairs.
|
|
assert_ne!(withdrawal_keypair.pk, voting_keypair.pk);
|
|
}
|
|
|
|
if !config.store_withdrawal_keystore && !config.random_withdrawal_keystore {
|
|
assert!(!withdrawal_keystore_path.exists())
|
|
}
|
|
|
|
if let Some(amount) = config.deposit_amount {
|
|
// Check that the deposit data can be decoded.
|
|
let data = validator.eth1_deposit_data().unwrap().unwrap();
|
|
|
|
// Ensure the amount is consistent.
|
|
assert_eq!(data.deposit_data.amount, amount);
|
|
|
|
// Check that the eth1 deposit data file is a valid format.
|
|
let hex =
|
|
String::from_utf8(fs::read(validator.dir().join(ETH1_DEPOSIT_DATA_FILE)).unwrap())
|
|
.unwrap();
|
|
assert!(hex.starts_with("0x"), "deposit data should have 0x prefix");
|
|
hex::decode(&hex[2..]).unwrap();
|
|
} else {
|
|
// If there was no deposit then we should return `Ok(None)`.
|
|
assert!(validator.eth1_deposit_data().unwrap().is_none());
|
|
}
|
|
|
|
let tx_hash_path = validator.dir().join(ETH1_DEPOSIT_TX_HASH_FILE);
|
|
|
|
// The eth1 deposit file should not exist, yet.
|
|
assert!(!tx_hash_path.exists());
|
|
|
|
let tx = "junk data";
|
|
|
|
// Save a tx hash.
|
|
validator.save_eth1_deposit_tx_hash(tx).unwrap();
|
|
|
|
// Ensure the saved tx hash is correct.
|
|
assert_eq!(fs::read(tx_hash_path).unwrap(), tx.as_bytes().to_vec());
|
|
|
|
// Saving a second tx hash should fail.
|
|
validator.save_eth1_deposit_tx_hash(tx).unwrap_err();
|
|
|
|
validator
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn concurrency() {
|
|
let harness = Harness::new();
|
|
|
|
let val_dir = harness.create_and_test(&BuildConfig::default());
|
|
let path = val_dir.dir().clone();
|
|
|
|
// Should not re-open whilst opened after build.
|
|
ValidatorDir::open(&path).unwrap_err();
|
|
|
|
drop(val_dir);
|
|
|
|
// Should re-open after drop.
|
|
let val_dir = ValidatorDir::open(&path).unwrap();
|
|
|
|
// Should not re-open when opened via ValidatorDir.
|
|
ValidatorDir::open(&path).unwrap_err();
|
|
|
|
drop(val_dir);
|
|
|
|
// Should re-open again.
|
|
ValidatorDir::open(&path).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn without_voting_keystore() {
|
|
let harness = Harness::new();
|
|
|
|
assert!(matches!(
|
|
Builder::new(harness.validators_dir.path().into(),)
|
|
.password_dir(harness.password_dir.path())
|
|
.random_withdrawal_keystore()
|
|
.unwrap()
|
|
.build(),
|
|
Err(BuilderError::UninitializedVotingKeystore)
|
|
))
|
|
}
|
|
|
|
#[test]
|
|
fn without_withdrawal_keystore() {
|
|
let harness = Harness::new();
|
|
let spec = &MainnetEthSpec::default_spec();
|
|
|
|
// Should build without withdrawal keystore if not storing the it or creating eth1 data.
|
|
Builder::new(harness.validators_dir.path().into())
|
|
.password_dir(harness.password_dir.path())
|
|
.random_voting_keystore()
|
|
.unwrap()
|
|
.store_withdrawal_keystore(false)
|
|
.build()
|
|
.unwrap();
|
|
|
|
assert!(
|
|
matches!(
|
|
Builder::new(harness.validators_dir.path().into(),)
|
|
.password_dir(harness.password_dir.path())
|
|
.random_voting_keystore()
|
|
.unwrap()
|
|
.store_withdrawal_keystore(true)
|
|
.build(),
|
|
Err(BuilderError::UninitializedWithdrawalKeystore)
|
|
),
|
|
"storing the keystore requires keystore"
|
|
);
|
|
|
|
assert!(
|
|
matches!(
|
|
Builder::new(harness.validators_dir.path().into(),)
|
|
.password_dir(harness.password_dir.path())
|
|
.random_voting_keystore()
|
|
.unwrap()
|
|
.create_eth1_tx_data(42, spec)
|
|
.build(),
|
|
Err(BuilderError::UninitializedWithdrawalKeystore)
|
|
),
|
|
"storing the keystore requires keystore"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_voting_keystore() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
random_voting_keystore: false,
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_withdrawal_keystore_without_saving() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
random_withdrawal_keystore: false,
|
|
store_withdrawal_keystore: false,
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn deterministic_withdrawal_keystore_with_saving() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
random_withdrawal_keystore: false,
|
|
store_withdrawal_keystore: true,
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn both_keystores_deterministic_without_saving() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
random_voting_keystore: false,
|
|
random_withdrawal_keystore: false,
|
|
store_withdrawal_keystore: false,
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn both_keystores_random_without_saving() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
random_voting_keystore: true,
|
|
random_withdrawal_keystore: true,
|
|
store_withdrawal_keystore: false,
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn both_keystores_deterministic_with_saving() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
random_voting_keystore: false,
|
|
random_withdrawal_keystore: false,
|
|
store_withdrawal_keystore: true,
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn eth1_data() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
deposit_amount: Some(123456),
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn store_withdrawal_keystore_without_eth1_data() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
store_withdrawal_keystore: false,
|
|
random_withdrawal_keystore: true,
|
|
deposit_amount: None,
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|
|
|
|
#[test]
|
|
fn store_withdrawal_keystore_with_eth1_data() {
|
|
let harness = Harness::new();
|
|
|
|
let config = BuildConfig {
|
|
store_withdrawal_keystore: false,
|
|
random_withdrawal_keystore: true,
|
|
deposit_amount: Some(32000000000),
|
|
..BuildConfig::default()
|
|
};
|
|
|
|
harness.create_and_test(&config);
|
|
}
|