Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions anchor/database/src/keysplit_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64>,
operators: impl IntoIterator<Item = &'a u64>,
) -> Result<Vec<Rsa<Public>>, DatabaseError> {
let conn = self.connection()?;
let mut stmt = conn.prepare(sql_operations::GET_OPERATOR_KEY)?;
Expand Down
1 change: 1 addition & 0 deletions anchor/keysplit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
13 changes: 7 additions & 6 deletions anchor/keysplit/src/cli.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -65,10 +65,11 @@ pub struct Manual {
pub struct SharedKeygenOptions {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something I thought of is that we should probably adjust our cli names here to match their docs https://docs.ssv.network/stakers/tools/ssv-keys-cli/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of our flags function differently, e.g. keystore_paths takes multiple paths instead of a folder if the user wants to split multiple and output_path points to the folder to place the input into (instead of being the path for the file to be written). We could change that of course, but personally I prefer our current behavior. wdyt?

My point is that matching the names (IMO) only has value if we also match the behavior.

#[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<String>,

#[clap(
long,
Expand All @@ -95,20 +96,20 @@ pub struct SharedKeygenOptions {

// Operators that are going to be part of the committee
#[derive(Debug, Clone)]
pub struct OperatorIds(pub Vec<u64>);
pub struct OperatorIds(pub BTreeSet<u64>);

// Enforce that the user can only enter 4, 7, 10, or 13 operators
impl FromStr for OperatorIds {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
// First, parse all the numbers from the input string
let numbers: Vec<u64> = s
let numbers = s
.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(|num| num.parse::<u64>())
.collect::<Result<Vec<u64>, _>>()
.collect::<Result<BTreeSet<_>, _>>()
.map_err(|e| format!("Failed to parse number: {e}"))?;

// Now validate the length matches our requirements
Expand Down
26 changes: 17 additions & 9 deletions anchor/keysplit/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<(KeyId, SecretKey)>, KeysplitError> {
let num_operators = shared.operators.0.len();
let threshold = num_operators - ((num_operators - 1) / 3);
Expand All @@ -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::<Result<Vec<_>, _>>()
.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<KeyShare>,
) -> Result<Vec<EncryptedKeyShare>, KeysplitError> {
key_shares
split: Split<KeyShare>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, it's not clear to me what the difference between Vec<KeyShare> and Split<KeyShare> is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a comment

) -> Result<Split<EncryptedKeyShare>, KeysplitError> {
let key_shares = split
.key_shares
.into_iter()
.map(|share| {
let pkey = PKey::from_rsa(share.public_key.clone())
Expand Down Expand Up @@ -58,5 +61,10 @@ pub fn encrypt_keyshares(
share_public_key: share.keyshare.public_key(),
})
})
.collect()
.collect::<Result<Vec<_>, _>>()?;

Ok(Split {
nonce: split.nonce,
key_shares,
})
}
67 changes: 39 additions & 28 deletions anchor/keysplit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Public>,
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<Public>,
share_public_key: PublicKey,
Expand All @@ -40,53 +41,63 @@ 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::<Result<Vec<_>, _>>()?;
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::<Result<Vec<_>, _>>()?;
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::<Result<Vec<_>, _>>()?;
info!("Encrypted all keyshares!");

// 5) Construct the payload and turn data into proper output format.
info!(
"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| {
Expand Down
59 changes: 38 additions & 21 deletions anchor/keysplit/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -61,29 +64,43 @@ impl From<EncryptedKeyShare> for Operator {

impl OutputData {
pub fn new(
encrypted_keys: Vec<EncryptedKeyShare>,
shared: SharedKeygenOptions,
keys: Keypair,
nonce: u64,
) -> Self {
let payload = Payload::new(&encrypted_keys, &keys, nonce, shared.owner);
let operators: Vec<Operator> = 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<Split<EncryptedKeyShare>>,
shared: &SharedKeygenOptions,
keys: Vec<Keypair>,
) -> Result<Self, KeysplitError> {
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<Operator> =
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,
})
}
}

Expand Down
Loading
Loading