From 6fb4dce7ca4a76ca3c4254c650dbed4071a3c9b9 Mon Sep 17 00:00:00 2001 From: Maksym Date: Mon, 20 Feb 2023 01:18:03 +0200 Subject: [PATCH] Add Gnosis chain deposit support --- src/cli/existing_mnemonic.rs | 2 +- src/cli/new_mnemonic.rs | 2 +- src/deposit.rs | 84 +++++++++++++++++++++++++----------- src/lib.rs | 1 + src/networks.rs | 77 +++++++++++++++++++++++++++++++++ src/validators.rs | 7 ++- 6 files changed, 144 insertions(+), 29 deletions(-) create mode 100644 src/networks.rs diff --git a/src/cli/existing_mnemonic.rs b/src/cli/existing_mnemonic.rs index def2ed4..90087d7 100644 --- a/src/cli/existing_mnemonic.rs +++ b/src/cli/existing_mnemonic.rs @@ -34,7 +34,7 @@ pub fn subcommand<'a, 'b>() -> App<'a, 'b> { .long("chain") .required(true) .takes_value(true) - .possible_values(&["goerli", "prater", "mainnet", "minimal"]) + .possible_values(&crate::networks::SUPPORTED_NETWORKS) .help( r#"The name of Ethereum PoS chain you are targeting. Use "mainnet" if you are diff --git a/src/cli/new_mnemonic.rs b/src/cli/new_mnemonic.rs index 637fc49..8a50a61 100644 --- a/src/cli/new_mnemonic.rs +++ b/src/cli/new_mnemonic.rs @@ -21,7 +21,7 @@ pub fn subcommand<'a, 'b>() -> App<'a, 'b> { .long("chain") .required(true) .takes_value(true) - .possible_values(&["goerli", "prater", "mainnet", "minimal"]) + .possible_values(&crate::networks::SUPPORTED_NETWORKS) .help( r#"The name of Ethereum PoS chain you are targeting. Use "mainnet" if you are diff --git a/src/deposit.rs b/src/deposit.rs index cb26bf7..ecce028 100644 --- a/src/deposit.rs +++ b/src/deposit.rs @@ -1,9 +1,8 @@ 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, Config, DepositData, Hash256, MinimalEthSpec, Signature}; -use crate::key_material::VotingKeyMaterial; +use crate::{key_material::VotingKeyMaterial, networks::NetworkSpec}; #[derive(Debug, Eq, PartialEq)] pub enum DepositError { @@ -23,7 +22,7 @@ pub(crate) fn keystore_to_deposit( // withdrawal credentials withdrawal_credentials: &[u8], deposit_amount_gwei: u64, - network: String, + network: NetworkSpec, chain_spec_file: Option, ) -> Result<(DepositData, ChainSpec), DepositError> { // Validate data input @@ -41,22 +40,14 @@ pub(crate) fn keystore_to_deposit( )); }; - let network_str = network.as_str(); - let spec; - - if ["goerli", "prater", "mainnet"].contains(&network_str) { - spec = Eth2NetworkConfig::constant(network_str) - .unwrap() - .unwrap() - .chain_spec::() - .unwrap(); - } else if network_str == "minimal" { + let spec = if !network.is_public() { + // Loads custom configuration for private net 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())) { + match Config::from_file(Path::new(chain_spec_file.unwrap().as_str())) { Ok(cfg) => cfg .apply_to_chain_spec::(&ChainSpec::minimal()) .unwrap(), @@ -68,10 +59,8 @@ pub(crate) fn keystore_to_deposit( } } } else { - return Err(DepositError::InvalidNetworkName( - "Unknown network name passed".to_string(), - )); - } + network.into() + }; let credentials_hash = Hash256::from_slice(withdrawal_credentials); @@ -105,7 +94,9 @@ mod test { use types::PublicKey; use super::keystore_to_deposit; - use crate::{key_material::VotingKeyMaterial, utils::get_withdrawal_credentials}; + use crate::{ + key_material::VotingKeyMaterial, networks::NetworkSpec, utils::get_withdrawal_credentials, + }; use std::{path::PathBuf, str::FromStr}; use test_log::test; @@ -132,7 +123,7 @@ mod test { &key_material, withdrawal_creds.as_slice(), 32_000_000_000, - "mainnet".to_string(), + NetworkSpec::Mainnet, None, ) .unwrap(); @@ -172,7 +163,7 @@ mod test { &key_material, withdrawal_creds.as_slice(), 32_000_000_000, - "mainnet".to_string(), + NetworkSpec::Mainnet, None, ) .unwrap(); @@ -212,7 +203,7 @@ mod test { &key_material, &withdrawal_creds, 32_000_000_000, - "mainnet".to_string(), + NetworkSpec::Mainnet, None, ) .unwrap(); @@ -259,7 +250,7 @@ mod test { &key_material, &withdrawal_creds.as_slice(), 32_000_000_000, - "goerli".to_string(), + NetworkSpec::Goerli, None, ) .unwrap(); @@ -294,7 +285,7 @@ mod test { &key_material, &withdrawal_creds.as_slice(), 32_000_000_000, - "minimal".to_string(), + NetworkSpec::Minimal, Some(manifest.to_str().unwrap().to_string()), ) .unwrap(); @@ -311,4 +302,47 @@ mod test { deposit_data.signature.to_string().as_str().strip_prefix("0x").unwrap() ); } + + #[test] + fn test_deposit_gnosis() { + let keystore = Keystore::from_json_str(KEYSTORE).unwrap(); + let keypair = keystore.decrypt_keypair(PASSWORD).unwrap(); + let key_material = VotingKeyMaterial { + keystore: Some(keystore.clone()), + keypair, + voting_secret: PlainText::from( + keystore + .decrypt_keypair(PASSWORD) + .unwrap() + .sk + .serialize() + .as_bytes() + .to_vec(), + ), + withdrawal_keypair: None, + }; + let withdrawal_creds = hex::decode(WITHDRAWAL_CREDENTIALS_ETH1).unwrap(); + let (deposit_data, _) = keystore_to_deposit( + &key_material, + &withdrawal_creds.as_slice(), + 32_000_000_000, + NetworkSpec::Gnosis, + None, + ) + .unwrap(); + + // Signature asserted here is generated with + // https://github.com/gnosischain/validator-data-generator + + // python ./staking_deposit/deposit.py existing-mnemonic --eth1_withdrawal_address=0x010000000000000000000000000000000000000001 + + // 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', 'ropsten', 'goerli', 'kiln', 'sepolia', 'gnosis', 'chiado']: [gnosis]: gnosis + assert_eq!( + "97cb6902975fc7cdcd685006ca972a22799aece787b9665af9e0b501e70a4db100cf280c99f1b20ea49c953b526b0e760b4a6d6a8d1092a6afbad3efdab8269384f2034c4fd97ebd8fc2101467b2fe6aeadcf5fcfaa11952be8d3939a55b10f7", + deposit_data.signature.to_string().as_str().strip_prefix("0x").unwrap() + ); + } } diff --git a/src/lib.rs b/src/lib.rs index 1957249..ea4920c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod bls_to_execution_change; pub mod cli; pub(crate) mod deposit; pub(crate) mod key_material; +pub mod networks; pub(crate) mod seed; pub(crate) mod utils; pub mod validators; diff --git a/src/networks.rs b/src/networks.rs new file mode 100644 index 0000000..565aef2 --- /dev/null +++ b/src/networks.rs @@ -0,0 +1,77 @@ +use eth2_network_config::Eth2NetworkConfig; +use types::{ChainSpec, GnosisEthSpec, MainnetEthSpec}; + +use crate::DepositError; + +/// String representation for all supported networks +pub static SUPPORTED_NETWORKS: [&str; 5] = ["goerli", "prater", "mainnet", "gnosis", "minimal"]; + +#[derive(Clone)] +pub enum NetworkSpec { + Mainnet, + Goerli, + Gnosis, + Minimal, +} + +/// Collects network spec from user or library inputs +impl TryFrom for NetworkSpec { + type Error = DepositError; + + fn try_from(value: String) -> Result { + let network = match value.as_str() { + "mainnet" => NetworkSpec::Mainnet, + "goerli" => NetworkSpec::Goerli, + "prater" => NetworkSpec::Goerli, + "gnosis" => NetworkSpec::Gnosis, + "minimal" => NetworkSpec::Minimal, + _ => { + log::info!("Invalid network name passed: {value}"); + return Err(DepositError::InvalidNetworkName( + "Unknown network name passed".to_string(), + )); + } + }; + Ok(network) + } +} + +/// Convert to a string that is recognizeable by Lighthouse's +/// Eth2NetworkConfig::constant +impl From for &'static str { + fn from(value: NetworkSpec) -> &'static str { + match value { + NetworkSpec::Mainnet => "mainnet", + NetworkSpec::Goerli => "goerli", + NetworkSpec::Gnosis => "gnosis", + NetworkSpec::Minimal => "minimal", + } + } +} + +/// Convert to Lighthouse internal spec repr. +/// Works only for public networks, private net needs +/// to load file. +impl From for ChainSpec { + fn from(value: NetworkSpec) -> ChainSpec { + match value { + NetworkSpec::Gnosis => Eth2NetworkConfig::constant(value.into()) + .unwrap() + .unwrap() + .chain_spec::() + .unwrap(), + _ => Eth2NetworkConfig::constant(value.into()) + .unwrap() + .unwrap() + .chain_spec::() + .unwrap(), + } + } +} + +impl NetworkSpec { + /// Find out if the config for public network, or private net one + pub fn is_public(&self) -> bool { + !matches!(self, NetworkSpec::Minimal) + } +} diff --git a/src/validators.rs b/src/validators.rs index 036e2b0..68d25f5 100644 --- a/src/validators.rs +++ b/src/validators.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use crate::deposit::{keystore_to_deposit, DepositError}; use crate::key_material::{seed_to_key_material, VotingKeyMaterial}; +use crate::networks::NetworkSpec; use crate::seed::get_eth2_seed; use crate::utils::get_withdrawal_credentials; use bip39::{Mnemonic, Seed as Bip39Seed}; @@ -243,6 +244,8 @@ impl Validators { let mut private_keys: Vec = vec![]; let mut deposit_data: Vec = vec![]; + let network_spec = NetworkSpec::try_from(network)?; + for key_with_store in self.key_material.iter() { if let Some(ks) = key_with_store.keystore.clone() { keystores.push(ks); @@ -260,7 +263,7 @@ impl Validators { &(*key_with_store).clone(), withdrawal_credentials.as_ref(), deposit_amount_gwei, - network.clone(), + network_spec.clone(), chain_spec_file.clone(), )?; @@ -278,7 +281,7 @@ impl Validators { deposit_message_root: hex::encode(deposit.as_deposit_message().tree_hash_root()), deposit_data_root: hex::encode(deposit.tree_hash_root()), fork_version: hex::encode(chain_spec.genesis_fork_version), - network_name: network.clone(), + network_name: Into::<&'static str>::into(network_spec.clone()).to_string(), deposit_cli_version: deposit_cli_version.clone(), }) }