Skip to content

Commit

Permalink
Merge pull request #40 from ChorusOne/custom-testnets
Browse files Browse the repository at this point in the history
Support for custom testnet config in mnemonic cmds
  • Loading branch information
mksh authored Jul 5, 2024
2 parents 921da4a + 58c56f3 commit 71b3b8d
Show file tree
Hide file tree
Showing 13 changed files with 994 additions and 625 deletions.
542 changes: 265 additions & 277 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ Regenerate key and deposit data with existing mnemonic:
./target/debug/eth-staking-smith existing-mnemonic --chain mainnet --keystore_password testtest --mnemonic "entire habit bottom mention spoil clown finger wheat motion fox axis mechanic country make garment bar blind stadium sugar water scissors canyon often ketchup" --num_validators 1 --withdrawal_credentials "0x0100000000000000000000000000000000000000000000000000000000000001"
```

## Using custom testnet config

Both `existing-mnemonic` and `new-mnemonic` commands support generating validators for custom testnets.
To reference custom testnet config yaml file, pass `--testnet_config` parameter
with that config as value, and omit `--chain` parameter:

### Example command
```
./target/debug/eth-staking-smith new-mnemonic --testnet_config /etc/privatenet/config.yaml --keystore_password testtest --num_validators 1 --withdrawal_credentials "0x0100000000000000000000000000000000000000000000000000000000000001"
```

## Converting your BLS 0x00 withdrawal address

Ethereum will be implementing a push-based approach for withdrawals, see [EIP-4895 docs](https://eips.ethereum.org/EIPS/eip-4895).
Expand Down
41 changes: 41 additions & 0 deletions src/chain_spec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::path::Path;

use eth2_network_config::Eth2NetworkConfig;
use types::{ChainSpec, Config, MainnetEthSpec, MinimalEthSpec};

use crate::DepositError;

pub fn chain_spec_for_network(network_name: String) -> Result<ChainSpec, DepositError> {
if ["goerli", "prater", "mainnet", "holesky"].contains(&network_name.as_str()) {
Ok(Eth2NetworkConfig::constant(&network_name)
.unwrap()
.unwrap()
.chain_spec::<MainnetEthSpec>()
.unwrap())
} else {
Err(DepositError::InvalidNetworkName(format!(
"unknown chain name: {network_name}"
)))
}
}

pub fn chain_spec_from_file(chain_spec_file: String) -> Result<ChainSpec, DepositError> {
match Config::from_file(Path::new(chain_spec_file.as_str())) {
Ok(cfg) => {
let spec = if cfg.preset_base == "minimal" {
cfg.apply_to_chain_spec::<MinimalEthSpec>(&ChainSpec::minimal())
.unwrap()
} else {
cfg.apply_to_chain_spec::<MainnetEthSpec>(&ChainSpec::mainnet())
.unwrap()
};
Ok(spec)
}
Err(e) => {
log::error!("Unable to load chain spec config: {:?}", e);
Err(DepositError::NoCustomConfig(
"Can not parse config file for custom network config".to_string(),
))
}
}
}
2 changes: 1 addition & 1 deletion src/cli/bls_to_execution_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ pub fn subcommand<'a, 'b>() -> App<'a, 'b> {
.long("chain")
.required(true)
.takes_value(true)
.possible_values(&["goerli", "prater", "mainnet", "minimal", "holesky"])
.possible_values(&["goerli", "prater", "mainnet", "holesky"])
.help(
r#"The name of Ethereum PoS chain you are
targeting. Use "mainnet" if you are
Expand Down
36 changes: 30 additions & 6 deletions src/cli/existing_mnemonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ pub fn subcommand<'a, 'b>() -> App<'a, 'b> {
.arg(
Arg::with_name("chain")
.long("chain")
.required(true)
.required(false)
.takes_value(true)
.possible_values(&["goerli", "prater", "mainnet", "minimal", "holesky"])
.possible_values(&["goerli", "prater", "mainnet", "holesky"])
.help(
r#"The name of Ethereum PoS chain you are
targeting. Use "mainnet" if you are
Expand Down Expand Up @@ -93,6 +93,13 @@ pub fn subcommand<'a, 'b>() -> App<'a, 'b> {
and consequently slower performance vs `pbkdf2` achieving better performance with lower security parameters compared to `scrypt`",
),
)
.arg(
Arg::with_name("testnet_config")
.long("testnet_config")
.required(false)
.takes_value(true)
.help("Path to a custom Eth PoS chain config")
)
}

#[allow(clippy::needless_lifetimes)]
Expand All @@ -105,9 +112,26 @@ pub fn run<'a>(sub_match: &ArgMatches<'a>) {
.parse::<u32>()
.expect("invalid number of validators");

let chain = sub_match
.value_of("chain")
.expect("missing chain identifier");
let chain = sub_match.value_of("chain");

let chain_spec_file = match chain {
Some(_) => None,
None => Some(
sub_match
.value_of("testnet_config")
.expect("must pass testnet config path if not using chain")
.to_string(),
),
};

let chain = if chain_spec_file.is_some() && chain.is_some() {
panic!("should only pass one of testnet_config or chain")
} else if chain_spec_file.is_some() {
// Placeholder value
""
} else {
chain.unwrap()
};

let keystore_password = sub_match.value_of("keystore_password");

Expand All @@ -133,7 +157,7 @@ pub fn run<'a>(sub_match: &ArgMatches<'a>) {
withdrawal_credentials,
32_000_000_000,
"2.3.0".to_string(),
None,
chain_spec_file,
)
.unwrap()
.try_into()
Expand Down
36 changes: 30 additions & 6 deletions src/cli/new_mnemonic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ pub fn subcommand<'a, 'b>() -> App<'a, 'b> {
.arg(
Arg::with_name("chain")
.long("chain")
.required(true)
.required(false)
.takes_value(true)
.possible_values(&["goerli", "prater", "mainnet", "minimal", "holesky"])
.possible_values(&["goerli", "prater", "mainnet", "holesky"])
.help(
r#"The name of Ethereum PoS chain you are
targeting. Use "mainnet" if you are
Expand Down Expand Up @@ -66,6 +66,13 @@ pub fn subcommand<'a, 'b>() -> App<'a, 'b> {
and consequently slower performance vs `pbkdf2` achieving better performance with lower security parameters compared to `scrypt`",
),
)
.arg(
Arg::with_name("testnet_config")
.long("testnet_config")
.required(false)
.takes_value(true)
.help("Path to a custom Eth PoS chain config")
)
}

#[allow(clippy::needless_lifetimes)]
Expand All @@ -78,9 +85,26 @@ pub fn run<'a>(sub_match: &ArgMatches<'a>) {
.parse::<u32>()
.expect("invalid number of validators");

let chain = sub_match
.value_of("chain")
.expect("missing chain identifier");
let chain = sub_match.value_of("chain");

let chain_spec_file = match chain {
Some(_) => None,
None => Some(
sub_match
.value_of("testnet_config")
.expect("must pass testnet config path if not using chain")
.to_string(),
),
};

let chain = if chain_spec_file.is_some() && chain.is_some() {
panic!("should only pass one of testnet_config or chain")
} else if chain_spec_file.is_some() {
// Placeholder value
""
} else {
chain.unwrap()
};

let keystore_password = sub_match.value_of("keystore_password");

Expand All @@ -102,7 +126,7 @@ pub fn run<'a>(sub_match: &ArgMatches<'a>) {
withdrawal_credentials,
32_000_000_000,
"2.3.0".to_string(),
None,
chain_spec_file,
)
.unwrap()
.try_into()
Expand Down
85 changes: 48 additions & 37 deletions src/deposit.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use eth2_keystore::keypair_from_secret;
use eth2_network_config::Eth2NetworkConfig;
use std::path::Path;
use types::{ChainSpec, Config, DepositData, Hash256, MainnetEthSpec, MinimalEthSpec, Signature};
use types::{ChainSpec, DepositData, Hash256, Signature};

use crate::key_material::VotingKeyMaterial;
use crate::{
chain_spec::{chain_spec_for_network, chain_spec_from_file},
key_material::VotingKeyMaterial,
};

#[derive(Debug, Eq, PartialEq)]
pub enum DepositError {
Expand Down Expand Up @@ -42,36 +43,12 @@ pub(crate) fn keystore_to_deposit(
};

let network_str = network.as_str();
let spec;

if ["goerli", "prater", "mainnet", "holesky"].contains(&network_str) {
spec = Eth2NetworkConfig::constant(network_str)
.unwrap()
.unwrap()
.chain_spec::<MainnetEthSpec>()
.unwrap();
} else if network_str == "minimal" {
if chain_spec_file.is_none() {
return Err(DepositError::NoCustomConfig(
"Custom config for minimal network must be provided".to_string(),
));
}
spec = match Config::from_file(Path::new(chain_spec_file.unwrap().as_str())) {
Ok(cfg) => cfg
.apply_to_chain_spec::<MinimalEthSpec>(&ChainSpec::minimal())
.unwrap(),
Err(e) => {
log::debug!("Unable to load chain spec config: {:?}", e);
return Err(DepositError::NoCustomConfig(
"Can not parse config file for minimal network".to_string(),
));
}
}
let spec = if network_str.is_empty() {
// Empty network name means custom config file is used
chain_spec_from_file(chain_spec_file.unwrap())?
} else {
return Err(DepositError::InvalidNetworkName(
"Unknown network name passed".to_string(),
));
}
chain_spec_for_network(network_str.to_string())?
};

let credentials_hash = Hash256::from_slice(withdrawal_credentials);

Expand Down Expand Up @@ -100,7 +77,6 @@ pub(crate) fn keystore_to_deposit(
mod test {

use eth2_keystore::{Keystore, PlainText};
use hex;
use pretty_assertions::assert_eq;
use types::PublicKey;

Expand Down Expand Up @@ -323,7 +299,7 @@ mod test {
}

#[test]
fn test_deposit_minimal() {
fn test_deposit_custom_testnet() {
let keystore = Keystore::from_json_str(KEYSTORE).unwrap();
let keypair = keystore.decrypt_keypair(PASSWORD).unwrap();
let mut manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
Expand All @@ -339,8 +315,43 @@ mod test {
&key_material,
&withdrawal_creds.as_slice(),
32_000_000_000,
"minimal".to_string(),
Some(manifest.to_str().unwrap().to_string()),
"".to_string(),
Some(manifest.to_str().unwrap().to_owned()),
)
.unwrap();

// Signature asserted here is generated with
// python ./staking_deposit/deposit.py existing-mnemonic --keystore_password test

// Please enter your mnemonic separated by spaces (" "): entire habit bottom mention spoil clown finger wheat motion fox axis mechanic country make garment bar blind stadium sugar water scissors canyon often ketchup
// Enter the index (key number) you wish to start generating more keys from. For example, if you've generated 4 keys in the past, you'd enter 4 here. [0]: 0
// Please choose how many new validators you wish to run: 1
// Please choose the (mainnet or testnet) network/chain name ['mainnet', 'prater', 'kintsugi', 'kiln', 'minimal']: [mainnet]: minimal
assert_eq!(
"974ab2ce579f12339bb3125311a4d69f50e8d40546d086629f8f9af31ef7ee1fd0400b40d1884f7f0487b17069054b6305eb8321e883a5110e7e67864da44e4e15efbed5ff752a1a93bada4f53cdd033d8884233925546e28b16991d44307d8d",
deposit_data.signature.to_string().as_str().strip_prefix("0x").unwrap()
);
}

#[test]
fn test_deposit_custom_minimal_testnet() {
let keystore = Keystore::from_json_str(KEYSTORE).unwrap();
let keypair = keystore.decrypt_keypair(PASSWORD).unwrap();
let mut manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest.push("tests/resources/minimal.yaml");
let key_material = VotingKeyMaterial {
keystore: Some(keystore.clone()),
keypair: keypair.clone(),
voting_secret: PlainText::from(keypair.sk.serialize().as_bytes().to_vec()),
withdrawal_keypair: None,
};
let withdrawal_creds = hex::decode(WITHDRAWAL_CREDENTIALS_ETH2).unwrap();
let (deposit_data, _) = keystore_to_deposit(
&key_material,
&withdrawal_creds.as_slice(),
32_000_000_000,
"".to_string(),
Some(manifest.to_str().unwrap().to_owned()),
)
.unwrap();

Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod bls_to_execution_change;
pub mod chain_spec;
pub mod cli;
pub(crate) mod deposit;
pub(crate) mod key_material;
Expand Down
21 changes: 7 additions & 14 deletions src/validators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ use crate::seed::get_eth2_seed;
use crate::utils::get_withdrawal_credentials;
use bip39::{Mnemonic, Seed as Bip39Seed};
use eth2_keystore::Keystore;
use eth2_network_config::Eth2NetworkConfig;
use regex::Regex;
use serde::{Deserialize, Serialize};
use tree_hash::TreeHash;
use types::{
DepositData, Hash256, Keypair, MainnetEthSpec, PublicKey, PublicKeyBytes, Signature,
SignatureBytes, SignedRoot,
ChainSpec, DepositData, Hash256, Keypair, PublicKey, PublicKeyBytes, Signature, SignatureBytes,
SignedRoot,
};

const ETH1_CREDENTIALS_PREFIX: &[u8] = &[
Expand Down Expand Up @@ -48,7 +47,7 @@ impl DepositExport {
Checks whether a deposit is valid based on the staking deposit rules.
https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#deposits
*/
pub fn validate(self) {
pub fn validate(self, spec: ChainSpec) {
let pub_key = &self.pubkey;
assert_eq!(96, pub_key.len());

Expand Down Expand Up @@ -87,16 +86,10 @@ impl DepositExport {
signature: signature_bytes,
};

let fork_version: [u8; 4] = self.fork_version.as_bytes()[4..8]
.try_into()
.expect("could not wrap fork version");
assert_eq!(vec![49, 48, 50, 48], fork_version); // should be 1020

let spec = Eth2NetworkConfig::constant(&self.network_name)
.unwrap()
.unwrap()
.chain_spec::<MainnetEthSpec>()
.unwrap();
let fork_version = hex::decode(self.fork_version).expect("could not wrap fork version");
let fork_version_from_chain = spec.genesis_fork_version.to_vec();
assert_eq!(fork_version, fork_version_from_chain); // should match the spec
assert_eq!(fork_version.len(), 4);

let domain = spec.get_deposit_domain();
let signing_root = deposit_data.as_deposit_message().signing_root(domain);
Expand Down
Loading

0 comments on commit 71b3b8d

Please sign in to comment.