mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-28 02:03:32 +00:00
EIP-2386 (draft): Eth2 wallet (#1117)
* Add test to understand flow of key storage * First commit * Committing to save trait stuff * Working naive design * Add keystore struct * Move keystore files into their own module * Add serde (de)serialize_with magic * Add keystore test * Fix tests * Add comments and minor fixes * Pass optional params to `to_keystore` function * Add `path` field to keystore * Add function to read Keystore from file * Add test vectors and fix Version serialization * Checksum params is empty object * Add public key to Keystore * Add function for saving keystore into file * Deleted account_manager main.rs * Move keystore module to validator_client * Add save_keystore method to validator_directory * Add load_keystore function. Minor refactorings * Fixed dependencies * Address some review comments * Add Password newtype; derive Zeroize * Fix test * Move keystore into own crate * Remove padding * Add error enum, zeroize more things * Fix comment * Add keystore builder * Remove keystore stuff from val client * Add more tests, comments * Add more comments, test vectors * Progress on improving JSON validation * More JSON verification * Start moving JSON into own mod * Remove old code * Add more tests, reader/writers * Tidy * Move keystore into own file * Move more logic into keystore file * Tidy * Tidy * Allow for odd-character hex * Add more json missing field checks * Use scrypt by default * Tidy, address comments * Test path and uuid in vectors * Fix comment * Add checks for kdf params * Enforce empty kdf message * Expose json_keystore mod * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * First commits on path derivation * Progress with implementation * Move key derivation into own crate * Start defining JSON wallet * Add progress * Split out encrypt/decrypt * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * Add progress * Replace some password usage with slice * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * Add progress * Expose PlainText struct * First commits on path derivation * Progress with implementation * More progress * Passing intermediate test vectors * Tidy, add comments * Add DerivedKey structs * Move key derivation into own crate * Add zeroize structs * Return error for empty seed * Add tests * Tidy * Add builder * Expose consts, remove Password * Minor progress * Expose SALT_SIZE * First compiling version * Add test vectors * Move dbg assert statement * Add mnemonic, tidy * Tidy * Add testing * Fix broken test * Address review comments Co-authored-by: pawan <pawandhananjay@gmail.com>
This commit is contained in:
22
eth2/utils/eth2_wallet/Cargo.toml
Normal file
22
eth2/utils/eth2_wallet/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "eth2_wallet"
|
||||
version = "0.1.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.102"
|
||||
serde_json = "1.0.41"
|
||||
serde_repr = "0.1"
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
rand = "0.7.2"
|
||||
eth2_keystore = { path = "../eth2_keystore" }
|
||||
eth2_key_derivation = { path = "../eth2_key_derivation" }
|
||||
tiny-bip39 = "0.7.3"
|
||||
|
||||
[dev-dependencies]
|
||||
hex = "0.3"
|
||||
eth2_ssz = { path = "../ssz" }
|
||||
tempfile = "3.1.0"
|
||||
65
eth2/utils/eth2_wallet/src/json_wallet/mod.rs
Normal file
65
eth2/utils/eth2_wallet/src/json_wallet/mod.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::*;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
pub use eth2_keystore::json_keystore::{
|
||||
Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, Kdf, KdfModule,
|
||||
Scrypt, Sha256Checksum,
|
||||
};
|
||||
pub use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct JsonWallet {
|
||||
pub crypto: Crypto,
|
||||
pub name: String,
|
||||
// TODO: confirm if this field is optional or not.
|
||||
//
|
||||
// Reference:
|
||||
//
|
||||
// https://github.com/sigp/lighthouse/pull/1117#discussion_r422892396
|
||||
pub nextaccount: u32,
|
||||
pub uuid: Uuid,
|
||||
pub version: Version,
|
||||
#[serde(rename = "type")]
|
||||
pub type_field: TypeField,
|
||||
}
|
||||
|
||||
/// Version for `JsonWallet`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum Version {
|
||||
V1 = 1,
|
||||
}
|
||||
|
||||
impl Version {
|
||||
pub fn one() -> Self {
|
||||
Version::V1
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for ensuring that serde only decodes valid checksum functions.
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(try_from = "String", into = "String")]
|
||||
pub enum TypeField {
|
||||
Hd,
|
||||
}
|
||||
|
||||
impl Into<String> for TypeField {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
TypeField::Hd => "hierarchical deterministic".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for TypeField {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
match s.as_ref() {
|
||||
"hierarchical deterministic" => Ok(TypeField::Hd),
|
||||
other => Err(format!("Unsupported type function: {}", other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
11
eth2/utils/eth2_wallet/src/lib.rs
Normal file
11
eth2/utils/eth2_wallet/src/lib.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
mod validator_path;
|
||||
mod wallet;
|
||||
|
||||
pub mod json_wallet;
|
||||
|
||||
pub use bip39;
|
||||
pub use validator_path::{KeyType, ValidatorPath, COIN_TYPE, PURPOSE};
|
||||
pub use wallet::{
|
||||
recover_validator_secret, DerivedKey, Error, KeystoreError, PlainText, ValidatorKeystores,
|
||||
Wallet, WalletBuilder,
|
||||
};
|
||||
41
eth2/utils/eth2_wallet/src/validator_path.rs
Normal file
41
eth2/utils/eth2_wallet/src/validator_path.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::fmt;
|
||||
use std::iter::Iterator;
|
||||
|
||||
pub const PURPOSE: u32 = 12381;
|
||||
pub const COIN_TYPE: u32 = 3600;
|
||||
|
||||
pub enum KeyType {
|
||||
Voting,
|
||||
Withdrawal,
|
||||
}
|
||||
|
||||
pub struct ValidatorPath(Vec<u32>);
|
||||
|
||||
impl ValidatorPath {
|
||||
pub fn new(index: u32, key_type: KeyType) -> Self {
|
||||
let mut vec = vec![PURPOSE, COIN_TYPE, index, 0];
|
||||
|
||||
match key_type {
|
||||
KeyType::Voting => vec.push(0),
|
||||
KeyType::Withdrawal => {}
|
||||
}
|
||||
|
||||
Self(vec)
|
||||
}
|
||||
|
||||
pub fn iter_nodes(&self) -> impl Iterator<Item = &u32> {
|
||||
self.0.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidatorPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "m")?;
|
||||
|
||||
for node in self.iter_nodes() {
|
||||
write!(f, "/{}", node)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
287
eth2/utils/eth2_wallet/src/wallet.rs
Normal file
287
eth2/utils/eth2_wallet/src/wallet.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
use crate::{
|
||||
json_wallet::{
|
||||
Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, JsonWallet,
|
||||
Kdf, KdfModule, Sha256Checksum, TypeField, Version,
|
||||
},
|
||||
KeyType, ValidatorPath,
|
||||
};
|
||||
use eth2_keystore::{
|
||||
decrypt, default_kdf, encrypt, keypair_from_secret, Keystore, KeystoreBuilder, IV_SIZE,
|
||||
SALT_SIZE,
|
||||
};
|
||||
use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub use bip39::{Mnemonic, Seed as Bip39Seed};
|
||||
pub use eth2_key_derivation::DerivedKey;
|
||||
pub use eth2_keystore::{Error as KeystoreError, PlainText};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
KeystoreError(KeystoreError),
|
||||
PathExhausted,
|
||||
EmptyPassword,
|
||||
EmptySeed,
|
||||
}
|
||||
|
||||
impl From<KeystoreError> for Error {
|
||||
fn from(e: KeystoreError) -> Error {
|
||||
Error::KeystoreError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains the two keystores required for an eth2 validator.
|
||||
pub struct ValidatorKeystores {
|
||||
/// Contains the secret key used for signing every-day consensus messages (blocks,
|
||||
/// attestations, etc).
|
||||
pub voting: Keystore,
|
||||
/// Contains the secret key that should eventually be required for withdrawing stacked ETH.
|
||||
pub withdrawal: Keystore,
|
||||
}
|
||||
|
||||
/// Constructs a `Keystore`.
|
||||
///
|
||||
/// Generates the KDF `salt` and AES `IV` using `rand::thread_rng()`.
|
||||
pub struct WalletBuilder<'a> {
|
||||
seed: PlainText,
|
||||
password: &'a [u8],
|
||||
kdf: Kdf,
|
||||
cipher: Cipher,
|
||||
uuid: Uuid,
|
||||
name: String,
|
||||
nextaccount: u32,
|
||||
}
|
||||
|
||||
impl<'a> WalletBuilder<'a> {
|
||||
/// Creates a new builder for a seed specified as a BIP-39 `Mnemonic` (where the nmemonic itself does
|
||||
/// not have a passphrase).
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns `Error::EmptyPassword` if `password == ""`.
|
||||
pub fn from_mnemonic(
|
||||
mnemonic: &Mnemonic,
|
||||
password: &'a [u8],
|
||||
name: String,
|
||||
) -> Result<Self, Error> {
|
||||
// TODO: `bip39` does not use zeroize. Perhaps we should make a PR upstream?
|
||||
let seed = Bip39Seed::new(mnemonic, "");
|
||||
|
||||
Self::from_seed_bytes(seed.as_bytes(), password, name)
|
||||
}
|
||||
|
||||
/// Creates a new builder from a `seed` specified as a byte slice.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns `Error::EmptyPassword` if `password == ""`.
|
||||
pub fn from_seed_bytes(seed: &[u8], password: &'a [u8], name: String) -> Result<Self, Error> {
|
||||
if password.is_empty() {
|
||||
Err(Error::EmptyPassword)
|
||||
} else if seed.is_empty() {
|
||||
Err(Error::EmptySeed)
|
||||
} else {
|
||||
let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>();
|
||||
let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into();
|
||||
|
||||
Ok(Self {
|
||||
seed: seed.to_vec().into(),
|
||||
password,
|
||||
kdf: default_kdf(salt.to_vec()),
|
||||
cipher: Cipher::Aes128Ctr(Aes128Ctr { iv }),
|
||||
uuid: Uuid::new_v4(),
|
||||
nextaccount: 0,
|
||||
name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes `self`, returning an encrypted `Wallet`.
|
||||
pub fn build(self) -> Result<Wallet, Error> {
|
||||
Wallet::encrypt(
|
||||
self.seed.as_bytes(),
|
||||
self.password,
|
||||
self.kdf,
|
||||
self.cipher,
|
||||
self.uuid,
|
||||
self.name,
|
||||
self.nextaccount,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Wallet {
|
||||
json: JsonWallet,
|
||||
}
|
||||
|
||||
impl Wallet {
|
||||
/// Instantiates `Self`, encrypting the `seed` using `password` (via `kdf` and `cipher`).
|
||||
///
|
||||
/// The `uuid`, `name` and `nextaccount` are carried through into the created wallet.
|
||||
fn encrypt(
|
||||
seed: &[u8],
|
||||
password: &[u8],
|
||||
kdf: Kdf,
|
||||
cipher: Cipher,
|
||||
uuid: Uuid,
|
||||
name: String,
|
||||
nextaccount: u32,
|
||||
) -> Result<Self, Error> {
|
||||
let (cipher_text, checksum) = encrypt(&seed, &password, &kdf, &cipher)?;
|
||||
|
||||
Ok(Self {
|
||||
json: JsonWallet {
|
||||
crypto: Crypto {
|
||||
kdf: KdfModule {
|
||||
function: kdf.function(),
|
||||
params: kdf,
|
||||
message: EmptyString,
|
||||
},
|
||||
checksum: ChecksumModule {
|
||||
function: Sha256Checksum::function(),
|
||||
params: EmptyMap,
|
||||
message: checksum.to_vec().into(),
|
||||
},
|
||||
cipher: CipherModule {
|
||||
function: cipher.function(),
|
||||
params: cipher,
|
||||
message: cipher_text.into(),
|
||||
},
|
||||
},
|
||||
uuid,
|
||||
nextaccount,
|
||||
version: Version::one(),
|
||||
type_field: TypeField::Hd,
|
||||
name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Produces a `Keystore` (encrypted with `keystore_password`) for the validator at
|
||||
/// `self.nextaccount`, incrementing `self.nextaccount` if the keystore was successfully
|
||||
/// generated.
|
||||
///
|
||||
/// Uses the default encryption settings of `KeystoreBuilder`, not necessarily those that were
|
||||
/// used to encrypt `self`.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - If `wallet_password` is unable to decrypt `self`.
|
||||
/// - If `keystore_password.is_empty()`.
|
||||
/// - If `self.nextaccount == u32::max_value()`.
|
||||
pub fn next_validator(
|
||||
&mut self,
|
||||
wallet_password: &[u8],
|
||||
voting_keystore_password: &[u8],
|
||||
withdrawal_keystore_password: &[u8],
|
||||
) -> Result<ValidatorKeystores, Error> {
|
||||
// Helper closure to reduce code duplication when generating keys.
|
||||
//
|
||||
// It is not a function on `self` to help protect against generating keys without
|
||||
// incrementing `nextaccount`.
|
||||
let derive = |key_type: KeyType, password: &[u8]| -> Result<Keystore, Error> {
|
||||
let (secret, path) =
|
||||
recover_validator_secret(&self, wallet_password, self.json.nextaccount, key_type)?;
|
||||
|
||||
let keypair = keypair_from_secret(secret.as_bytes())?;
|
||||
|
||||
KeystoreBuilder::new(&keypair, password, format!("{}", path))?
|
||||
.build()
|
||||
.map_err(Into::into)
|
||||
};
|
||||
|
||||
let keystores = ValidatorKeystores {
|
||||
voting: derive(KeyType::Voting, voting_keystore_password)?,
|
||||
withdrawal: derive(KeyType::Withdrawal, withdrawal_keystore_password)?,
|
||||
};
|
||||
|
||||
self.json.nextaccount = self
|
||||
.json
|
||||
.nextaccount
|
||||
.checked_add(1)
|
||||
.ok_or_else(|| Error::PathExhausted)?;
|
||||
|
||||
Ok(keystores)
|
||||
}
|
||||
|
||||
/// Returns the value of the JSON wallet `nextaccount` field.
|
||||
///
|
||||
/// This will be the index of the next wallet generated with `Self::next_validator`.
|
||||
pub fn nextaccount(&self) -> u32 {
|
||||
self.json.nextaccount
|
||||
}
|
||||
|
||||
/// Returns the value of the JSON wallet `name` field.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.json.name
|
||||
}
|
||||
|
||||
/// Returns the value of the JSON wallet `uuid` field.
|
||||
pub fn uuid(&self) -> &Uuid {
|
||||
&self.json.uuid
|
||||
}
|
||||
|
||||
/// Returns the value of the JSON wallet `type` field.
|
||||
pub fn type_field(&self) -> String {
|
||||
self.json.type_field.clone().into()
|
||||
}
|
||||
|
||||
/// Returns the master seed of this wallet. Care should be taken not to leak this seed.
|
||||
pub fn decrypt_seed(&self, password: &[u8]) -> Result<PlainText, Error> {
|
||||
decrypt(password, &self.json.crypto).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Encodes `self` as a JSON object.
|
||||
pub fn to_json_string(&self) -> Result<String, Error> {
|
||||
serde_json::to_string(self)
|
||||
.map_err(|e| KeystoreError::UnableToSerialize(format!("{}", e)))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns `self` from an encoded JSON object.
|
||||
pub fn from_json_str(json_string: &str) -> Result<Self, Error> {
|
||||
serde_json::from_str(json_string)
|
||||
.map_err(|e| KeystoreError::InvalidJson(format!("{}", e)))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Encodes self as a JSON object to the given `writer`.
|
||||
pub fn to_json_writer<W: Write>(&self, writer: W) -> Result<(), Error> {
|
||||
serde_json::to_writer(writer, self)
|
||||
.map_err(|e| KeystoreError::WriteError(format!("{}", e)))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Instantiates `self` from a JSON `reader`.
|
||||
pub fn from_json_reader<R: Read>(reader: R) -> Result<Self, Error> {
|
||||
serde_json::from_reader(reader)
|
||||
.map_err(|e| KeystoreError::ReadError(format!("{}", e)))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `(secret, path)` for the `key_type` for the validator at `index`.
|
||||
///
|
||||
/// This function should only be used for recovering lost keys, not creating new ones because it
|
||||
/// does not update `wallet.nextaccount`. Using this function to generate new keys can easily
|
||||
/// result in the same key being unknowingly generated twice.
|
||||
///
|
||||
/// To generate consecutive keys safely, use `Wallet::next_voting_keystore`.
|
||||
pub fn recover_validator_secret(
|
||||
wallet: &Wallet,
|
||||
wallet_password: &[u8],
|
||||
index: u32,
|
||||
key_type: KeyType,
|
||||
) -> Result<(PlainText, ValidatorPath), Error> {
|
||||
let path = ValidatorPath::new(index, key_type);
|
||||
let secret = wallet.decrypt_seed(wallet_password)?;
|
||||
let master = DerivedKey::from_seed(secret.as_bytes()).map_err(|()| Error::EmptyPassword)?;
|
||||
|
||||
let destination = path.iter_nodes().fold(master, |dk, i| dk.child(*i));
|
||||
|
||||
Ok((destination.secret().to_vec().into(), path))
|
||||
}
|
||||
60
eth2/utils/eth2_wallet/tests/eip2386_vectors.rs
Normal file
60
eth2/utils/eth2_wallet/tests/eip2386_vectors.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use eth2_keystore::Uuid;
|
||||
use eth2_wallet::Wallet;
|
||||
|
||||
const EXPECTED_SECRET: &str = "147addc7ec981eb2715a22603813271cce540e0b7f577126011eb06249d9227c";
|
||||
const PASSWORD: &str = "testpassword";
|
||||
|
||||
pub fn decode_and_check_seed(json: &str) -> Wallet {
|
||||
let wallet = Wallet::from_json_str(json).expect("should decode keystore json");
|
||||
let expected_sk = hex::decode(EXPECTED_SECRET).unwrap();
|
||||
let seed = wallet.decrypt_seed(PASSWORD.as_bytes()).unwrap();
|
||||
assert_eq!(seed.as_bytes(), &expected_sk[..]);
|
||||
wallet
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eip2386_test_vector_scrypt() {
|
||||
let vector = r#"
|
||||
{
|
||||
"crypto": {
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"message": "8bdadea203eeaf8f23c96137af176ded4b098773410634727bd81c4e8f7f1021",
|
||||
"params": {}
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"message": "7f8211b88dfb8694bac7de3fa32f5f84d0a30f15563358133cda3b287e0f3f4a",
|
||||
"params": {
|
||||
"iv": "9476702ab99beff3e8012eff49ffb60d"
|
||||
}
|
||||
},
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"message": "",
|
||||
"params": {
|
||||
"c": 16,
|
||||
"dklen": 32,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "dd35b0c08ebb672fe18832120a55cb8098f428306bf5820f5486b514f61eb712"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Test wallet 2",
|
||||
"nextaccount": 0,
|
||||
"type": "hierarchical deterministic",
|
||||
"uuid": "b74559b8-ed56-4841-b25c-dba1b7c9d9d5",
|
||||
"version": 1
|
||||
}
|
||||
"#;
|
||||
|
||||
let wallet = decode_and_check_seed(&vector);
|
||||
assert_eq!(
|
||||
*wallet.uuid(),
|
||||
Uuid::parse_str("b74559b8-ed56-4841-b25c-dba1b7c9d9d5").unwrap(),
|
||||
"uuid"
|
||||
);
|
||||
assert_eq!(wallet.name(), "Test wallet 2", "name");
|
||||
assert_eq!(wallet.nextaccount(), 0, "nextaccount");
|
||||
assert_eq!(wallet.type_field(), "hierarchical deterministic", "type");
|
||||
}
|
||||
246
eth2/utils/eth2_wallet/tests/json.rs
Normal file
246
eth2/utils/eth2_wallet/tests/json.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use eth2_wallet::{Error, KeystoreError, Wallet};
|
||||
|
||||
fn assert_bad_json(json: &str) {
|
||||
match Wallet::from_json_str(&json) {
|
||||
Err(Error::KeystoreError(KeystoreError::InvalidJson(_))) => {}
|
||||
_ => panic!("expected invalid json error"),
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Note: the `crypto` object is inherited from the `eth2_keystore` crate so we don't test it here.
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn additional_top_level_param() {
|
||||
let vector = r#"
|
||||
{
|
||||
"crypto": {
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"message": "8bdadea203eeaf8f23c96137af176ded4b098773410634727bd81c4e8f7f1021",
|
||||
"params": {}
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"message": "7f8211b88dfb8694bac7de3fa32f5f84d0a30f15563358133cda3b287e0f3f4a",
|
||||
"params": {
|
||||
"iv": "9476702ab99beff3e8012eff49ffb60d"
|
||||
}
|
||||
},
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"message": "",
|
||||
"params": {
|
||||
"c": 16,
|
||||
"dklen": 32,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "dd35b0c08ebb672fe18832120a55cb8098f428306bf5820f5486b514f61eb712"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Test wallet 2",
|
||||
"nextaccount": 0,
|
||||
"type": "hierarchical deterministic",
|
||||
"uuid": "b74559b8-ed56-4841-b25c-dba1b7c9d9d5",
|
||||
"version": 1,
|
||||
"cats": 42
|
||||
}
|
||||
"#;
|
||||
|
||||
assert_bad_json(&vector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_top_level_param() {
|
||||
let vector = r#"
|
||||
{
|
||||
"crypto": {
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"message": "8bdadea203eeaf8f23c96137af176ded4b098773410634727bd81c4e8f7f1021",
|
||||
"params": {}
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"message": "7f8211b88dfb8694bac7de3fa32f5f84d0a30f15563358133cda3b287e0f3f4a",
|
||||
"params": {
|
||||
"iv": "9476702ab99beff3e8012eff49ffb60d"
|
||||
}
|
||||
},
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"message": "",
|
||||
"params": {
|
||||
"c": 16,
|
||||
"dklen": 32,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "dd35b0c08ebb672fe18832120a55cb8098f428306bf5820f5486b514f61eb712"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Test wallet 2",
|
||||
"nextaccount": 0,
|
||||
"type": "hierarchical deterministic",
|
||||
"uuid": "b74559b8-ed56-4841-b25c-dba1b7c9d9d5"
|
||||
}
|
||||
"#;
|
||||
|
||||
assert_bad_json(&vector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_version() {
|
||||
let vector = r#"
|
||||
{
|
||||
"crypto": {
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"message": "8bdadea203eeaf8f23c96137af176ded4b098773410634727bd81c4e8f7f1021",
|
||||
"params": {}
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"message": "7f8211b88dfb8694bac7de3fa32f5f84d0a30f15563358133cda3b287e0f3f4a",
|
||||
"params": {
|
||||
"iv": "9476702ab99beff3e8012eff49ffb60d"
|
||||
}
|
||||
},
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"message": "",
|
||||
"params": {
|
||||
"c": 16,
|
||||
"dklen": 32,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "dd35b0c08ebb672fe18832120a55cb8098f428306bf5820f5486b514f61eb712"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Test wallet 2",
|
||||
"nextaccount": 0,
|
||||
"type": "hierarchical deterministic",
|
||||
"uuid": "b74559b8-ed56-4841-b25c-dba1b7c9d9d5",
|
||||
"version": 2
|
||||
}
|
||||
"#;
|
||||
|
||||
assert_bad_json(&vector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_uuid() {
|
||||
let vector = r#"
|
||||
{
|
||||
"crypto": {
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"message": "8bdadea203eeaf8f23c96137af176ded4b098773410634727bd81c4e8f7f1021",
|
||||
"params": {}
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"message": "7f8211b88dfb8694bac7de3fa32f5f84d0a30f15563358133cda3b287e0f3f4a",
|
||||
"params": {
|
||||
"iv": "9476702ab99beff3e8012eff49ffb60d"
|
||||
}
|
||||
},
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"message": "",
|
||||
"params": {
|
||||
"c": 16,
|
||||
"dklen": 32,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "dd35b0c08ebb672fe18832120a55cb8098f428306bf5820f5486b514f61eb712"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Test wallet 2",
|
||||
"nextaccount": 0,
|
||||
"type": "hierarchical deterministic",
|
||||
"uuid": "!b74559b8-ed56-4841-b25c-dba1b7c9d9d5",
|
||||
"version": 1
|
||||
}
|
||||
"#;
|
||||
|
||||
assert_bad_json(&vector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_type() {
|
||||
let vector = r#"
|
||||
{
|
||||
"crypto": {
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"message": "8bdadea203eeaf8f23c96137af176ded4b098773410634727bd81c4e8f7f1021",
|
||||
"params": {}
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"message": "7f8211b88dfb8694bac7de3fa32f5f84d0a30f15563358133cda3b287e0f3f4a",
|
||||
"params": {
|
||||
"iv": "9476702ab99beff3e8012eff49ffb60d"
|
||||
}
|
||||
},
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"message": "",
|
||||
"params": {
|
||||
"c": 16,
|
||||
"dklen": 32,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "dd35b0c08ebb672fe18832120a55cb8098f428306bf5820f5486b514f61eb712"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Test wallet 2",
|
||||
"nextaccount": 0,
|
||||
"type": "something else",
|
||||
"uuid": "b74559b8-ed56-4841-b25c-dba1b7c9d9d5",
|
||||
"version": 1
|
||||
}
|
||||
"#;
|
||||
|
||||
assert_bad_json(&vector);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_that_u32_nextaccount() {
|
||||
let vector = r#"
|
||||
{
|
||||
"crypto": {
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"message": "8bdadea203eeaf8f23c96137af176ded4b098773410634727bd81c4e8f7f1021",
|
||||
"params": {}
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"message": "7f8211b88dfb8694bac7de3fa32f5f84d0a30f15563358133cda3b287e0f3f4a",
|
||||
"params": {
|
||||
"iv": "9476702ab99beff3e8012eff49ffb60d"
|
||||
}
|
||||
},
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"message": "",
|
||||
"params": {
|
||||
"c": 16,
|
||||
"dklen": 32,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "dd35b0c08ebb672fe18832120a55cb8098f428306bf5820f5486b514f61eb712"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Test wallet 2",
|
||||
"nextaccount": 4294967297,
|
||||
"type": "hierarchical deterministic",
|
||||
"uuid": "b74559b8-ed56-4841-b25c-dba1b7c9d9d5",
|
||||
"version": 1
|
||||
}
|
||||
"#;
|
||||
|
||||
assert_bad_json(&vector);
|
||||
}
|
||||
280
eth2/utils/eth2_wallet/tests/tests.rs
Normal file
280
eth2/utils/eth2_wallet/tests/tests.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use eth2_wallet::{
|
||||
bip39::{Language, Mnemonic, Seed},
|
||||
recover_validator_secret, DerivedKey, Error, KeyType, KeystoreError, Wallet, WalletBuilder,
|
||||
};
|
||||
use ssz::Encode;
|
||||
use std::fs::OpenOptions;
|
||||
use tempfile::tempdir;
|
||||
|
||||
const NAME: &str = "Wallet McWalletface";
|
||||
const SEED: &[u8] = &[42; 42];
|
||||
const WALLET_PASSWORD: &[u8] = &[43; 43];
|
||||
const VOTING_KEYSTORE_PASSWORD: &[u8] = &[44; 44];
|
||||
const WITHDRAWAL_KEYSTORE_PASSWORD: &[u8] = &[45; 45];
|
||||
const MNEMONIC: &str =
|
||||
"enemy fog enlist laundry nurse hungry discover turkey holiday resemble glad discover";
|
||||
|
||||
fn wallet_from_seed() -> Wallet {
|
||||
WalletBuilder::from_seed_bytes(SEED, WALLET_PASSWORD, NAME.into())
|
||||
.expect("should init builder")
|
||||
.build()
|
||||
.expect("should build wallet")
|
||||
}
|
||||
|
||||
fn recovered_voting_key(wallet: &Wallet, index: u32) -> Vec<u8> {
|
||||
let (secret, path) = recover_validator_secret(wallet, WALLET_PASSWORD, index, KeyType::Voting)
|
||||
.expect("should recover voting secret");
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", path),
|
||||
format!("m/12381/3600/{}/0/0", index),
|
||||
"path should be as expected"
|
||||
);
|
||||
|
||||
secret.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn recovered_withdrawal_key(wallet: &Wallet, index: u32) -> Vec<u8> {
|
||||
let (secret, path) =
|
||||
recover_validator_secret(wallet, WALLET_PASSWORD, index, KeyType::Withdrawal)
|
||||
.expect("should recover withdrawal secret");
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", path),
|
||||
format!("m/12381/3600/{}/0", index),
|
||||
"path should be as expected"
|
||||
);
|
||||
|
||||
secret.as_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn manually_derived_voting_key(index: u32) -> Vec<u8> {
|
||||
DerivedKey::from_seed(SEED)
|
||||
.expect("should derive master key")
|
||||
.child(12381)
|
||||
.child(3600)
|
||||
.child(index)
|
||||
.child(0)
|
||||
.child(0)
|
||||
.secret()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
fn manually_derived_withdrawal_key(index: u32) -> Vec<u8> {
|
||||
DerivedKey::from_seed(SEED)
|
||||
.expect("should derive master key")
|
||||
.child(12381)
|
||||
.child(3600)
|
||||
.child(index)
|
||||
.child(0)
|
||||
.secret()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mnemonic_equality() {
|
||||
let m = Mnemonic::from_phrase(MNEMONIC, Language::English).unwrap();
|
||||
|
||||
let from_mnemonic = WalletBuilder::from_mnemonic(&m, WALLET_PASSWORD, NAME.into())
|
||||
.expect("should init builder")
|
||||
.build()
|
||||
.expect("should build wallet");
|
||||
|
||||
let seed = Seed::new(&m, "");
|
||||
|
||||
let from_seed = WalletBuilder::from_seed_bytes(seed.as_bytes(), WALLET_PASSWORD, NAME.into())
|
||||
.expect("should init builder")
|
||||
.build()
|
||||
.expect("should build wallet");
|
||||
|
||||
assert_eq!(
|
||||
from_mnemonic
|
||||
.decrypt_seed(WALLET_PASSWORD)
|
||||
.unwrap()
|
||||
.as_bytes(),
|
||||
from_seed.decrypt_seed(WALLET_PASSWORD).unwrap().as_bytes(),
|
||||
"wallet from mnemonic should match wallet from seed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata() {
|
||||
let wallet = wallet_from_seed();
|
||||
assert_eq!(wallet.name(), NAME, "name");
|
||||
assert_eq!(&wallet.type_field(), "hierarchical deterministic", "name");
|
||||
assert_eq!(wallet.nextaccount(), 0, "name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_round_trip() {
|
||||
let wallet = wallet_from_seed();
|
||||
|
||||
let json = wallet.to_json_string().unwrap();
|
||||
let decoded = Wallet::from_json_str(&json).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decoded.decrypt_seed(&[1, 2, 3]).err().unwrap(),
|
||||
Error::KeystoreError(KeystoreError::InvalidPassword),
|
||||
"should not decrypt with bad password"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
wallet.decrypt_seed(WALLET_PASSWORD).unwrap().as_bytes(),
|
||||
decoded.decrypt_seed(WALLET_PASSWORD).unwrap().as_bytes(),
|
||||
"should decrypt with good password"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_round_trip() {
|
||||
let wallet = wallet_from_seed();
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("keystore.json");
|
||||
|
||||
let get_file = || {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.create(true)
|
||||
.open(path.clone())
|
||||
.expect("should create file")
|
||||
};
|
||||
|
||||
wallet
|
||||
.to_json_writer(&mut get_file())
|
||||
.expect("should write to file");
|
||||
|
||||
let decoded = Wallet::from_json_reader(&mut get_file()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decoded.decrypt_seed(&[1, 2, 3]).err().unwrap(),
|
||||
Error::KeystoreError(KeystoreError::InvalidPassword),
|
||||
"should not decrypt with bad password"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
wallet.decrypt_seed(WALLET_PASSWORD).unwrap().as_bytes(),
|
||||
decoded.decrypt_seed(WALLET_PASSWORD).unwrap().as_bytes(),
|
||||
"should decrypt with good password"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_wallet_password() {
|
||||
assert_eq!(
|
||||
WalletBuilder::from_seed_bytes(SEED, &[], NAME.into())
|
||||
.err()
|
||||
.expect("should error"),
|
||||
Error::EmptyPassword
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_wallet_seed() {
|
||||
assert_eq!(
|
||||
WalletBuilder::from_seed_bytes(&[], WALLET_PASSWORD, NAME.into())
|
||||
.err()
|
||||
.expect("should error"),
|
||||
Error::EmptySeed
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_keystore_password() {
|
||||
let mut wallet = wallet_from_seed();
|
||||
|
||||
assert_eq!(wallet.nextaccount(), 0, "initial nextaccount");
|
||||
|
||||
assert_eq!(
|
||||
wallet
|
||||
.next_validator(WALLET_PASSWORD, &[], WITHDRAWAL_KEYSTORE_PASSWORD,)
|
||||
.err()
|
||||
.expect("should error"),
|
||||
Error::KeystoreError(KeystoreError::EmptyPassword),
|
||||
"should fail with empty voting password"
|
||||
);
|
||||
|
||||
assert_eq!(wallet.nextaccount(), 0, "next account should not update");
|
||||
|
||||
assert_eq!(
|
||||
wallet
|
||||
.next_validator(WALLET_PASSWORD, VOTING_KEYSTORE_PASSWORD, &[],)
|
||||
.err()
|
||||
.expect("should error"),
|
||||
Error::KeystoreError(KeystoreError::EmptyPassword),
|
||||
"should fail with empty withdrawal password"
|
||||
);
|
||||
|
||||
assert_eq!(wallet.nextaccount(), 0, "next account should not update");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_derivation_from_seed() {
|
||||
let mut wallet = wallet_from_seed();
|
||||
|
||||
for i in 0..4 {
|
||||
assert_eq!(wallet.nextaccount(), i, "initial nextaccount");
|
||||
|
||||
let keystores = wallet
|
||||
.next_validator(
|
||||
WALLET_PASSWORD,
|
||||
VOTING_KEYSTORE_PASSWORD,
|
||||
WITHDRAWAL_KEYSTORE_PASSWORD,
|
||||
)
|
||||
.expect("should generate keystores");
|
||||
|
||||
assert_eq!(
|
||||
keystores.voting.path(),
|
||||
format!("m/12381/3600/{}/0/0", i),
|
||||
"voting path should match"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
keystores.withdrawal.path(),
|
||||
format!("m/12381/3600/{}/0", i),
|
||||
"withdrawal path should match"
|
||||
);
|
||||
|
||||
let voting_keypair = keystores
|
||||
.voting
|
||||
.decrypt_keypair(VOTING_KEYSTORE_PASSWORD)
|
||||
.expect("should decrypt voting keypair");
|
||||
|
||||
assert_eq!(
|
||||
voting_keypair.sk.as_ssz_bytes(),
|
||||
manually_derived_voting_key(i),
|
||||
"voting secret should match manually derived"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
voting_keypair.sk.as_ssz_bytes(),
|
||||
recovered_voting_key(&wallet, i),
|
||||
"voting secret should match recovered"
|
||||
);
|
||||
|
||||
let withdrawal_keypair = keystores
|
||||
.withdrawal
|
||||
.decrypt_keypair(WITHDRAWAL_KEYSTORE_PASSWORD)
|
||||
.expect("should decrypt withdrawal keypair");
|
||||
|
||||
assert_eq!(
|
||||
withdrawal_keypair.sk.as_ssz_bytes(),
|
||||
manually_derived_withdrawal_key(i),
|
||||
"withdrawal secret should match manually derived"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
withdrawal_keypair.sk.as_ssz_bytes(),
|
||||
recovered_withdrawal_key(&wallet, i),
|
||||
"withdrawal secret should match recovered"
|
||||
);
|
||||
|
||||
assert_ne!(
|
||||
withdrawal_keypair.sk.as_ssz_bytes(),
|
||||
voting_keypair.sk.as_ssz_bytes(),
|
||||
"voting and withdrawal keypairs should be distinct"
|
||||
);
|
||||
|
||||
assert_eq!(wallet.nextaccount(), i + 1, "updated nextaccount");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user