From ad8397358e60715bc9c2ca6d8328dbed2190f347 Mon Sep 17 00:00:00 2001 From: Maksym Kulish Date: Fri, 18 Oct 2024 08:29:14 +0300 Subject: [PATCH] Generate presigned exit messages in a batch --- src/cli/batch_presigned_exit_message.rs | 118 ++++++++++++++++ src/cli/mod.rs | 1 + src/cli/presigned_exit_message.rs | 2 +- src/main.rs | 8 +- src/voluntary_exit/mod.rs | 31 +++++ tests/e2e/batch_presigned_exit_message.rs | 155 ++++++++++++++++++++++ tests/e2e/mod.rs | 1 + 7 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 src/cli/batch_presigned_exit_message.rs create mode 100644 tests/e2e/batch_presigned_exit_message.rs diff --git a/src/cli/batch_presigned_exit_message.rs b/src/cli/batch_presigned_exit_message.rs new file mode 100644 index 0000000..5270513 --- /dev/null +++ b/src/cli/batch_presigned_exit_message.rs @@ -0,0 +1,118 @@ +use std::collections::HashMap; + +use clap::{arg, Parser}; + +use crate::beacon_node::BeaconNodeExportable; +use crate::voluntary_exit::operations::SignedVoluntaryExitValidator; +use crate::{chain_spec::validators_root_and_spec, voluntary_exit}; + +#[derive(Clone, Parser)] +pub struct BatchPresignedExitMessageSubcommandOpts { + /// 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, + + /// This is comma separated mapping of validator seed index to + /// validator beacon chain index. For example, to generate exit messages + /// for a validators with seed indices 0 and 1, and beacon chain indices + /// 111356 and 111358, pass "0:111356,1:111358" to this command. + #[arg(long, visible_alias = "seed_beacon_mapping")] + pub seed_beacon_mapping: String, + + /// Epoch number which must be included in the presigned exit message. + #[arg(long)] + 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 BatchPresignedExitMessageSubcommandOpts { + 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 mut seed_beacon_mapping: HashMap = HashMap::new(); + + for seed_beacon_pair in self.seed_beacon_mapping.split(",") { + let seed_beacon_pair_split = seed_beacon_pair.split(":"); + let seed_beacon_pair_vec: Vec = seed_beacon_pair_split.map(|s| s.parse().unwrap_or_else(|e| { + panic!("Invalid seed to beacon mapping part, not parse-able as integer: {s}: {e:?}"); + })).collect(); + if seed_beacon_pair_vec.len() != 2 { + panic!("Every mapping in seed beacon pair split must have only one seed index and beacon index") + } + seed_beacon_mapping.insert( + *seed_beacon_pair_vec.first().unwrap(), + *seed_beacon_pair_vec.get(1).unwrap(), + ); + } + + let (voluntary_exits, key_materials) = + voluntary_exit::voluntary_exit_message_batch_from_mnemonic( + self.mnemonic.as_bytes(), + seed_beacon_mapping, + self.epoch, + ); + + let mut signed_voluntary_exits = vec![]; + + for (idx, voluntary_exit) in voluntary_exits.into_iter().enumerate() { + let key_material = key_materials.get(idx).unwrap(); + 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, + ); + signed_voluntary_exits.push(signed_voluntary_exit.export()); + } + let presigned_exit_message_batch_json = + serde_json::to_string_pretty(&signed_voluntary_exits) + .expect("could not parse validator export"); + println!("{}", presigned_exit_message_batch_json); + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 215b4c5..ddc0987 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,3 +1,4 @@ +pub mod batch_presigned_exit_message; pub mod bls_to_execution_change; pub mod existing_mnemonic; pub mod new_mnemonic; diff --git a/src/cli/presigned_exit_message.rs b/src/cli/presigned_exit_message.rs index 4924c67..8f174a0 100644 --- a/src/cli/presigned_exit_message.rs +++ b/src/cli/presigned_exit_message.rs @@ -44,7 +44,7 @@ pub struct PresignedExitMessageSubcommandOpts { pub validator_beacon_index: u32, /// Epoch number which must be included in the presigned exit message. - #[arg(long, visible_alias = "execution_address")] + #[arg(long)] pub epoch: u64, /// Path to a custom Eth PoS chain config diff --git a/src/main.rs b/src/main.rs index 185b7f0..94d5159 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ #![forbid(unsafe_code)] use clap::{Parser, Subcommand}; use eth_staking_smith::cli::{ - bls_to_execution_change, existing_mnemonic, new_mnemonic, presigned_exit_message, + batch_presigned_exit_message, bls_to_execution_change, existing_mnemonic, new_mnemonic, + presigned_exit_message, }; #[derive(Parser)] @@ -23,6 +24,10 @@ enum SubCommands { /// 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), + /// Generate multiple persigned exit messages from the same mnemonic + BatchPresignedExitMessage( + batch_presigned_exit_message::BatchPresignedExitMessageSubcommandOpts, + ), } impl SubCommands { @@ -32,6 +37,7 @@ impl SubCommands { Self::ExistingMnemonic(sub) => sub.run(), Self::NewMnemonic(sub) => sub.run(), Self::PresignedExitMessage(sub) => sub.run(), + Self::BatchPresignedExitMessage(sub) => sub.run(), } } } diff --git a/src/voluntary_exit/mod.rs b/src/voluntary_exit/mod.rs index 7b69bf5..93ae215 100644 --- a/src/voluntary_exit/mod.rs +++ b/src/voluntary_exit/mod.rs @@ -1,5 +1,7 @@ pub(crate) mod operations; +use std::collections::HashMap; + use types::{Epoch, VoluntaryExit}; use crate::key_material::VotingKeyMaterial; @@ -33,6 +35,35 @@ pub fn voluntary_exit_message_from_mnemonic( (voluntary_exit, key_material.clone()) } +pub fn voluntary_exit_message_batch_from_mnemonic( + mnemonic_phrase: &[u8], + seed_beacon_mapping: HashMap, + epoch: u64, +) -> (Vec, Vec) { + let (seed, _) = crate::seed::get_eth2_seed(Some(mnemonic_phrase)); + + let mut all_materials = vec![]; + let mut all_messages = vec![]; + + for (seed_index, beacon_index) in seed_beacon_mapping { + let key_materials = + crate::key_material::seed_to_key_material(&seed, 1, seed_index, None, false, None); + + let key_material = key_materials + .first() + .expect("Error deriving key material from mnemonic"); + all_materials.push(key_material.clone()); + + let voluntary_exit = VoluntaryExit { + epoch: Epoch::from(epoch), + validator_index: beacon_index as u64, + }; + all_messages.push(voluntary_exit); + } + + (all_messages, all_materials) +} + pub fn voluntary_exit_message_from_secret_key( secret_key_bytes: &[u8], validator_beacon_index: u64, diff --git a/tests/e2e/batch_presigned_exit_message.rs b/tests/e2e/batch_presigned_exit_message.rs new file mode 100644 index 0000000..17cd245 --- /dev/null +++ b/tests/e2e/batch_presigned_exit_message.rs @@ -0,0 +1,155 @@ +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 3 \ + --mnemonic 'ski interest capable knee usual ugly duty exercise tattoo subway delay upper bid forget say' +./target/debug/eth-staking-smith existing-mnemonic \ + --chain mainnet \ + --num_validators 3 \ + --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" + }, + { + "amount": 32000000000, + "deposit_cli_version": "2.7.0", + "deposit_data_root": "21e499c8fe06ec48b410c9c8a05c65856a6f8a0059da638e959008c3a98a8863", + "deposit_message_root": "c17da3de7a90e706f6299b35fd958c1c6cf47138073fa7d704405a7dea37e760", + "fork_version": "00000000", + "network_name": "mainnet", + "pubkey": "8b9fc0882dc9257619f973fd7034d70f4fbdf7148600e7decb4ffc74536720e4fcb0853f855bd818bb881ca219682477", + "signature": "b788c42fc128e92baf5f0347acba0b0608e6aa3c36a94ce8845afd8d557503ef418230d7a576b92c633c99ef9a44f27a05156c1166aec7e28487bdad98b574911b0f9848de8d881a062773e8f75b1ebdea86e6af9279ba7c62fb2f078e8e8f30", + "withdrawal_credentials": "006ab1394ad6a99cd25e2f1f15da057cfde5025b066bcecc1afedc2a4cb36314" + }, + { + "amount": 32000000000, + "deposit_cli_version": "2.7.0", + "deposit_data_root": "dd07496493d9bc8d239c589ccb0e0c51a03a23934565629053b11806418fbbdb", + "deposit_message_root": "7c86984887d258b74f446154ab40d0e83329309c15b824bd67420225a63d6ae4", + "fork_version": "00000000", + "network_name": "mainnet", + "pubkey": "a15cc019cf4ce59f587d24bd58ae6011c8b638770c3c133cc9f081e161e7db01c92611f1a566b00208dd1e709f6ec716", + "signature": "b6312a2a9fc8427391d69e94b2d6c77db0bf78e3b1ffe368c833d1abf9f6e73e00b98d22e311fe44f7f012aa857339d715b5bbde6b28c76af3fff64f951b9a413e94a0d3729d358037bbfabd6b1905be503a91d8b19cb4fa912e2e7ddeaf044d", + "withdrawal_credentials": "0020e45be0f34aa53665c8f8d98b60163c9ba0b0549199172bb1a7c6f544f061" + } + ], + "keystores": [], + "mnemonic": { + "seed": "ski interest capable knee usual ugly duty exercise tattoo subway delay upper bid forget say" + }, + "private_keys": [ + "6d446ca271eb229044b9039354ecdfa6244d1a11615ec1a46fc82a800367de5d", + "17432f01cff4c21d848183909a300a776a57f75827414a853a52f0cbdb212f7e", + "338cc9dd5d27a9385e79487f597a72250e0f4fd2d6271ea012b8520b5455fc49" + ] +} + +./ethdo validator exit --epoch 305658 --private-key=0x6d446ca271eb229044b9039354ecdfa6244d1a11615ec1a46fc82a800367de5d --offline --json | jq +{ + "message": { + "epoch": "305658", + "validator_index": "100" + }, + "signature": "0xa74f22d26da9934c2a9c783799fb9e7bef49b3d7c3759a0683b52ee5d71516c0ecdbcc47703f11959c5e701a6c47194410bed800217bd4dd0dab1e0587b14551771accd04ff1c78302f9605f44c3894976c5b3537b70cb7ac9dcb5398dc22079" +} + +./ethdo validator exit --epoch 305658 --private-key=0x338cc9dd5d27a9385e79487f597a72250e0f4fd2d6271ea012b8520b5455fc49 --offline --json | jq + +{ + "message": { + "epoch": "305658", + "validator_index": "200" + }, + "signature": "0x8db88aabdd8f03cebba47cf3df7dd5e06ab9a49f57fc209a00cb73c5ecdea192b6ab0c5965ad8e7b6b63b9d397be3df40ea84150f2ed13ca9e0ba382c24f583ca921ff0364f18e51444838992d628623598c7c12122ff46d +} + +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" + }, + { + "index": "200", + "pubkey": "a15cc019cf4ce59f587d24bd58ae6011c8b638770c3c133cc9f081e161e7db01c92611f1a566b00208dd1e709f6ec716", + "state": "active_ongoing", + "withdrawal_credentials": "0x0100000000000000000000000d369bb49efa5100fd3b86a9f828c55da04d2d50" + } + ] +} + +*/ + +#[test] +fn test_batch_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 seed_beacon_mapping = "0:100,2:200"; + let epoch = "305658"; + + // run eth-staking-smith + let mut cmd = Command::cargo_bin("eth-staking-smith")?; + + cmd.arg("batch-presigned-exit-message"); + cmd.arg("--chain"); + cmd.arg(chain); + cmd.arg("--seed_beacon_mapping"); + cmd.arg(seed_beacon_mapping); + 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_exits: Vec = serde_json::from_str(command_output)?; + let signed_voluntary_exit1 = signed_voluntary_exits.get(0).unwrap(); + let signed_voluntary_exit2 = signed_voluntary_exits.get(1).unwrap(); + + let mut signatures = vec![ + signed_voluntary_exit1.signature.to_string(), + signed_voluntary_exit2.signature.to_string(), + ]; + signatures.sort(); + + assert_eq!( + signatures, + vec![ + "0x8db88aabdd8f03cebba47cf3df7dd5e06ab9a49f57fc209a00cb73c5ecdea192b6ab0c5965ad8e7b6b63b9d397be3df40ea84150f2ed13ca9e0ba382c24f583ca921ff0364f18e51444838992d628623598c7c12122ff46da795c000ae15dd65", + "0xa74f22d26da9934c2a9c783799fb9e7bef49b3d7c3759a0683b52ee5d71516c0ecdbcc47703f11959c5e701a6c47194410bed800217bd4dd0dab1e0587b14551771accd04ff1c78302f9605f44c3894976c5b3537b70cb7ac9dcb5398dc22079", + ] + ); + + Ok(()) +} diff --git a/tests/e2e/mod.rs b/tests/e2e/mod.rs index 49c56c2..1dcbea2 100644 --- a/tests/e2e/mod.rs +++ b/tests/e2e/mod.rs @@ -1,3 +1,4 @@ +mod batch_presigned_exit_message; mod bls_to_execution_change; mod existing_mnemonic; mod new_mnemonic;