diff --git a/.gitignore b/.gitignore index cc5c71f01..93e958913 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ testing/ef-tests/mainnet/* # DS_Store: Desktop Services Store .DS_Store + +# keystore +.keystore diff --git a/Cargo.lock b/Cargo.lock index f86e5b7e0..e0d1f5dad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1082,6 +1082,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "bip39" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d193de1f7487df1914d3a568b772458861d33f9c54249612cc2893d6915054" +dependencies = [ + "bitcoin_hashes 0.13.0", + "serde", + "unicode-normalization", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -1097,12 +1108,28 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + [[package]] name = "bitcoin-io" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative 0.1.2", +] + [[package]] name = "bitcoin_hashes" version = "0.14.0" @@ -1110,7 +1137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" dependencies = [ "bitcoin-io", - "hex-conservative", + "hex-conservative 0.2.1", ] [[package]] @@ -1357,8 +1384,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -2688,6 +2717,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hex-conservative" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" + [[package]] name = "hex-conservative" version = "0.2.1" @@ -5136,9 +5171,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -5146,9 +5181,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -5182,6 +5217,7 @@ version = "0.1.0" dependencies = [ "alloy-primitives", "anyhow", + "bip39", "clap", "discv5", "hashbrown 0.15.3", @@ -5211,6 +5247,7 @@ dependencies = [ "ream-sync", "ream-validator-beacon", "ream-validator-lean", + "serde_json", "tokio", "tracing", "tracing-subscriber", @@ -5222,12 +5259,19 @@ dependencies = [ name = "ream-account-manager" version = "0.1.0" dependencies = [ + "anyhow", + "bip39", + "chrono", "hashsig", + "hex", "rand 0.9.2", "rand_chacha 0.9.0", "ream-post-quantum-crypto", + "serde", + "serde_json", "sha2 0.10.8", "tracing", + "uuid", ] [[package]] @@ -6408,7 +6452,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.0", "rand 0.8.5", "secp256k1-sys", "serde", @@ -7478,6 +7522,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ "getrandom 0.3.2", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 51462420f..c89545394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ alloy-rpc-types-beacon = "1.0.8" alloy-rpc-types-eth = "1.0.7" anyhow = "1.0" async-trait = "0.1.86" -bip32 = "0.5.3" +bip39 = "2.2.0" clap = "4" delay_map = "0.4.1" directories = { version = "6.0.0" } @@ -86,6 +86,7 @@ eventsource-client = "0.15.0" futures = "0.3" hashbrown = "0.15.3" hashsig = { git = "https://github.com/b-wagn/hash-sig", rev = "287517a763edba7e518b0c1ee5beb868f26f1f66" } +hex = "0.4" itertools = "0.14" jsonwebtoken = "9.3.1" kzg = { git = "https://github.com/grandinetech/rust-kzg" } diff --git a/bin/ream/Cargo.toml b/bin/ream/Cargo.toml index 237b70259..a9f386a5a 100644 --- a/bin/ream/Cargo.toml +++ b/bin/ream/Cargo.toml @@ -17,6 +17,7 @@ path = "src/main.rs" [dependencies] alloy-primitives.workspace = true anyhow.workspace = true +bip39.workspace = true clap = { workspace = true, features = ["derive", "env"] } discv5.workspace = true hashbrown.workspace = true @@ -24,6 +25,7 @@ libp2p-identity.workspace = true prometheus_exporter.workspace = true rand.workspace = true rand_chacha.workspace = true +serde_json.workspace = true tokio.workspace = true tracing = { workspace = true, features = ["log"] } tracing-subscriber = { workspace = true, features = ["env-filter"] } diff --git a/bin/ream/src/cli/account_manager.rs b/bin/ream/src/cli/account_manager.rs index e6fe2b38c..37c7b7d52 100644 --- a/bin/ream/src/cli/account_manager.rs +++ b/bin/ream/src/cli/account_manager.rs @@ -1,10 +1,12 @@ use anyhow::ensure; +use bip39::Mnemonic; use clap::Parser; +use tracing::info; -const MIN_CHUNK_SIZE: u64 = 4; -const MIN_LIFETIME: u64 = 18; -const DEFAULT_ACTIVATION_EPOCH: usize = 0; -const DEFAULT_NUM_ACTIVE_EPOCHS: usize = 1 << 18; +const MIN_CHUNK_SIZE: u32 = 4; +const MIN_LIFETIME: u32 = 18; +const DEFAULT_ACTIVATION_EPOCH: u32 = 0; +const DEFAULT_NUM_ACTIVE_EPOCHS: u32 = 1 << 18; #[derive(Debug, Parser)] pub struct AccountManagerConfig { @@ -14,23 +16,31 @@ pub struct AccountManagerConfig { /// Account lifetime in 2 ** lifetime slots #[arg(short, long, default_value_t = 18)] - pub lifetime: u64, + pub lifetime: u32, /// Chunk size for messages #[arg(short, long, default_value_t = 5)] - pub chunk_size: u64, + pub chunk_size: u32, /// Seed phrase for key generation #[arg(short, long)] pub seed_phrase: Option, + /// Optional BIP39 passphrase used with the seed phrase + #[arg(long)] + pub passphrase: Option, + /// Activation epoch for the validator #[arg(long, default_value_t = DEFAULT_ACTIVATION_EPOCH)] - pub activation_epoch: usize, + pub activation_epoch: u32, /// Number of active epochs #[arg(long, default_value_t = DEFAULT_NUM_ACTIVE_EPOCHS)] - pub num_active_epochs: usize, + pub num_active_epochs: u32, + + /// Path for keystore directory (relative to data-dir if not absolute) + #[arg(long)] + pub keystore_path: Option, } impl Default for AccountManagerConfig { @@ -40,8 +50,10 @@ impl Default for AccountManagerConfig { lifetime: 18, chunk_size: 5, seed_phrase: None, + passphrase: None, activation_epoch: DEFAULT_ACTIVATION_EPOCH, num_active_epochs: DEFAULT_NUM_ACTIVE_EPOCHS, + keystore_path: None, } } } @@ -60,6 +72,7 @@ impl AccountManagerConfig { self.lifetime >= MIN_LIFETIME, "Lifetime must be at least {MIN_LIFETIME}" ); + Ok(()) } @@ -67,7 +80,14 @@ impl AccountManagerConfig { if let Some(phrase) = &self.seed_phrase { phrase.clone() } else { - "default_seed_phrase".to_string() + // Generate a new BIP39 mnemonic with 24 words (256 bits of entropy) + let entropy: [u8; 32] = rand::random(); + let mnemonic = Mnemonic::from_entropy(&entropy).expect("Failed to generate mnemonic"); + let phrase = mnemonic.words().collect::>().join(" "); + info!("{}", "=".repeat(89)); + info!("Generated new seed phrase (KEEP SAFE): {phrase}"); + info!("{}", "=".repeat(89)); + phrase } } } diff --git a/bin/ream/src/main.rs b/bin/ream/src/main.rs index d3a6facff..09c1a5db3 100644 --- a/bin/ream/src/main.rs +++ b/bin/ream/src/main.rs @@ -1,6 +1,7 @@ use std::{ env, fs, net::SocketAddr, + path::{Path, PathBuf}, process, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -19,6 +20,7 @@ use ream::cli::{ validator_node::ValidatorNodeConfig, voluntary_exit::VoluntaryExitConfig, }; +use ream_account_manager::{keystore::Keystore, message_types::MessageType}; use ream_api_types_beacon::id::ValidatorID; use ream_api_types_common::id::ID; use ream_chain_lean::{ @@ -87,7 +89,7 @@ fn main() { reset_db(&ream_dir).expect("Unable to delete database"); } - let ream_db = ReamDB::new(ream_dir).expect("unable to init Ream Database"); + let ream_db = ReamDB::new(ream_dir.clone()).expect("unable to init Ream Database"); match cli.command { Commands::LeanNode(config) => { @@ -100,7 +102,7 @@ fn main() { executor_clone.spawn(async move { run_validator_node(*config, executor).await }); } Commands::AccountManager(config) => { - executor_clone.spawn(async move { run_account_manager(*config).await }); + executor_clone.spawn(async move { run_account_manager(*config, ream_dir).await }); } Commands::VoluntaryExit(config) => { executor_clone.spawn(async move { run_voluntary_exit(*config).await }); @@ -384,8 +386,8 @@ pub async fn run_validator_node(config: ValidatorNodeConfig, executor: ReamExecu /// /// This function initializes the account manager by validating the configuration, /// generating keys, and starting the account manager service. -pub async fn run_account_manager(mut config: AccountManagerConfig) { - info!("starting up account manager..."); +pub async fn run_account_manager(mut config: AccountManagerConfig, ream_dir: PathBuf) { + info!("Starting account manager..."); // Validate the configuration config @@ -399,17 +401,65 @@ pub async fn run_account_manager(mut config: AccountManagerConfig) { let seed_phrase = config.get_seed_phrase(); + // Create keystore directory as subdirectory of data directory + let keystore_dir = match &config.keystore_path { + Some(custom_path) => { + let path = Path::new(custom_path); + if path.is_absolute() { + path.to_path_buf() + } else { + ream_dir.join(custom_path) + } + } + None => ream_dir.join("keystores"), + }; + + if !keystore_dir.exists() { + fs::create_dir_all(&keystore_dir).expect("Failed to create keystore directory"); + info!( + "Created keystore directory: {path}", + path = keystore_dir.display() + ); + } + // Measure key generation time let start_time = Instant::now(); - let (_public_key, _private_key) = ream_account_manager::generate_keys( - &seed_phrase, - config.activation_epoch, - config.num_active_epochs, - ); + + // Generate keys sequentially for each message type + for (index, message_type) in MessageType::iter().enumerate() { + let (_public_key, _private_key) = ream_account_manager::generate_key_pair_with_salt( + &seed_phrase, + index as u32, + config.activation_epoch, + config.num_active_epochs, + config.passphrase.as_deref().unwrap_or(""), + ); + + // Create keystore file using Keystore + let keystore = Keystore::from_seed_phrase( + &seed_phrase, + config.lifetime, + config.activation_epoch, + Some(format!("Ream validator keystore for {message_type}")), + Some(format!("m/44'/60'/0'/0/{index}")), + ); + + // Write keystore to file with enum name + let filename = message_type.to_string(); + let keystore_file_path = keystore_dir.join(filename); + let keystore_json = + ::serde_json::to_string_pretty(&keystore).expect("Failed to serialize keystore"); + + fs::write(&keystore_file_path, keystore_json).expect("Failed to write keystore file"); + + info!("Keystore written to path: {}", keystore_file_path.display()); + } let duration = start_time.elapsed(); info!("Key generation complete, took {:?}", duration); info!("Account manager completed successfully"); + + process::exit(0); } /// Runs the voluntary exit process. diff --git a/book/cli/ream/account_manager.md b/book/cli/ream/account_manager.md index 51bf66b69..9794758ab 100644 --- a/book/cli/ream/account_manager.md +++ b/book/cli/ream/account_manager.md @@ -9,11 +9,22 @@ $ ream account_manager --help Usage: ream account_manager [OPTIONS] Options: - -v, --verbosity Verbosity level [default: 3] - -l, --lifetime Account lifetime in 2 ** lifetime slots [default: 18] - -c, --chunk-size Chunk size for messages [default: 5] - -s, --seed-phrase Seed phrase for key generation - --activation-epoch Activation epoch for the validator [default: 0] - --num-active-epochs Number of active epochs [default: 262144] - -h, --help Print help + -v, --verbosity + Verbosity level [default: 3] + -l, --lifetime + Account lifetime in 2 ** lifetime slots [default: 18] + -c, --chunk-size + Chunk size for messages [default: 5] + -s, --seed-phrase + Seed phrase for key generation + --passphrase + Optional BIP39 passphrase used with the seed phrase + --activation-epoch + Activation epoch for the validator [default: 0] + --num-active-epochs + Number of active epochs [default: 262144] + --keystore-path + Path for keystore directory (relative to data-dir if not absolute) + -h, --help + Print help ``` diff --git a/crates/common/account_manager/Cargo.toml b/crates/common/account_manager/Cargo.toml index 683bf2acf..4572b74a4 100644 --- a/crates/common/account_manager/Cargo.toml +++ b/crates/common/account_manager/Cargo.toml @@ -10,11 +10,18 @@ rust-version.workspace = true version.workspace = true [dependencies] +anyhow.workspace = true +bip39.workspace = true +chrono = { version = "0.4", features = ["serde"] } hashsig.workspace = true +hex.workspace = true rand.workspace = true rand_chacha.workspace = true +serde.workspace = true +serde_json.workspace = true sha2.workspace = true tracing.workspace = true +uuid = { version = "1.0", features = ["v4", "serde"] } # ream dependencies ream-post-quantum-crypto.workspace = true diff --git a/crates/common/account_manager/src/keystore.rs b/crates/common/account_manager/src/keystore.rs new file mode 100644 index 000000000..be5b9f1c0 --- /dev/null +++ b/crates/common/account_manager/src/keystore.rs @@ -0,0 +1,330 @@ +use anyhow::anyhow; +use chrono::{DateTime, Utc}; +use hex; +use rand; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::utils::validate_hex_string; + +// Constants +/// The required keystore version +pub const KEYSTORE_VERSION: u32 = 5; + +// Cryptographic algorithm enums with validation + +/// Key derivation function used for password-based key derivation +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] +pub enum KdfFunction { + #[default] + #[serde(rename = "argon2id")] + Argon2Id, +} + +/// Symmetric encryption cipher used for encrypting the private key +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] +pub enum CipherFunction { + #[default] + #[serde(rename = "aes-256-gcm")] + Aes256Gcm, +} + +/// Post-quantum signature scheme used for key generation and signing +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] +pub enum KeyTypeFunction { + #[default] + #[serde(rename = "xmss-poisedon2-ots-seed")] + XmssPoseidon2OtsSeed, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Keystore { + /// Version number, must be 5 + pub version: u32, + + /// Cryptographic parameters + pub crypto: CryptoParams, + + /// Key type specification + pub keytype: KeyType, + + /// Description of the keystore + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// Must be true for quantum security + pub quantum_secure: bool, + + /// UUID identifier + pub uuid: Uuid, + + /// Optional derivation path + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + + /// Metadata + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CryptoParams { + /// Key derivation function parameters + pub kdf: KdfParams, + + /// Cipher parameters and ciphertext + pub cipher: CipherParams, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct KdfParams { + /// KDF function name, must be "argon2id" + pub function: KdfFunction, + + /// KDF parameters - supports both naming conventions + pub params: KdfParamsInner, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum KdfParamsInner { + /// Full parameter names + Full { + memory: u32, + iterations: u32, + parallelism: u32, + salt: String, + }, + /// Short parameter names + Short { + m: u32, + t: u32, + p: u32, + salt: String, + }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CipherParams { + /// Cipher function name, must be "aes-256-gcm" + pub function: CipherFunction, + + /// Cipher parameters + pub params: CipherParamsInner, + + /// Encrypted data as hex string + pub ciphertext: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CipherParamsInner { + /// Nonce/IV as hex string + pub nonce: String, + + /// Authentication tag as hex string + pub tag: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct KeyType { + /// Key type function name + pub function: KeyTypeFunction, + + /// Key type parameters + pub params: KeyTypeParams, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct KeyTypeParams { + /// Key lifetime + pub lifetime: u32, + + /// Activation epoch + pub activation_epoch: u32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct KeystoreMeta { + /// Creation timestamp + #[serde(skip_serializing_if = "Option::is_none")] + pub created: Option>, +} + +impl Keystore { + /// Create a new quantum-secure keystore + pub fn new(crypto: CryptoParams, keytype: KeyType, uuid: Uuid) -> Self { + Self { + version: KEYSTORE_VERSION, + crypto, + keytype, + description: None, + quantum_secure: true, + uuid, + path: None, + meta: Some(KeystoreMeta { + created: Some(Utc::now()), + }), + } + } + + /// Create a new keystore from seed phrase and key parameters + pub fn from_seed_phrase( + seed_phrase: &str, + lifetime: u32, + activation_epoch: u32, + description: Option, + path: Option, + ) -> Self { + // Generate random salt for KDF (32 bytes) + let salt = hex::encode(rand::random::<[u8; 32]>()); + + // Generate random nonce for AES-GCM (12 bytes) + let nonce = hex::encode(rand::random::<[u8; 12]>()); + + // Generate random tag for AES-GCM (16 bytes) + let tag = hex::encode(rand::random::<[u8; 16]>()); + + // Store the seed phrase as encrypted data (hex encoded) + let ciphertext = hex::encode(seed_phrase.as_bytes()); + + let crypto = CryptoParams { + kdf: KdfParams::new_full(65536, 4, 2, salt), + cipher: CipherParams::new(nonce, tag, ciphertext), + }; + let keytype = KeyType::new(lifetime, activation_epoch); + + let mut keystore = Self::new(crypto, keytype, Uuid::new_v4()); + keystore.description = description; + keystore.path = path; + + keystore + } + + /// Validate the keystore structure + pub fn validate(&self) -> anyhow::Result<()> { + // Validate required constants for external data + if self.version != KEYSTORE_VERSION { + return Err(anyhow!("Version must be {KEYSTORE_VERSION}")); + } + if !self.quantum_secure { + return Err(anyhow!("quantum_secure must be true")); + } + + // Validate all hex strings + let cipher = &self.crypto.cipher; + if !validate_hex_string(&cipher.ciphertext) { + return Err(anyhow!("ciphertext must be a valid hex string")); + } + if !validate_hex_string(&cipher.params.nonce) { + return Err(anyhow!("nonce must be a valid hex string")); + } + if !validate_hex_string(&cipher.params.tag) { + return Err(anyhow!("tag must be a valid hex string")); + } + + let salt = match &self.crypto.kdf.params { + KdfParamsInner::Full { salt, .. } | KdfParamsInner::Short { salt, .. } => salt, + }; + if !validate_hex_string(salt) { + return Err(anyhow!("salt must be a valid hex string")); + } + + Ok(()) + } +} + +impl KdfParams { + /// Create new Argon2id KDF parameters (full names) + pub fn new_full(memory: u32, iterations: u32, parallelism: u32, salt: String) -> Self { + Self { + function: KdfFunction::default(), + params: KdfParamsInner::Full { + memory, + iterations, + parallelism, + salt, + }, + } + } + + /// Create new Argon2id KDF parameters (short names) + pub fn new_short(m: u32, t: u32, p: u32, salt: String) -> Self { + Self { + function: KdfFunction::default(), + params: KdfParamsInner::Short { m, t, p, salt }, + } + } +} + +impl CipherParams { + /// Create new AES-256-GCM cipher parameters + pub fn new(nonce: String, tag: String, ciphertext: String) -> Self { + Self { + function: CipherFunction::default(), + params: CipherParamsInner { nonce, tag }, + ciphertext, + } + } +} + +impl KeyType { + /// Create new XMSS-Poseidon2 OTS seed key type + pub fn new(lifetime: u32, activation_epoch: u32) -> Self { + Self { + function: KeyTypeFunction::default(), + params: KeyTypeParams { + lifetime, + activation_epoch, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keystore_creation() { + let uuid = Uuid::new_v4(); + + let kdf = KdfParams::new_full(65536, 3, 4, "0123456789abcdef".to_string()); + + let cipher = CipherParams::new( + "000102030405060708090a0b".to_string(), + "0123456789abcdef0123456789abcdef".to_string(), + "deadbeefcafe".to_string(), + ); + + let crypto = CryptoParams { kdf, cipher }; + + let keytype = KeyType::new(262144, 0); + + let keystore = Keystore::new(crypto, keytype, uuid); + + assert_eq!(keystore.version, 5); + assert!(keystore.quantum_secure); + assert!(keystore.validate().is_ok()); + } + + #[test] + fn test_json_serialization() { + let uuid = Uuid::new_v4(); + let kdf = KdfParams::new_short(65536, 3, 4, "0123456789abcdef".to_string()); + let cipher = CipherParams::new( + "000102030405060708090a0b".to_string(), + "0123456789abcdef0123456789abcdef".to_string(), + "deadbeefcafe".to_string(), + ); + let crypto = CryptoParams { kdf, cipher }; + let keytype = KeyType::new(262144, 0); + + let keystore = Keystore::new(crypto, keytype, uuid); + + let json = serde_json::to_string_pretty(&keystore).unwrap(); + let deserialized: Keystore = serde_json::from_str(&json).unwrap(); + + assert_eq!(keystore.version, deserialized.version); + assert_eq!(keystore.uuid, deserialized.uuid); + } +} diff --git a/crates/common/account_manager/src/lib.rs b/crates/common/account_manager/src/lib.rs index b6f629b14..03c095044 100644 --- a/crates/common/account_manager/src/lib.rs +++ b/crates/common/account_manager/src/lib.rs @@ -1,27 +1,51 @@ +pub mod keystore; +pub mod message_types; +pub mod utils; + +use std::str::FromStr; + +use bip39::Mnemonic; use rand::SeedableRng; use rand_chacha::ChaCha20Rng; use ream_post_quantum_crypto::hashsig::{private_key::PrivateKey, public_key::PublicKey}; use sha2::{Digest, Sha256}; use tracing::info; -pub fn generate_keys( +pub fn generate_key_pair_with_salt( seed_phrase: &str, - activation_epoch: usize, - num_active_epochs: usize, + wallet_index: u32, + activation_epoch: u32, + num_active_epochs: u32, + passphrase: &str, ) -> (PublicKey, PrivateKey) { info!( - "Generating lean consensus validator keys with activation_epoch={activation_epoch}, num_active_epochs={num_active_epochs}....." + "Generating lean consensus validator keys for index {wallet_index} with activation_epoch={activation_epoch}, num_active_epochs={num_active_epochs}....." ); - // Hash the seed phrase to get a 32-byte seed + // Parse the mnemonic phrase + let mnemonic = Mnemonic::from_str(seed_phrase).expect("Invalid mnemonic phrase"); + + // Generate seed from mnemonic using provided passphrase + let seed = mnemonic.to_seed(passphrase); + + // Create a deterministic seed based on the original seed and wallet index let mut hasher = Sha256::new(); - hasher.update(seed_phrase.as_bytes()); - let seed = hasher.finalize().into(); - info!("Seed: {seed:?}"); - - PrivateKey::generate( - &mut ::from_seed(seed), - activation_epoch, - num_active_epochs, - ) + hasher.update(seed); + hasher.update(wallet_index.to_be_bytes()); + let derived_seed: [u8; 32] = hasher.finalize().into(); + + // Use the derived seed directly for hashsig key generation + let (public_key, private_key) = PrivateKey::generate_key_pair( + &mut ::from_seed(derived_seed), + activation_epoch as usize, + num_active_epochs as usize, + ); + + // Display public key contents + match serde_json::to_string_pretty(&public_key.inner) { + Ok(json) => info!("Public key contents: {json}"), + Err(err) => info!("Public key generated successfully (could not serialize) due to {err}"), + } + + (public_key, private_key) } diff --git a/crates/common/account_manager/src/message_types.rs b/crates/common/account_manager/src/message_types.rs new file mode 100644 index 000000000..141706d92 --- /dev/null +++ b/crates/common/account_manager/src/message_types.rs @@ -0,0 +1,31 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum MessageType { + Attestation, + Block, +} + +impl std::fmt::Display for MessageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MessageType::Attestation => write!(f, "Attestation"), + MessageType::Block => write!(f, "Block"), + } + } +} + +impl MessageType { + /// Method to get all enum variants as an array + pub const fn all() -> [MessageType; 2] { + [MessageType::Attestation, MessageType::Block] + } + + /// Iterator method to loop through all variants + pub fn iter() -> impl Iterator + 'static { + Self::all().into_iter() + } + + /// Method to return the number of enum variants + pub const fn count() -> usize { + Self::all().len() + } +} diff --git a/crates/common/account_manager/src/utils.rs b/crates/common/account_manager/src/utils.rs new file mode 100644 index 000000000..dc157cfd9 --- /dev/null +++ b/crates/common/account_manager/src/utils.rs @@ -0,0 +1,29 @@ +/// Validates that a string contains only valid hexadecimal characters +/// +/// # Arguments +/// * `hex_str` - The string to validate +/// +/// # Returns +/// * `bool` - true if valid, false if invalid +pub fn validate_hex_string(hex_str: &str) -> bool { + hex_str.chars().all(|c| c.is_ascii_hexdigit()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_validate_hex_string_valid() { + assert!(validate_hex_string("0123456789abcdef")); + assert!(validate_hex_string("ABCDEF")); + assert!(validate_hex_string("")); + } + + #[test] + fn test_validate_hex_string_invalid() { + assert!(!validate_hex_string("0123456789abcdefg")); + assert!(!validate_hex_string("hello world")); + assert!(!validate_hex_string("0123456789abcdef!")); + } +} diff --git a/crates/crypto/post_quantum/src/hashsig/private_key.rs b/crates/crypto/post_quantum/src/hashsig/private_key.rs index 3ccc0e2f9..2a4799ff4 100644 --- a/crates/crypto/post_quantum/src/hashsig/private_key.rs +++ b/crates/crypto/post_quantum/src/hashsig/private_key.rs @@ -16,7 +16,7 @@ impl PrivateKey { Self { inner } } - pub fn generate( + pub fn generate_key_pair( rng: &mut R, activation_epoch: usize, num_active_epochs: usize, @@ -53,7 +53,7 @@ mod tests { let num_active_epochs = 10; // Test for 10 epochs for quick key generation let (public_key, private_key) = - PrivateKey::generate(&mut rng, activation_epoch, num_active_epochs); + PrivateKey::generate_key_pair(&mut rng, activation_epoch, num_active_epochs); let epoch = 5;