Skip to content

Commit

Permalink
Generate presigned exit messages in a batch
Browse files Browse the repository at this point in the history
  • Loading branch information
mksh committed Oct 18, 2024
1 parent bbfebe1 commit ad83973
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 2 deletions.
118 changes: 118 additions & 0 deletions src/cli/batch_presigned_exit_message.rs
Original file line number Diff line number Diff line change
@@ -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<crate::networks::SupportedNetworks>,

/// 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<String>,

/// 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<String>,
}

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<u32, u32> = 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<u32> = 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);
}
}
1 change: 1 addition & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod batch_presigned_exit_message;
pub mod bls_to_execution_change;
pub mod existing_mnemonic;
pub mod new_mnemonic;
Expand Down
2 changes: 1 addition & 1 deletion src/cli/presigned_exit_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions src/voluntary_exit/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub(crate) mod operations;

use std::collections::HashMap;

use types::{Epoch, VoluntaryExit};

use crate::key_material::VotingKeyMaterial;
Expand Down Expand Up @@ -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<u32, u32>,
epoch: u64,
) -> (Vec<VoluntaryExit>, Vec<VotingKeyMaterial>) {
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,
Expand Down
155 changes: 155 additions & 0 deletions tests/e2e/batch_presigned_exit_message.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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<SignedVoluntaryExit> = 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(())
}
1 change: 1 addition & 0 deletions tests/e2e/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod batch_presigned_exit_message;
mod bls_to_execution_change;
mod existing_mnemonic;
mod new_mnemonic;
Expand Down

0 comments on commit ad83973

Please sign in to comment.