Skip to content

Commit

Permalink
[STACKED] Command to generate presigned exit message (#55)
Browse files Browse the repository at this point in the history
* Command to generate presigned exit message

* Docs for presigned-exit-message

* Code review feedback
  • Loading branch information
mksh authored Aug 23, 2024
1 parent e4f1882 commit f4fb8fb
Show file tree
Hide file tree
Showing 17 changed files with 448 additions and 88 deletions.
42 changes: 30 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
47 changes: 0 additions & 47 deletions src/bls_to_execution_change/constants.rs

This file was deleted.

1 change: 0 additions & 1 deletion src/bls_to_execution_change/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pub mod constants;
pub(crate) mod operator;

use regex::Regex;
Expand Down
4 changes: 3 additions & 1 deletion src/bls_to_execution_change/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
2 changes: 1 addition & 1 deletion src/bls_to_execution_change/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
29 changes: 28 additions & 1 deletion src/chain_spec.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -40,3 +40,30 @@ pub fn chain_spec_from_file(chain_spec_file: String) -> Result<ChainSpec, Deposi
}
}
}

pub fn validators_root_and_spec(
chain: Option<SupportedNetworks>,
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"),
)
}
}
43 changes: 19 additions & 24 deletions src/cli/bls_to_execution_change.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -95,15 +90,15 @@ 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,
);

signed_bls_to_execution_change.clone().validate(
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();
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 bls_to_execution_change;
pub mod existing_mnemonic;
pub mod new_mnemonic;
pub mod presigned_exit_message;
100 changes: 100 additions & 0 deletions src/cli/presigned_exit_message.rs
Original file line number Diff line number Diff line change
@@ -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<crate::networks::SupportedNetworks>,

/// 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<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 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);
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
8 changes: 7 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -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 {
Expand All @@ -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(),
}
}
}
Expand Down
Loading

0 comments on commit f4fb8fb

Please sign in to comment.