diff --git a/README.md b/README.md index f3a13e5..9ad3b8e 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,36 @@ You can use `eth-staking-smith` as follows to convert your address: Note that --validator-index and --validator-start-index are two distinct parameter, the former being index of validator on Beacon chain, and the latter is the index of validator private key derived from the seed + +### Command to send SignedBLSToExecutionChange request to Beacon node + +``` +curl -H "Content-Type: application/json" -d '{ + "message": { + "validator_index": 100, + "from_bls_pubkey": "0x0045b91b2f60b88e7392d49ae1364b55e713d06f30e563f9f99e10994b26221d", + "to_execution_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F" + }, + "signature": "0x9220e5badefdfe8abc36cae01af29b981edeb940ff88c438f72c8af876fbd6416138c85f5348c5ace92a081fa15291aa0ffb856141b871dc807f3ec2fe9c8415cac3d76579c61455ab3938bc162e139d060c8aa13fcd670febe46bf0bb579c5a" +}' http://localhost:3500/eth/v1/beacon/pool/bls_to_execution_change +``` + +## Generating pre-signed exit message + +It is possible to create pre-signed voluntary exit message for every validator that +is generated from some known mnemonic, given the minimum epoch for exit to trigger. + +Use `eth-staking-smith` via command line like: + +### Command to generate presigned exit message + +``` +./target/debug/eth-staking-smith presigned-exit-message --chain mainnet --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" --validator_seed_index 0 --validator_beacon_index 100 --epoch 300000 +``` + +Note that --validator-beacon-index and --validator-seed-index are two distinct parameter, the former being index of validator on Beacon chain, and the latter is the index of validator private key derived from the seed + + ## Exporting CLI standard output into common keystores folder format Most validator clients recognize the keystore folder format, @@ -117,18 +147,6 @@ lighthouse account validator import \ --directory validator_keys/ --password-file ./password.txt ``` -### Command to send SignedBLSToExecutionChange request to Beacon node - -``` -curl -H "Content-Type: application/json" -d '{ - "message": { - "validator_index": 100, - "from_bls_pubkey": "0x0045b91b2f60b88e7392d49ae1364b55e713d06f30e563f9f99e10994b26221d", - "to_execution_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F" - }, - "signature": "0x9220e5badefdfe8abc36cae01af29b981edeb940ff88c438f72c8af876fbd6416138c85f5348c5ace92a081fa15291aa0ffb856141b871dc807f3ec2fe9c8415cac3d76579c61455ab3938bc162e139d060c8aa13fcd670febe46bf0bb579c5a" -}' http://localhost:3500/eth/v1/beacon/pool/bls_to_execution_change -``` # Implementation Details To avoid heavy lifting, we're interfacing [Lighthouse account manager](https://github.com/sigp/lighthouse/blob/stable/account_manager), but optimizing it in a way so all operations are done in memory and key material is never written to filesystem during the generation to cater for our use case. diff --git a/src/bls_to_execution_change/constants.rs b/src/bls_to_execution_change/constants.rs deleted file mode 100644 index 0d8c2ef..0000000 --- a/src/bls_to_execution_change/constants.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::collections::HashMap; - -use lazy_static::lazy_static; -use types::Hash256; - -use crate::networks::SupportedNetworks; - -fn decode_genesis_validators_root(hex_value: &str) -> Hash256 { - Hash256::from_slice(hex::decode(hex_value).unwrap().as_slice()) -} - -// Genesis validators root values are not present in chain spec, -// but instead acquired from genesis. The values below are well-known -// and taken from repositories in https://github.com/eth-clients organization. -lazy_static! { - pub static ref GENESIS_VALIDATORS_ROOT_MAINNET: Hash256 = decode_genesis_validators_root( - "4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95" - ); - pub static ref GENESIS_VALIDATORS_ROOT_HOLESKY: Hash256 = decode_genesis_validators_root( - "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1" - ); - pub static ref GENESIS_VALIDATORS_ROOT_GOERLI: Hash256 = decode_genesis_validators_root( - "043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb" - ); - pub static ref GENESIS_VALIDATOR_ROOT: HashMap = HashMap::from([ - ( - SupportedNetworks::Mainnet, - GENESIS_VALIDATORS_ROOT_MAINNET.to_owned() - ), - ( - SupportedNetworks::Prater, - GENESIS_VALIDATORS_ROOT_GOERLI.to_owned() - ), - ( - SupportedNetworks::Goerli, - GENESIS_VALIDATORS_ROOT_GOERLI.to_owned() - ), - ( - SupportedNetworks::Holesky, - GENESIS_VALIDATORS_ROOT_HOLESKY.to_owned() - ), - ]); -} - -pub(crate) fn validators_root_for(network: &SupportedNetworks) -> Hash256 { - *GENESIS_VALIDATOR_ROOT.get(network).unwrap() -} diff --git a/src/bls_to_execution_change/mod.rs b/src/bls_to_execution_change/mod.rs index c17733e..eb06438 100644 --- a/src/bls_to_execution_change/mod.rs +++ b/src/bls_to_execution_change/mod.rs @@ -1,4 +1,3 @@ -pub mod constants; pub(crate) mod operator; use regex::Regex; diff --git a/src/bls_to_execution_change/operator.rs b/src/bls_to_execution_change/operator.rs index 9fa3005..b4b49ba 100644 --- a/src/bls_to_execution_change/operator.rs +++ b/src/bls_to_execution_change/operator.rs @@ -72,6 +72,8 @@ impl SignedBlsToExecutionChangeOperator for SignedBlsToExecutionChange { to_execution_address: self.message.to_execution_address, }; let signing_root = bls_to_execution_change.signing_root(domain); - self.signature.verify(&withdrawal_pubkey, signing_root); + if !self.signature.verify(&withdrawal_pubkey, signing_root) { + panic!("Invalid bls to execution change signature") + } } } diff --git a/src/bls_to_execution_change/test.rs b/src/bls_to_execution_change/test.rs index cec6271..66c0064 100644 --- a/src/bls_to_execution_change/test.rs +++ b/src/bls_to_execution_change/test.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use types::{Hash256, PublicKey}; -use crate::{bls_to_execution_change::constants::validators_root_for, chain_spec, utils}; +use crate::{chain_spec, networks::validators_root_for, utils}; const EXECUTION_WITHDRAWAL_ADDRESS: &str = "0x71C7656EC7ab88b098defB751B7401B5f6d8976F"; const PHRASE: &str = "entire habit bottom mention spoil clown finger wheat motion fox axis mechanic country make garment bar blind stadium sugar water scissors canyon often ketchup"; diff --git a/src/chain_spec.rs b/src/chain_spec.rs index 9a58b78..1d60e57 100644 --- a/src/chain_spec.rs +++ b/src/chain_spec.rs @@ -1,7 +1,7 @@ use std::path::Path; use eth2_network_config::Eth2NetworkConfig; -use types::{ChainSpec, Config, MainnetEthSpec, MinimalEthSpec}; +use types::{ChainSpec, Config, Hash256, MainnetEthSpec, MinimalEthSpec}; use crate::{networks::SupportedNetworks, DepositError}; @@ -40,3 +40,30 @@ pub fn chain_spec_from_file(chain_spec_file: String) -> Result, + testnet_properties: Option<(String, String)>, +) -> (Hash256, ChainSpec) { + if chain.is_some() { + let well_known_chain = chain.unwrap(); + ( + crate::networks::validators_root_for(&well_known_chain), + chain_spec_for_network(&well_known_chain).expect("Invalid chain spec"), + ) + } else { + let (genesis_validators_root_str, testnet_config_path) = testnet_properties.expect( + "If custom testnet config is passed, genesis validators root value must be included", + ); + let genesis_validators_root_bytes = hex::decode( + genesis_validators_root_str + .strip_prefix("0x") + .unwrap_or(&genesis_validators_root_str), + ) + .expect("Invalid custom genesis validators root"); + ( + Hash256::from_slice(genesis_validators_root_bytes.as_slice()), + chain_spec_from_file(testnet_config_path).expect("Invalid chain spec in file"), + ) + } +} diff --git a/src/cli/bls_to_execution_change.rs b/src/cli/bls_to_execution_change.rs index 97cb62c..54b9d87 100644 --- a/src/cli/bls_to_execution_change.rs +++ b/src/cli/bls_to_execution_change.rs @@ -1,7 +1,7 @@ +use crate::bls_to_execution_change; use crate::bls_to_execution_change::operator::SignedBlsToExecutionChangeOperator; -use crate::{bls_to_execution_change, chain_spec}; +use crate::chain_spec::validators_root_and_spec; use clap::{arg, Parser}; -use types::Hash256; #[derive(Clone, Parser)] pub struct BlsToExecutionChangeSubcommandOpts { @@ -64,26 +64,21 @@ impl BlsToExecutionChangeSubcommandOpts { self.chain.clone() }; - let (genesis_validator_root, spec) = if chain.is_some() { - let well_known_chain = chain.unwrap(); - ( - bls_to_execution_change::constants::validators_root_for(&well_known_chain), - chain_spec::chain_spec_for_network(&well_known_chain).expect("Invalid chain spec"), - ) - } else { - let genesis_validators_root_str = self.genesis_validators_root.clone().expect("If custom testnet config is passed, genesis validators root value must be included"); - let genesis_validators_root_bytes = hex::decode( - genesis_validators_root_str - .strip_prefix("0x") - .unwrap_or(&genesis_validators_root_str), - ) - .expect("Invalid custom genesis validators root"); - ( - Hash256::from_slice(genesis_validators_root_bytes.as_slice()), - chain_spec::chain_spec_from_file(self.testnet_config.clone().unwrap()) - .expect("Invalid chain spec in file"), - ) - }; + let (genesis_validators_root, spec) = validators_root_and_spec( + chain.clone(), + if chain.is_some() { + None + } else { + Some(( + self.genesis_validators_root + .clone() + .expect("Genesis validators root parameter must be set"), + self.testnet_config + .clone() + .expect("Testnet config must be set"), + )) + }, + ); let (bls_to_execution_change, keypair) = bls_to_execution_change::bls_execution_change_from_mnemonic( @@ -95,7 +90,7 @@ impl BlsToExecutionChangeSubcommandOpts { let signed_bls_to_execution_change = bls_to_execution_change.sign( &keypair.withdrawal_keypair.unwrap().sk, - genesis_validator_root, + genesis_validators_root, &spec, ); @@ -103,7 +98,7 @@ impl BlsToExecutionChangeSubcommandOpts { self.bls_withdrawal_credentials.as_str(), self.execution_address.as_str(), &spec, - &genesis_validator_root, + &genesis_validators_root, ); let export = signed_bls_to_execution_change.export(); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cec4916..215b4c5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,3 +1,4 @@ pub mod bls_to_execution_change; pub mod existing_mnemonic; pub mod new_mnemonic; +pub mod presigned_exit_message; diff --git a/src/cli/presigned_exit_message.rs b/src/cli/presigned_exit_message.rs new file mode 100644 index 0000000..275285c --- /dev/null +++ b/src/cli/presigned_exit_message.rs @@ -0,0 +1,100 @@ +use clap::{arg, Parser}; + +use crate::voluntary_exit::operator::SignedVoluntaryExitOperator; +use crate::{chain_spec::validators_root_and_spec, voluntary_exit}; + +#[derive(Clone, Parser)] +pub struct PresignedExitMessageSubcommandOpts { + /// The mnemonic that you used to generate your + /// keys. + /// + /// It is recommended not to use this + /// argument, and wait for the CLI to ask you + /// for your mnemonic as otherwise it will + /// appear in your shell history. + #[arg(long)] + pub mnemonic: String, + + /// The name of Ethereum PoS chain you are targeting. + /// + /// Use "mainnet" if you are + /// depositing ETH + #[arg(value_enum, long)] + pub chain: Option, + + /// The index of the first validator's keys you wish to generate the address for + /// e.g. if you generated 3 keys before (index #0, index #1, index #2) + /// and you want to generate for the 2nd validator, + /// the validator_start_index would be 1. + /// If no index specified, it will be set to 0. + #[arg(long, visible_alias = "validator_seed_index")] + pub validator_seed_index: u32, + + /// On-chain beacon index of the validator. + #[arg(long, visible_alias = "validator_beacon_index")] + pub validator_beacon_index: u32, + + /// Epoch number which must be included in the presigned exit message. + #[arg(long, visible_alias = "execution_address")] + pub epoch: u64, + + /// Path to a custom Eth PoS chain config + #[arg(long, visible_alias = "testnet_config")] + pub testnet_config: Option, + + /// Custom genesis validators root for the custom testnet, passed as hex string. + /// See https://eth2book.info/capella/part3/containers/state/ for value + /// description + #[arg(long, visible_alias = "genesis_validators_root")] + pub genesis_validators_root: Option, +} + +impl PresignedExitMessageSubcommandOpts { + pub fn run(&self) { + let chain = if self.chain.is_some() && self.testnet_config.is_some() { + panic!("should only pass one of testnet_config or chain") + } else if self.testnet_config.is_some() { + // Signalizes custom testnet config will be used + None + } else { + self.chain.clone() + }; + + let (genesis_validators_root, spec) = validators_root_and_spec( + chain.clone(), + if chain.is_some() { + None + } else { + Some(( + self.genesis_validators_root + .clone() + .expect("Genesis validators root parameter must be set"), + self.testnet_config + .clone() + .expect("Testnet config must be set"), + )) + }, + ); + + let (voluntary_exit, key_material) = voluntary_exit::voluntary_exit_message_from_mnemonic( + self.mnemonic.as_bytes(), + self.validator_seed_index as u64, + self.validator_beacon_index as u64, + self.epoch, + ); + + let signed_voluntary_exit = + voluntary_exit.sign(&key_material.keypair.sk, genesis_validators_root, &spec); + + signed_voluntary_exit.clone().validate( + &key_material.keypair.pk, + &spec, + &genesis_validators_root, + ); + let export = signed_voluntary_exit.export(); + + let presigned_exit_message_json = + serde_json::to_string_pretty(&export).expect("could not parse validator export"); + println!("{}", presigned_exit_message_json); + } +} diff --git a/src/lib.rs b/src/lib.rs index 72f5338..54e0a61 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ pub mod networks; pub(crate) mod seed; pub mod utils; pub mod validators; +pub mod voluntary_exit; pub use deposit::DepositError; pub use validators::*; diff --git a/src/main.rs b/src/main.rs index 7a6d290..185b7f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ #![forbid(unsafe_code)] use clap::{Parser, Subcommand}; -use eth_staking_smith::cli::{bls_to_execution_change, existing_mnemonic, new_mnemonic}; +use eth_staking_smith::cli::{ + bls_to_execution_change, existing_mnemonic, new_mnemonic, presigned_exit_message, +}; #[derive(Parser)] #[command(author, version, about, long_about = None)] @@ -18,6 +20,9 @@ enum SubCommands { ExistingMnemonic(existing_mnemonic::ExistingMnemonicSubcommandOpts), /// Generate new keys with new mnemonic. NewMnemonic(new_mnemonic::NewMnemonicSubcommandOpts), + /// Generate presigned exit message which can be sent + /// to the Beacon Node to start voluntary exit process for the validator + PresignedExitMessage(presigned_exit_message::PresignedExitMessageSubcommandOpts), } impl SubCommands { @@ -26,6 +31,7 @@ impl SubCommands { Self::BlsToExecutionChange(sub) => sub.run(), Self::ExistingMnemonic(sub) => sub.run(), Self::NewMnemonic(sub) => sub.run(), + Self::PresignedExitMessage(sub) => sub.run(), } } } diff --git a/src/networks.rs b/src/networks.rs index 89bed64..7925d37 100644 --- a/src/networks.rs +++ b/src/networks.rs @@ -1,3 +1,8 @@ +use std::collections::HashMap; + +use lazy_static::lazy_static; +use types::Hash256; + #[derive(clap::ValueEnum, Clone, Hash, Eq, PartialEq)] pub enum SupportedNetworks { Mainnet, @@ -18,3 +23,44 @@ impl std::fmt::Display for SupportedNetworks { write!(f, "{}", s) } } + +fn decode_genesis_validators_root(hex_value: &str) -> Hash256 { + Hash256::from_slice(hex::decode(hex_value).unwrap().as_slice()) +} + +// Genesis validators root values are not present in chain spec, +// but instead acquired from genesis. The values below are well-known +// and taken from repositories in https://github.com/eth-clients organization. +lazy_static! { + pub static ref GENESIS_VALIDATORS_ROOT_MAINNET: Hash256 = decode_genesis_validators_root( + "4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95" + ); + pub static ref GENESIS_VALIDATORS_ROOT_HOLESKY: Hash256 = decode_genesis_validators_root( + "9143aa7c615a7f7115e2b6aac319c03529df8242ae705fba9df39b79c59fa8b1" + ); + pub static ref GENESIS_VALIDATORS_ROOT_GOERLI: Hash256 = decode_genesis_validators_root( + "043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb" + ); + pub static ref GENESIS_VALIDATOR_ROOT: HashMap = HashMap::from([ + ( + SupportedNetworks::Mainnet, + GENESIS_VALIDATORS_ROOT_MAINNET.to_owned() + ), + ( + SupportedNetworks::Prater, + GENESIS_VALIDATORS_ROOT_GOERLI.to_owned() + ), + ( + SupportedNetworks::Goerli, + GENESIS_VALIDATORS_ROOT_GOERLI.to_owned() + ), + ( + SupportedNetworks::Holesky, + GENESIS_VALIDATORS_ROOT_HOLESKY.to_owned() + ), + ]); +} + +pub(crate) fn validators_root_for(network: &SupportedNetworks) -> Hash256 { + *GENESIS_VALIDATOR_ROOT.get(network).unwrap() +} diff --git a/src/voluntary_exit/mod.rs b/src/voluntary_exit/mod.rs new file mode 100644 index 0000000..fe871f3 --- /dev/null +++ b/src/voluntary_exit/mod.rs @@ -0,0 +1,37 @@ +pub(crate) mod operator; + +use types::{Epoch, VoluntaryExit}; + +use crate::key_material::VotingKeyMaterial; + +pub fn voluntary_exit_message_from_mnemonic( + mnemonic_phrase: &[u8], + validator_seed_index: u64, + validator_beacon_index: u64, + epoch: u64, +) -> (VoluntaryExit, VotingKeyMaterial) { + let (seed, _) = crate::seed::get_eth2_seed(Some(mnemonic_phrase)); + + let key_materials = crate::key_material::seed_to_key_material( + &seed, + 1, + validator_seed_index as u32, + None, + false, + None, + ); + + let key_material = key_materials + .first() + .expect("Error deriving key material from mnemonic"); + + let voluntary_exit = VoluntaryExit { + epoch: Epoch::from(epoch), + validator_index: validator_beacon_index, + }; + + (voluntary_exit, key_material.clone()) +} + +#[cfg(test)] +mod test; diff --git a/src/voluntary_exit/operator.rs b/src/voluntary_exit/operator.rs new file mode 100644 index 0000000..0721159 --- /dev/null +++ b/src/voluntary_exit/operator.rs @@ -0,0 +1,47 @@ +use tree_hash::Hash256; +use types::{ + ChainSpec, Domain, ForkName, PublicKey, SignedRoot, SignedVoluntaryExit, VoluntaryExit, +}; + +pub(crate) trait SignedVoluntaryExitOperator { + fn export(&self) -> serde_json::Value; + + fn validate(self, pubkey: &PublicKey, spec: &ChainSpec, genesis_validators_root: &Hash256); +} + +impl SignedVoluntaryExitOperator for SignedVoluntaryExit { + fn export(&self) -> serde_json::Value { + serde_json::json!({ + "message": { + "epoch": self.message.epoch.as_u64(), + "validator_index": self.message.validator_index, + }, + "signature": self.signature.to_string() + }) + } + + fn validate(self, pubkey: &PublicKey, spec: &ChainSpec, genesis_validators_root: &Hash256) { + let fork_name = spec.fork_name_at_epoch(self.message.epoch); + let fork_version = match fork_name { + ForkName::Base | ForkName::Altair | ForkName::Merge | ForkName::Capella => { + spec.fork_version_for_name(fork_name) + } + // EIP-7044 + ForkName::Deneb => spec.fork_version_for_name(ForkName::Capella), + }; + let domain = spec.compute_domain( + Domain::VoluntaryExit, + fork_version, + *genesis_validators_root, + ); + + let voluntary_exit: VoluntaryExit = VoluntaryExit { + validator_index: self.message.validator_index, + epoch: self.message.epoch, + }; + let signing_root = voluntary_exit.signing_root(domain); + if !self.signature.verify(pubkey, signing_root) { + panic!("Invalid voluntary exit signature") + } + } +} diff --git a/src/voluntary_exit/test.rs b/src/voluntary_exit/test.rs new file mode 100644 index 0000000..c0bcb00 --- /dev/null +++ b/src/voluntary_exit/test.rs @@ -0,0 +1,24 @@ +use crate::{chain_spec::validators_root_and_spec, networks::SupportedNetworks}; + +const PHRASE: &str = "entire habit bottom mention spoil clown finger wheat motion fox axis mechanic country make garment bar blind stadium sugar water scissors canyon often ketchup"; + +#[test] +fn it_generates_presigned_exit_message() { + let (genesis_validators_root, spec) = + validators_root_and_spec(Some(SupportedNetworks::Holesky), None); + + let (voluntary_exit, key_material) = + crate::voluntary_exit::voluntary_exit_message_from_mnemonic( + PHRASE.as_bytes(), + 0, + 100, + 73682, + ); + + let signed_voluntary_exit = + voluntary_exit.sign(&key_material.keypair.sk, genesis_validators_root, &spec); + + assert_eq!(100, signed_voluntary_exit.message.validator_index); + assert_eq!(73682, signed_voluntary_exit.message.epoch.as_u64()); + assert_eq!("0xa418543c8bdc266e00a45d7409386fffe02ad9ce3c1e707d562b38f7160966567602c518f6615edc8ecd964b978837f30a742c535dafc33137833b3aa6c30f4fecf37449de405ed50d8e436f21c6f58c0a48407037b1985507c3221ce53ba213", signed_voluntary_exit.signature.to_string()); +} diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs index c29808b..49c56c2 100644 --- a/tests/e2e/mod.rs +++ b/tests/e2e/mod.rs @@ -1,6 +1,7 @@ mod bls_to_execution_change; mod existing_mnemonic; mod new_mnemonic; +mod presigned_exit_message; use serde::{Deserialize, Serialize}; diff --git a/tests/e2e/presigned_exit_message.rs b/tests/e2e/presigned_exit_message.rs new file mode 100644 index 0000000..d8d1d03 --- /dev/null +++ b/tests/e2e/presigned_exit_message.rs @@ -0,0 +1,103 @@ +use assert_cmd::prelude::*; +use std::process::Command; +use types::SignedVoluntaryExit; + +/** + +Command sequence to verify signature: + +./target/debug/eth-staking-smith existing-mnemonic \ + --chain mainnet \ + --num_validators 1 \ + --mnemonic 'ski interest capable knee usual ugly duty exercise tattoo subway delay upper bid forget say' +{ + "deposit_data": [ + { + "amount": 32000000000, + "deposit_cli_version": "2.7.0", + "deposit_data_root": "7ac103cb959b55dff155f7406393c3e6f1ba0011baee2b61bca00fdc3b2cb2c2", + "deposit_message_root": "bfd9d2c616eb570ad3fd4d4caf169b88f80490d8923537474bf1f6c5cec5e56d", + "fork_version": "00000000", + "network_name": "mainnet", + "pubkey": "8844cebb34d10e0e57f3c29ada375dafe14762ab85b2e408c3d6d55ce6d03317660bca9f2c2d17d8fbe14a2529ada1ea", + "signature": "96ebebf92967a2b187e031062f5cb5128a2bfc42559bd9dfdd1e481a056b3ef2cfddf1a0381530286013e3893e097b02129113e62a94bedd250253eb766f010824d0be7616f51b9f7609972695231bcda1cabf7a6a2d60a07e14237f2b6096ab", + "withdrawal_credentials": "0045b91b2f60b88e7392d49ae1364b55e713d06f30e563f9f99e10994b26221d" + } + ], + "keystores": [], + "mnemonic": { + "seed": "ski interest capable knee usual ugly duty exercise tattoo subway delay upper bid forget say" + }, + "private_keys": [ + "6d446ca271eb229044b9039354ecdfa6244d1a11615ec1a46fc82a800367de5d" + ] +} + +./ethdo validator exit --epoch 305658 --private-key=0x6d446ca271eb229044b9039354ecdfa6244d1a11615ec1a46fc82a800367de5d --offline --json | jq +{ + "message": { + "epoch": "305658", + "validator_index": "100" + }, + "signature": "0xa74f22d26da9934c2a9c783799fb9e7bef49b3d7c3759a0683b52ee5d71516c0ecdbcc47703f11959c5e701a6c47194410bed800217bd4dd0dab1e0587b14551771accd04ff1c78302f9605f44c3894976c5b3537b70cb7ac9dcb5398dc22079" +} + +cat offline-preparation.json +{ + "version": "3", + "genesis_validators_root": "0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95", + "epoch": "305658", + "genesis_fork_version": "0x00000000", + "exit_fork_version": "0x03000000", + "current_fork_version": "0x04000000", + "bls_to_execution_change_domain_type": "0x0a000000", + "voluntary_exit_domain_type": "0x04000000", + "validators": [ + { + "index": "100", + "pubkey": "8844cebb34d10e0e57f3c29ada375dafe14762ab85b2e408c3d6d55ce6d03317660bca9f2c2d17d8fbe14a2529ada1ea", + "state": "active_ongoing", + "withdrawal_credentials": "0x0100000000000000000000000d369bb49efa5100fd3b86a9f828c55da04d2d50" + } + ] +} + +*/ + +#[test] +fn test_presigned_exit_message() -> Result<(), Box> { + let chain = "mainnet"; + let expected_mnemonic = "ski interest capable knee usual ugly duty exercise tattoo subway delay upper bid forget say"; + let validator_start_index = "0"; + let validator_index = "100"; + let epoch = "305658"; + + // run eth-staking-smith + let mut cmd = Command::cargo_bin("eth-staking-smith")?; + + cmd.arg("presigned-exit-message"); + cmd.arg("--chain"); + cmd.arg(chain); + cmd.arg("--validator_seed_index"); + cmd.arg(validator_start_index); + cmd.arg("--validator_beacon_index"); + cmd.arg(validator_index); + cmd.arg("--mnemonic"); + cmd.arg(expected_mnemonic); + cmd.arg("--epoch"); + cmd.arg(epoch); + + cmd.assert().success(); + + let output = &cmd.output()?.stdout; + let command_output = std::str::from_utf8(output)?; + + let signed_voluntary_exit: SignedVoluntaryExit = serde_json::from_str(command_output)?; + + assert_eq!( + signed_voluntary_exit.signature.to_string(), + "0xa74f22d26da9934c2a9c783799fb9e7bef49b3d7c3759a0683b52ee5d71516c0ecdbcc47703f11959c5e701a6c47194410bed800217bd4dd0dab1e0587b14551771accd04ff1c78302f9605f44c3894976c5b3537b70cb7ac9dcb5398dc22079" + ); + + Ok(()) +}