Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support 4 custom testnet config in mnemonic cmds #45

Merged
merged 2 commits into from
Jul 16, 2024
Merged
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
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)
jenpaff marked this conversation as resolved.
Show resolved Hide resolved
.takes_value(true)
.possible_values(&["goerli", "prater", "mainnet", "minimal", "holesky"])
.possible_values(&["goerli", "prater", "mainnet", "holesky"])
jenpaff marked this conversation as resolved.
Show resolved Hide resolved
.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() {
jenpaff marked this conversation as resolved.
Show resolved Hide resolved
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
87 changes: 49 additions & 38 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,11 +299,46 @@ 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"));
manifest.push("tests/resources/helder-testnet.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();

// 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/testnet.yaml");
manifest.push("tests/resources/minimal.yaml");
let key_material = VotingKeyMaterial {
keystore: Some(keystore.clone()),
keypair: keypair.clone(),
Expand All @@ -339,8 +350,8 @@ 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();

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
Loading