diff --git a/Cargo.lock b/Cargo.lock index 28e618748..aa511d6e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4421,6 +4421,7 @@ dependencies = [ "hex", "openssl", "operator_key", + "rayon", "rpassword", "serde", "serde_json", diff --git a/anchor/database/src/keysplit_operations.rs b/anchor/database/src/keysplit_operations.rs index e4aa1fe3f..cfaa243a9 100644 --- a/anchor/database/src/keysplit_operations.rs +++ b/anchor/database/src/keysplit_operations.rs @@ -7,9 +7,9 @@ use super::{DatabaseError, NetworkDatabase, sql_operations}; impl NetworkDatabase { // Get the public key for each operator id - pub fn get_keys_for_operators( + pub fn get_keys_for_operators<'a>( &self, - operators: Vec, + operators: impl IntoIterator, ) -> Result>, DatabaseError> { let conn = self.connection()?; let mut stmt = conn.prepare(sql_operations::GET_OPERATOR_KEY)?; diff --git a/anchor/keysplit/Cargo.toml b/anchor/keysplit/Cargo.toml index 8a09fe47e..097a6b12e 100644 --- a/anchor/keysplit/Cargo.toml +++ b/anchor/keysplit/Cargo.toml @@ -17,6 +17,7 @@ global_config = { workspace = true } hex = { workspace = true } openssl = { workspace = true } operator_key = { workspace = true } +rayon = "1.10.0" rpassword = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/anchor/keysplit/src/cli.rs b/anchor/keysplit/src/cli.rs index 8eb4cef4a..ada5b03c2 100644 --- a/anchor/keysplit/src/cli.rs +++ b/anchor/keysplit/src/cli.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, str::FromStr}; +use std::{collections::BTreeSet, path::PathBuf, str::FromStr}; use clap::Parser; use openssl::{pkey::Public, rsa::Rsa}; @@ -65,10 +65,11 @@ pub struct Manual { pub struct SharedKeygenOptions { #[clap( long, - help = "Path to the validator keystore file", + help = "Path(s) to the validator keystore file", + num_args = 1.., value_name = "PATH" )] - pub keystore_path: String, + pub keystore_paths: Vec, #[clap( long, @@ -95,7 +96,7 @@ pub struct SharedKeygenOptions { // Operators that are going to be part of the committee #[derive(Debug, Clone)] -pub struct OperatorIds(pub Vec); +pub struct OperatorIds(pub BTreeSet); // Enforce that the user can only enter 4, 7, 10, or 13 operators impl FromStr for OperatorIds { @@ -103,12 +104,12 @@ impl FromStr for OperatorIds { fn from_str(s: &str) -> Result { // First, parse all the numbers from the input string - let numbers: Vec = s + let numbers = s .split(',') .map(str::trim) .filter(|s| !s.is_empty()) .map(|num| num.parse::()) - .collect::, _>>() + .collect::, _>>() .map_err(|e| format!("Failed to parse number: {e}"))?; // Now validate the length matches our requirements diff --git a/anchor/keysplit/src/crypto.rs b/anchor/keysplit/src/crypto.rs index 88aed71e2..50ded2d50 100644 --- a/anchor/keysplit/src/crypto.rs +++ b/anchor/keysplit/src/crypto.rs @@ -2,12 +2,12 @@ use bls_lagrange::{KeyId, split}; use openssl::{encrypt::Encrypter, pkey::PKey}; use types::SecretKey; -use crate::{EncryptedKeyShare, KeyShare, KeysplitError, cli::SharedKeygenOptions}; +use crate::{EncryptedKeyShare, KeyShare, KeysplitError, cli::SharedKeygenOptions, split::Split}; // Given a secret key, split it into parts -pub fn split_keys( +pub fn split_key( shared: &SharedKeygenOptions, - sk: SecretKey, + sk: &SecretKey, ) -> Result, KeysplitError> { let num_operators = shared.operators.0.len(); let threshold = num_operators - ((num_operators - 1) / 3); @@ -17,17 +17,20 @@ pub fn split_keys( .operators .0 .iter() - .map(|id| KeyId::try_from(*id).unwrap()); + .map(|id| KeyId::try_from(*id)) + .collect::, _>>() + .map_err(|e| KeysplitError::SplitFailure(format!("Failed to create key id: {e:?}")))?; - split(&sk, threshold as u64, key_ids) + split(sk, threshold as u64, key_ids) .map_err(|e| KeysplitError::SplitFailure(format!("Failed to split key: {e:?}"))) } // Encrypt the keyshare with the operators rsa public key pub fn encrypt_keyshares( - key_shares: Vec, -) -> Result, KeysplitError> { - key_shares + split: Split, +) -> Result, KeysplitError> { + let key_shares = split + .key_shares .into_iter() .map(|share| { let pkey = PKey::from_rsa(share.public_key.clone()) @@ -58,5 +61,10 @@ pub fn encrypt_keyshares( share_public_key: share.keyshare.public_key(), }) }) - .collect() + .collect::, _>>()?; + + Ok(Split { + nonce: split.nonce, + key_shares, + }) } diff --git a/anchor/keysplit/src/lib.rs b/anchor/keysplit/src/lib.rs index d51cd8bc6..4b5d68548 100644 --- a/anchor/keysplit/src/lib.rs +++ b/anchor/keysplit/src/lib.rs @@ -4,11 +4,12 @@ pub use cli::{KeygenSubcommands, Keysplit, Manual, Onchain}; use error::KeysplitError; use global_config::GlobalConfig; use openssl::{pkey::Public, rsa::Rsa}; +use rayon::prelude::*; use tracing::info; use types::{PublicKey, SecretKey}; use crate::{ - crypto::{encrypt_keyshares, split_keys}, + crypto::{encrypt_keyshares, split_key}, output::OutputData, split::{manual_split, onchain_split}, util::read_password, @@ -17,19 +18,19 @@ use crate::{ mod cli; mod crypto; mod error; -mod output; +pub mod output; mod split; mod util; // A specific operators keyshare -pub(crate) struct KeyShare { +pub struct KeyShare { id: u64, public_key: Rsa, keyshare: SecretKey, } // A keyshare where the secretkey has been encrypted with the operators public key -pub(crate) struct EncryptedKeyShare { +pub struct EncryptedKeyShare { id: u64, public_key: Rsa, share_public_key: PublicKey, @@ -40,45 +41,55 @@ pub fn run_keysplitter( keysplit: Keysplit, global_config: GlobalConfig, ) -> Result<(), KeysplitError> { - let shared = keysplit.get_shared().clone(); + let shared = keysplit.get_shared(); info!("----- Anchor Keysplitter -----"); - // 1) Read in the keystore file and parse it into a usable format - info!( - "Reading in validator keystore file from {}...", - shared.keystore_path - ); - let keystore = eth2_keystore::Keystore::from_json_file(&shared.keystore_path) - .map_err(|e| KeysplitError::Keystore(format!("Failed to read keystore file: {e:?}")))?; - info!("Successfully read in validator keystore file"); + // 1) Read in the keystore files and parse them into a usable format + let keystores = shared + .keystore_paths + .iter() + .map(|path| { + info!("Reading in validator keystore file from {path}...",); + eth2_keystore::Keystore::from_json_file(path).map_err(|e| { + KeysplitError::Keystore(format!("Failed to read keystore file: {e:?}")) + }) + }) + .collect::, _>>()?; + info!("Successfully read in validator keystore file(s)"); // 2) Extract the validator keys from the keystore file - info!("Extracting keys from keystore file..."); - let keys = keystore - .decrypt_keypair( - read_password(shared.password_file.as_deref()) - .map_err(|e| KeysplitError::Keystore(format!("Unable to get password: {e}")))? - .as_bytes(), - ) - .map_err(|e| KeysplitError::Keystore(format!("Failed to decrypt keystore file: {e:?}")))?; - info!("Successfully extracted keys from keystore file"); + info!("Extracting keys from keystore file(s)..."); + let password = read_password(shared.password_file.as_deref()) + .map_err(|e| KeysplitError::Keystore(format!("Unable to get password: {e}")))?; + let keys = keystores + .into_par_iter() + .map(|keystore| { + keystore.decrypt_keypair(password.as_bytes()).map_err(|e| { + KeysplitError::Keystore(format!("Failed to decrypt keystore file: {e:?}")) + }) + }) + .collect::, _>>()?; + info!("Successfully extracted keys from keystore file(s)"); // 3) Split the key into keyshares and group together relevant information info!( - "Splitting validator key into {} shares...", + "Splitting validator key(s) into {} shares...", shared.operators.0.len() ); - let (keyshares, nonce) = match keysplit.subcommand { - KeygenSubcommands::Manual(manual) => manual_split(manual, keys.sk.clone()), + let splits = match keysplit.subcommand { + KeygenSubcommands::Manual(manual) => manual_split(manual, keys.iter().map(|k| &k.sk)), KeygenSubcommands::Onchain(onchain) => { - onchain_split(onchain, global_config, keys.sk.clone()) + onchain_split(onchain, global_config, keys.iter().map(|k| &k.sk)) } }?; info!("Successfully split validator key into shares"); // 4) Encrypt the keyshares with the operators public keys info!("Encrypting keyshares..."); - let encrypted_keyshares = encrypt_keyshares(keyshares)?; + let encrypted_keyshares = splits + .into_par_iter() + .map(encrypt_keyshares) + .collect::, _>>()?; info!("Encrypted all keyshares!"); // 5) Construct the payload and turn data into proper output format. @@ -86,7 +97,7 @@ pub fn run_keysplitter( "Constructing output and writing to file {}...", shared.output_path ); - let output = OutputData::new(encrypted_keyshares, shared.clone(), keys, nonce); + let output = OutputData::new(encrypted_keyshares, &shared, keys)?; // 6) Write output data to file let json_data = serde_json::to_string_pretty(&output).map_err(|e| { diff --git a/anchor/keysplit/src/output.rs b/anchor/keysplit/src/output.rs index 8871a6aad..a73cb16e4 100644 --- a/anchor/keysplit/src/output.rs +++ b/anchor/keysplit/src/output.rs @@ -4,7 +4,10 @@ use openssl::{pkey::Public, rsa::Rsa}; use serde::Serialize; use types::{Address, Keypair, PublicKey}; -use crate::{EncryptedKeyShare, cli::SharedKeygenOptions, util::serialize_rsa}; +use crate::{ + EncryptedKeyShare, cli::SharedKeygenOptions, error::KeysplitError, split::Split, + util::serialize_rsa, +}; const VERSION: &str = "v1.2.1"; @@ -61,29 +64,43 @@ impl From for Operator { impl OutputData { pub fn new( - encrypted_keys: Vec, - shared: SharedKeygenOptions, - keys: Keypair, - nonce: u64, - ) -> Self { - let payload = Payload::new(&encrypted_keys, &keys, nonce, shared.owner); - let operators: Vec = encrypted_keys.into_iter().map(Operator::from).collect(); - - let output_key_data = OutputKeyData { - owner_nonce: nonce, - owner_address: shared.owner, - public_key: keys.pk, - operators, - }; + encrypted_keys: Vec>, + shared: &SharedKeygenOptions, + keys: Vec, + ) -> Result { + if encrypted_keys.len() != keys.len() { + return Err(KeysplitError::Misc( + "Mismatch between encrypted keys shares and keypairs".to_string(), + )); + } - Self { + let shares = encrypted_keys + .into_iter() + .zip(keys) + .map(|(share, key)| { + let payload = Payload::new(&share.key_shares, &key, share.nonce, shared.owner); + let operators: Vec = + share.key_shares.into_iter().map(Operator::from).collect(); + + let output_key_data = OutputKeyData { + owner_nonce: share.nonce, + owner_address: shared.owner, + public_key: key.pk, + operators, + }; + + OutputKeyShare { + data: output_key_data, + payload, + } + }) + .collect(); + + Ok(Self { version: VERSION.to_string(), created_at: Utc::now(), - shares: vec![OutputKeyShare { - data: output_key_data, - payload, - }], - } + shares, + }) } } diff --git a/anchor/keysplit/src/split.rs b/anchor/keysplit/src/split.rs index 074f30762..bc5a6afb6 100644 --- a/anchor/keysplit/src/split.rs +++ b/anchor/keysplit/src/split.rs @@ -3,17 +3,24 @@ use std::{path::Path, sync::Arc}; use database::NetworkDatabase; use eth::SsvEventSyncer; use global_config::GlobalConfig; -use openssl::rsa::Rsa; +use openssl::{pkey::Public, rsa::Rsa}; use ssv_types::domain_type::DomainType; use types::SecretKey; -use crate::{KeyShare, KeysplitError, Manual, Onchain, split_keys}; +use crate::{KeyShare, KeysplitError, Manual, Onchain, cli::SharedKeygenOptions, split_key}; + +/// A single successfully split validator key. Contains a Vec of the key shares ([`KeyShare`] or +/// [`EncryptedKeyShare`]) and the nonce needed to sign the shares. +pub struct Split { + pub key_shares: Vec, + pub nonce: u64, +} // Split the key with manually input nonce value and rsa public keys -pub fn manual_split( +pub fn manual_split<'a>( manual: Manual, - secret_key: SecretKey, -) -> Result<(Vec, u64), KeysplitError> { + secret_keys: impl IntoIterator, +) -> Result>, KeysplitError> { // Make sure num operators == num keys if manual.shared.operators.0.len() != manual.public_keys.len() { return Err(KeysplitError::InvalidKeyLen( @@ -21,34 +28,21 @@ pub fn manual_split( )); } - // Split the secret key into N keyshares - let split_keys = split_keys(&manual.shared, secret_key)?; - - // With each keyshare, zip it with its corresponding rsa public key - Ok(( - split_keys - .into_iter() - .zip(manual.public_keys) - .map(|(split_key, rsa)| KeyShare { - id: u64::from(split_key.0), - public_key: rsa, - keyshare: split_key.1, - }) - .collect(), + create_keyshares_for_keys( manual.nonce, - )) + &manual.shared, + secret_keys, + &manual.public_keys, + ) } // Split the key using onchain data. This takes human error out of the equation and utilizes data // scrapped from the chain to input the correct operator public keys and owner nonce -pub fn onchain_split( +pub fn onchain_split<'a>( onchain: Onchain, global_config: GlobalConfig, - secret_key: SecretKey, -) -> Result<(Vec, u64), KeysplitError> { - // Split the secret key into N shares - let split_keys = split_keys(&onchain.shared, secret_key)?; - + secret_keys: impl IntoIterator, +) -> Result>, KeysplitError> { // Construct DB and perform sync let db = build_db(); let mut syncer = @@ -61,7 +55,7 @@ pub fn onchain_split( runtime.block_on(async { syncer.keysplit_sync().await }); let public_keys = db - .get_keys_for_operators(onchain.shared.operators.0) + .get_keys_for_operators(&onchain.shared.operators.0) .map_err(|_| { KeysplitError::InvalidOperator("One or more operators do not exist".to_string()) })?; @@ -76,19 +70,46 @@ pub fn onchain_split( } }; + create_keyshares_for_keys(nonce, &onchain.shared, secret_keys, &public_keys) +} + +fn create_keyshares_for_keys<'a>( + nonce: u64, + shared: &SharedKeygenOptions, + secret_keys: impl IntoIterator, + public_keys: &[Rsa], +) -> Result>, KeysplitError> { + secret_keys + .into_iter() + .enumerate() + .map(|(i, secret_key)| { + create_keyshares_for_key(nonce + i as u64, shared, secret_key, public_keys) + }) + .collect() +} + +fn create_keyshares_for_key( + nonce: u64, + shared: &SharedKeygenOptions, + secret_key: &SecretKey, + public_keys: &[Rsa], +) -> Result, KeysplitError> { + // Split the secret key into N shares + let split_keys = split_key(shared, secret_key)?; + // With each keyshare, zip it with its corresponding rsa public key - Ok(( - split_keys + Ok(Split { + key_shares: split_keys .into_iter() - .zip(public_keys) + .zip(public_keys.iter()) .map(|(split_key, rsa)| KeyShare { id: u64::from(split_key.0), - public_key: rsa, + public_key: rsa.clone(), keyshare: split_key.1, }) .collect(), nonce, - )) + }) } // Build a network database for the keysplit