Skip to content

Commit

Permalink
Generate presigned exit messages in a batch (#59)
Browse files Browse the repository at this point in the history
* Generate presigned exit messages in a batch

* Allow providing mnemonic env var for the rest of commands

* Batch presigned exit message review feedback

* Fix readme
  • Loading branch information
mksh authored Oct 30, 2024
1 parent 4a9cc16 commit 006ed16
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 16 deletions.
41 changes: 41 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ pretty_assertions = "^1.4"
assert_cmd = "2.0"
predicates = "3.0"
httpmock = "0.7"
serial_test = "*"

[[test]]
name = "e2e-tests"
Expand Down
46 changes: 42 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,40 @@ Regenerate key and deposit data with existing mnemonic:

### Example command:

with mnemonic in plain text:
```
./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"
```
or with mnemonic as an environment variable `MNEMONIC`:


## Passing mnemonic as environment variable

It is not always desirable to pass mnemonic as CLI argument, because
it can be either mistakenly recorded in shell history or recorded by
the monitoring software that could be logging process arguments on host.

This is why all commands that accept `--mnemonic` argument also support
taking mnemonic as environment variable `MNEMONIC`

For example, `existing-mnemonic` command works like follows with mnemonic in plain text:
```
./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"
```

And, as follows with mnemonic as an environment variable `MNEMONIC`:

```
export 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"
./target/debug/eth-staking-smith existing-mnemonic --chain mainnet --keystore_password testtest --num_validators 1 --withdrawal_credentials "0x0100000000000000000000000000000000000000000000000000000000000001"
```

Or, it makes possible to have bash prompt for mnemonic with hidden input like follows

```
echo "Please enter your mnemonic" ; read -s MNEMONIC ; export MNEMONIC
Please enter your mnemonic
./target/debug/eth-staking-smith existing-mnemonic --chain mainnet --keystore_password testtest --num_validators 1 --withdrawal_credentials "0x0100000000000000000000000000000000000000000000000000000000000001"
```

## Using custom testnet config

Both `existing-mnemonic` and `new-mnemonic` commands support generating validators for custom testnets.
Expand Down Expand Up @@ -122,8 +145,6 @@ then `--mnemonic` and `--validator-seed-index` may be omitted like follows
./target/debug/eth-staking-smith presigned-exit-message --chain mainnet --private-key "0x3f3e0a69a6a66aeaec606a2ccb47c703afb2e8ae64f70a1650c03343b06e8f0c" --validator_beacon_index 100 --epoch 300000
```



### Command to send VoluntaryExitMessage request to Beacon node

```
Expand All @@ -134,6 +155,23 @@ then `--mnemonic` and `--validator-seed-index` may be omitted like follows
Notice `--beacon-node-uri` parameter which makes payload to be sent to beacon node


### Command to generate batch of presigned exit messages

Sometimes it may be desirable to generate batch of presigned exit messages for the
validators created from the same mnemonic.

```
./target/debug/eth-staking-smith batch-presigned-exit-message --chain=mainnet --mnemonic='ski interest capable knee usual ugly duty exercise tattoo subway delay upper bid forget say' --epoch 305658 --seed-beacon-mapping='0:100,2:200'
```

Instead of accepting single `--validator-seed-index` and `--validator-beacon-index` pair of parameter,
it takes comma-separated mapping of validator seed index to validator beacon index in `--seed-beacon-mapping`
parameter. Keys and values in mapping should be separated by colon, so mapping of `0:100,2:200`
will read as follows

- validator with seed index `0` with given mnemonic has index `100` on beacon chain
- validator with seed index `1` has beacon index `200`

## Exporting CLI standard output into common keystores folder format

Most validator clients recognize the keystore folder format,
Expand Down
119 changes: 119 additions & 0 deletions src/cli/batch_presigned_exit_message.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
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.
///
/// This can be provided in two ways:
///
/// 1. Through the MNEMONIC environment variable (recommended)
///
/// 2. Through the --mnemonic argument in plain text.
#[arg(long, env = "MNEMONIC")]
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);
}
}
11 changes: 6 additions & 5 deletions src/cli/bls_to_execution_change.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ pub struct BlsToExecutionChangeSubcommandOpts {
/// 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)]
/// This can be provided in two ways:
///
/// 1. Through the MNEMONIC environment variable (recommended)
///
/// 2. Through the --mnemonic argument in plain text.
#[arg(long, env = "MNEMONIC")]
pub mnemonic: String,

/// The name of Ethereum PoS chain you are targeting.
Expand Down
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
13 changes: 7 additions & 6 deletions src/cli/presigned_exit_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ 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, required_unless_present = "private_key")]
/// This can be provided in two ways:
///
/// 1. Through the MNEMONIC environment variable (recommended)
///
/// 2. Through the --mnemonic argument in plain text.
#[arg(long, required_unless_present = "private_key", env = "MNEMONIC")]
pub mnemonic: Option<String>,

/// The name of Ethereum PoS chain you are targeting.
Expand Down Expand Up @@ -44,7 +45,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
Loading

0 comments on commit 006ed16

Please sign in to comment.