Skip to content

Commit 7ac903a

Browse files
authored
feat(keysplit): Split multiple validator keys into one file (#440)
Support splitting multiple keys into a single json file, to allow the user registering multiple validators in one go. Co-Authored-By: Daniel Knopik <[email protected]>
1 parent fffaae7 commit 7ac903a

File tree

8 files changed

+158
-98
lines changed

8 files changed

+158
-98
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

anchor/database/src/keysplit_operations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ use super::{DatabaseError, NetworkDatabase, sql_operations};
77

88
impl NetworkDatabase {
99
// Get the public key for each operator id
10-
pub fn get_keys_for_operators(
10+
pub fn get_keys_for_operators<'a>(
1111
&self,
12-
operators: Vec<u64>,
12+
operators: impl IntoIterator<Item = &'a u64>,
1313
) -> Result<Vec<Rsa<Public>>, DatabaseError> {
1414
let conn = self.connection()?;
1515
let mut stmt = conn.prepare(sql_operations::GET_OPERATOR_KEY)?;

anchor/keysplit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ global_config = { workspace = true }
1717
hex = { workspace = true }
1818
openssl = { workspace = true }
1919
operator_key = { workspace = true }
20+
rayon = "1.10.0"
2021
rpassword = { workspace = true }
2122
serde = { workspace = true }
2223
serde_json = { workspace = true }

anchor/keysplit/src/cli.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{path::PathBuf, str::FromStr};
1+
use std::{collections::BTreeSet, path::PathBuf, str::FromStr};
22

33
use clap::Parser;
44
use openssl::{pkey::Public, rsa::Rsa};
@@ -65,10 +65,11 @@ pub struct Manual {
6565
pub struct SharedKeygenOptions {
6666
#[clap(
6767
long,
68-
help = "Path to the validator keystore file",
68+
help = "Path(s) to the validator keystore file",
69+
num_args = 1..,
6970
value_name = "PATH"
7071
)]
71-
pub keystore_path: String,
72+
pub keystore_paths: Vec<String>,
7273

7374
#[clap(
7475
long,
@@ -95,20 +96,20 @@ pub struct SharedKeygenOptions {
9596

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

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

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

114115
// Now validate the length matches our requirements

anchor/keysplit/src/crypto.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ use bls_lagrange::{KeyId, split};
22
use openssl::{encrypt::Encrypter, pkey::PKey};
33
use types::SecretKey;
44

5-
use crate::{EncryptedKeyShare, KeyShare, KeysplitError, cli::SharedKeygenOptions};
5+
use crate::{EncryptedKeyShare, KeyShare, KeysplitError, cli::SharedKeygenOptions, split::Split};
66

77
// Given a secret key, split it into parts
8-
pub fn split_keys(
8+
pub fn split_key(
99
shared: &SharedKeygenOptions,
10-
sk: SecretKey,
10+
sk: &SecretKey,
1111
) -> Result<Vec<(KeyId, SecretKey)>, KeysplitError> {
1212
let num_operators = shared.operators.0.len();
1313
let threshold = num_operators - ((num_operators - 1) / 3);
@@ -17,17 +17,20 @@ pub fn split_keys(
1717
.operators
1818
.0
1919
.iter()
20-
.map(|id| KeyId::try_from(*id).unwrap());
20+
.map(|id| KeyId::try_from(*id))
21+
.collect::<Result<Vec<_>, _>>()
22+
.map_err(|e| KeysplitError::SplitFailure(format!("Failed to create key id: {e:?}")))?;
2123

22-
split(&sk, threshold as u64, key_ids)
24+
split(sk, threshold as u64, key_ids)
2325
.map_err(|e| KeysplitError::SplitFailure(format!("Failed to split key: {e:?}")))
2426
}
2527

2628
// Encrypt the keyshare with the operators rsa public key
2729
pub fn encrypt_keyshares(
28-
key_shares: Vec<KeyShare>,
29-
) -> Result<Vec<EncryptedKeyShare>, KeysplitError> {
30-
key_shares
30+
split: Split<KeyShare>,
31+
) -> Result<Split<EncryptedKeyShare>, KeysplitError> {
32+
let key_shares = split
33+
.key_shares
3134
.into_iter()
3235
.map(|share| {
3336
let pkey = PKey::from_rsa(share.public_key.clone())
@@ -58,5 +61,10 @@ pub fn encrypt_keyshares(
5861
share_public_key: share.keyshare.public_key(),
5962
})
6063
})
61-
.collect()
64+
.collect::<Result<Vec<_>, _>>()?;
65+
66+
Ok(Split {
67+
nonce: split.nonce,
68+
key_shares,
69+
})
6270
}

anchor/keysplit/src/lib.rs

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ pub use cli::{KeygenSubcommands, Keysplit, Manual, Onchain};
44
use error::KeysplitError;
55
use global_config::GlobalConfig;
66
use openssl::{pkey::Public, rsa::Rsa};
7+
use rayon::prelude::*;
78
use tracing::info;
89
use types::{PublicKey, SecretKey};
910

1011
use crate::{
11-
crypto::{encrypt_keyshares, split_keys},
12+
crypto::{encrypt_keyshares, split_key},
1213
output::OutputData,
1314
split::{manual_split, onchain_split},
1415
util::read_password,
@@ -17,19 +18,19 @@ use crate::{
1718
mod cli;
1819
mod crypto;
1920
mod error;
20-
mod output;
21+
pub mod output;
2122
mod split;
2223
mod util;
2324

2425
// A specific operators keyshare
25-
pub(crate) struct KeyShare {
26+
pub struct KeyShare {
2627
id: u64,
2728
public_key: Rsa<Public>,
2829
keyshare: SecretKey,
2930
}
3031

3132
// A keyshare where the secretkey has been encrypted with the operators public key
32-
pub(crate) struct EncryptedKeyShare {
33+
pub struct EncryptedKeyShare {
3334
id: u64,
3435
public_key: Rsa<Public>,
3536
share_public_key: PublicKey,
@@ -40,53 +41,63 @@ pub fn run_keysplitter(
4041
keysplit: Keysplit,
4142
global_config: GlobalConfig,
4243
) -> Result<(), KeysplitError> {
43-
let shared = keysplit.get_shared().clone();
44+
let shared = keysplit.get_shared();
4445
info!("----- Anchor Keysplitter -----");
4546

46-
// 1) Read in the keystore file and parse it into a usable format
47-
info!(
48-
"Reading in validator keystore file from {}...",
49-
shared.keystore_path
50-
);
51-
let keystore = eth2_keystore::Keystore::from_json_file(&shared.keystore_path)
52-
.map_err(|e| KeysplitError::Keystore(format!("Failed to read keystore file: {e:?}")))?;
53-
info!("Successfully read in validator keystore file");
47+
// 1) Read in the keystore files and parse them into a usable format
48+
let keystores = shared
49+
.keystore_paths
50+
.iter()
51+
.map(|path| {
52+
info!("Reading in validator keystore file from {path}...",);
53+
eth2_keystore::Keystore::from_json_file(path).map_err(|e| {
54+
KeysplitError::Keystore(format!("Failed to read keystore file: {e:?}"))
55+
})
56+
})
57+
.collect::<Result<Vec<_>, _>>()?;
58+
info!("Successfully read in validator keystore file(s)");
5459

5560
// 2) Extract the validator keys from the keystore file
56-
info!("Extracting keys from keystore file...");
57-
let keys = keystore
58-
.decrypt_keypair(
59-
read_password(shared.password_file.as_deref())
60-
.map_err(|e| KeysplitError::Keystore(format!("Unable to get password: {e}")))?
61-
.as_bytes(),
62-
)
63-
.map_err(|e| KeysplitError::Keystore(format!("Failed to decrypt keystore file: {e:?}")))?;
64-
info!("Successfully extracted keys from keystore file");
61+
info!("Extracting keys from keystore file(s)...");
62+
let password = read_password(shared.password_file.as_deref())
63+
.map_err(|e| KeysplitError::Keystore(format!("Unable to get password: {e}")))?;
64+
let keys = keystores
65+
.into_par_iter()
66+
.map(|keystore| {
67+
keystore.decrypt_keypair(password.as_bytes()).map_err(|e| {
68+
KeysplitError::Keystore(format!("Failed to decrypt keystore file: {e:?}"))
69+
})
70+
})
71+
.collect::<Result<Vec<_>, _>>()?;
72+
info!("Successfully extracted keys from keystore file(s)");
6573

6674
// 3) Split the key into keyshares and group together relevant information
6775
info!(
68-
"Splitting validator key into {} shares...",
76+
"Splitting validator key(s) into {} shares...",
6977
shared.operators.0.len()
7078
);
71-
let (keyshares, nonce) = match keysplit.subcommand {
72-
KeygenSubcommands::Manual(manual) => manual_split(manual, keys.sk.clone()),
79+
let splits = match keysplit.subcommand {
80+
KeygenSubcommands::Manual(manual) => manual_split(manual, keys.iter().map(|k| &k.sk)),
7381
KeygenSubcommands::Onchain(onchain) => {
74-
onchain_split(onchain, global_config, keys.sk.clone())
82+
onchain_split(onchain, global_config, keys.iter().map(|k| &k.sk))
7583
}
7684
}?;
7785
info!("Successfully split validator key into shares");
7886

7987
// 4) Encrypt the keyshares with the operators public keys
8088
info!("Encrypting keyshares...");
81-
let encrypted_keyshares = encrypt_keyshares(keyshares)?;
89+
let encrypted_keyshares = splits
90+
.into_par_iter()
91+
.map(encrypt_keyshares)
92+
.collect::<Result<Vec<_>, _>>()?;
8293
info!("Encrypted all keyshares!");
8394

8495
// 5) Construct the payload and turn data into proper output format.
8596
info!(
8697
"Constructing output and writing to file {}...",
8798
shared.output_path
8899
);
89-
let output = OutputData::new(encrypted_keyshares, shared.clone(), keys, nonce);
100+
let output = OutputData::new(encrypted_keyshares, &shared, keys)?;
90101

91102
// 6) Write output data to file
92103
let json_data = serde_json::to_string_pretty(&output).map_err(|e| {

anchor/keysplit/src/output.rs

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ use openssl::{pkey::Public, rsa::Rsa};
44
use serde::Serialize;
55
use types::{Address, Keypair, PublicKey};
66

7-
use crate::{EncryptedKeyShare, cli::SharedKeygenOptions, util::serialize_rsa};
7+
use crate::{
8+
EncryptedKeyShare, cli::SharedKeygenOptions, error::KeysplitError, split::Split,
9+
util::serialize_rsa,
10+
};
811

912
const VERSION: &str = "v1.2.1";
1013

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

6265
impl OutputData {
6366
pub fn new(
64-
encrypted_keys: Vec<EncryptedKeyShare>,
65-
shared: SharedKeygenOptions,
66-
keys: Keypair,
67-
nonce: u64,
68-
) -> Self {
69-
let payload = Payload::new(&encrypted_keys, &keys, nonce, shared.owner);
70-
let operators: Vec<Operator> = encrypted_keys.into_iter().map(Operator::from).collect();
71-
72-
let output_key_data = OutputKeyData {
73-
owner_nonce: nonce,
74-
owner_address: shared.owner,
75-
public_key: keys.pk,
76-
operators,
77-
};
67+
encrypted_keys: Vec<Split<EncryptedKeyShare>>,
68+
shared: &SharedKeygenOptions,
69+
keys: Vec<Keypair>,
70+
) -> Result<Self, KeysplitError> {
71+
if encrypted_keys.len() != keys.len() {
72+
return Err(KeysplitError::Misc(
73+
"Mismatch between encrypted keys shares and keypairs".to_string(),
74+
));
75+
}
7876

79-
Self {
77+
let shares = encrypted_keys
78+
.into_iter()
79+
.zip(keys)
80+
.map(|(share, key)| {
81+
let payload = Payload::new(&share.key_shares, &key, share.nonce, shared.owner);
82+
let operators: Vec<Operator> =
83+
share.key_shares.into_iter().map(Operator::from).collect();
84+
85+
let output_key_data = OutputKeyData {
86+
owner_nonce: share.nonce,
87+
owner_address: shared.owner,
88+
public_key: key.pk,
89+
operators,
90+
};
91+
92+
OutputKeyShare {
93+
data: output_key_data,
94+
payload,
95+
}
96+
})
97+
.collect();
98+
99+
Ok(Self {
80100
version: VERSION.to_string(),
81101
created_at: Utc::now(),
82-
shares: vec![OutputKeyShare {
83-
data: output_key_data,
84-
payload,
85-
}],
86-
}
102+
shares,
103+
})
87104
}
88105
}
89106

0 commit comments

Comments
 (0)