From 9503e338b560056ce07b5041182781cb6ef8f416 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 15 Aug 2022 14:31:01 +1000 Subject: [PATCH] Add JSON deposit data to `create` --- Cargo.lock | 4 ++ account_manager/Cargo.toml | 2 + account_manager/src/validator/create.rs | 42 ++++++++++++++- common/validator_dir/Cargo.toml | 2 + common/validator_dir/src/validator_dir.rs | 65 ++++++++++++++++++++++- 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cd7ff2ff9..b8e830df83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,8 @@ dependencies = [ "filesystem", "safe_arith", "sensitive_url", + "serde", + "serde_json", "slashing_protection", "slot_clock", "tempfile", @@ -7342,10 +7344,12 @@ dependencies = [ "deposit_contract", "derivative", "eth2_keystore", + "eth2_serde_utils", "filesystem", "hex", "lockfile", "rand 0.8.5", + "serde", "tempfile", "tree_hash", "types", diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index ce863f9147..7d90cbb427 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -24,6 +24,8 @@ safe_arith = {path = "../consensus/safe_arith"} slot_clock = { path = "../common/slot_clock" } filesystem = { path = "../common/filesystem" } sensitive_url = { path = "../common/sensitive_url" } +serde = { version = "1.0.116", features = ["derive"] } +serde_json = "1.0.58" [dev-dependencies] tempfile = "3.1.0" diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index bbd2cbc999..17ff97c9ea 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -22,6 +22,7 @@ pub const WALLET_NAME_FLAG: &str = "wallet-name"; pub const WALLET_PASSWORD_FLAG: &str = "wallet-password"; pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; pub const STORE_WITHDRAW_FLAG: &str = "store-withdrawal-keystore"; +pub const JSON_DEPOSIT_DATA_PATH: &str = "json-deposit-data-path"; pub const COUNT_FLAG: &str = "count"; pub const AT_MOST_FLAG: &str = "at-most"; pub const WALLET_PASSWORD_PROMPT: &str = "Enter your wallet's password:"; @@ -110,6 +111,17 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> { .long(STDIN_INPUTS_FLAG) .help("If present, read all user inputs from stdin instead of tty."), ) + .arg( + Arg::with_name(JSON_DEPOSIT_DATA_PATH) + .long(JSON_DEPOSIT_DATA_PATH) + .value_name("PATH") + .help( + "When provided, outputs a JSON file containing deposit data which \ + is equivalent to the 'deposit-data-*.json' file used by the \ + staking-deposit-cli tool.", + ) + .takes_value(true), + ) } pub fn cli_run( @@ -140,6 +152,9 @@ pub fn cli_run( let count: Option = clap_utils::parse_optional(matches, COUNT_FLAG)?; let at_most: Option = clap_utils::parse_optional(matches, AT_MOST_FLAG)?; + let json_deposit_data_path: Option = + clap_utils::parse_optional(matches, JSON_DEPOSIT_DATA_PATH)?; + // The command will always fail if the wallet dir does not exist. if !wallet_base_dir.exists() { return Err(format!( @@ -212,6 +227,8 @@ pub fn cli_run( ) })?; + let mut json_deposit_data = Some(vec![]).filter(|_| json_deposit_data_path.is_some()); + for i in 0..n { let voting_password = random_password(); let withdrawal_password = random_password(); @@ -241,7 +258,7 @@ pub fn cli_run( ) })?; - ValidatorDirBuilder::new(validator_dir.clone()) + let validator_dir = ValidatorDirBuilder::new(validator_dir.clone()) .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) @@ -250,9 +267,32 @@ pub fn cli_run( .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; + if let Some(json_deposit_data) = &mut json_deposit_data { + let standard_deposit_data_json = validator_dir + .standard_deposit_data_json(&spec) + .map_err(|e| format!("Unable to create standard JSON deposit data: {:?}", e))?; + json_deposit_data.push(standard_deposit_data_json); + } + println!("{}/{}\t{}", i + 1, n, voting_pubkey.as_hex_string()); } + // If configured, create a single JSON file which contains deposit data information for all + // validators. + if let Some(json_deposit_data_path) = json_deposit_data_path { + let json_deposit_data = + json_deposit_data.ok_or("Internal error: JSON deposit data is None")?; + + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&json_deposit_data_path) + .map_err(|e| format!("Unable to create {:?}: {:?}", json_deposit_data_path, e))?; + + serde_json::to_writer(&mut file, &json_deposit_data) + .map_err(|e| format!("Unable write JSON to {:?}: {:?}", json_deposit_data_path, e))?; + } + Ok(()) } diff --git a/common/validator_dir/Cargo.toml b/common/validator_dir/Cargo.toml index 0eba4cf232..1ce0806a18 100644 --- a/common/validator_dir/Cargo.toml +++ b/common/validator_dir/Cargo.toml @@ -20,6 +20,8 @@ tree_hash = "0.4.1" hex = "0.4.2" derivative = "2.1.1" lockfile = { path = "../lockfile" } +serde = { version = "1.0.116", features = ["derive"] } +eth2_serde_utils = "0.1.1" [dev-dependencies] tempfile = "3.1.0" diff --git a/common/validator_dir/src/validator_dir.rs b/common/validator_dir/src/validator_dir.rs index cb1ddde24a..0ef0cb590e 100644 --- a/common/validator_dir/src/validator_dir.rs +++ b/common/validator_dir/src/validator_dir.rs @@ -6,11 +6,12 @@ use deposit_contract::decode_eth1_tx_data; use derivative::Derivative; use eth2_keystore::{Error as KeystoreError, Keystore, PlainText}; use lockfile::{Lockfile, LockfileError}; +use serde::{Deserialize, Serialize}; use std::fs::{read, write, File}; use std::io; use std::path::{Path, PathBuf}; use tree_hash::TreeHash; -use types::{DepositData, Hash256, Keypair}; +use types::*; /// The file used to save the Eth1 transaction hash from a deposit. pub const ETH1_DEPOSIT_TX_HASH_FILE: &str = "eth1-deposit-tx-hash.txt"; @@ -41,6 +42,8 @@ pub enum Error { Eth1DepositRootMismatch, #[cfg(feature = "unencrypted_keys")] SszKeypairError(String), + DepositDataMissing, + ConfigNameUnspecified, } /// Information required to submit a deposit to the Eth1 deposit contract. @@ -54,6 +57,26 @@ pub struct Eth1DepositData { pub root: Hash256, } +/// The structure generated by the `staking-deposit-cli` which has become a quasi-standard for +/// browser-based deposit submission tools (e.g., the Ethereum Launchpad and Lido). +/// +/// We assume this code as the canonical definition: +/// +/// https://github.com/ethereum/staking-deposit-cli/blob/76ed78224fdfe3daca788d12442b3d1a37978296/staking_deposit/credentials.py#L131-L144 +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct StandardDepositDataJson { + pub pubkey: PublicKeyBytes, + pub withdrawal_credentials: Hash256, + #[serde(with = "eth2_serde_utils::quoted_u64")] + pub amount: u64, + pub signature: SignatureBytes, + #[serde(with = "eth2_serde_utils::bytes_4_hex")] + pub fork_version: [u8; 4], + pub eth2_network_name: String, + pub deposit_message_root: Hash256, + pub deposit_data_root: Hash256, +} + /// Provides a wrapper around a directory containing validator information. /// /// Holds a lockfile in `self.dir` to attempt to prevent concurrent access from multiple @@ -203,6 +226,46 @@ impl ValidatorDir { root, })) } + + /// Calls `Self::eth1_deposit_data` and then builds a `StandardDepositDataJson` from the result. + /// + /// The provided `spec` must match the value that was used to create the deposit, otherwise + /// an inconsistent result may be returned. + pub fn standard_deposit_data_json( + &self, + spec: &ChainSpec, + ) -> Result { + let deposit_data = self.eth1_deposit_data()?.ok_or(Error::DepositDataMissing)?; + + let domain = spec.get_deposit_domain(); + let deposit_message_root = deposit_data + .deposit_data + .as_deposit_message() + .signing_root(domain); + + let deposit_data_root = deposit_data.deposit_data.tree_hash_root(); + + let DepositData { + pubkey, + withdrawal_credentials, + amount, + signature, + } = deposit_data.deposit_data; + + Ok(StandardDepositDataJson { + pubkey, + withdrawal_credentials, + amount, + signature, + fork_version: spec.genesis_fork_version, + eth2_network_name: spec + .config_name + .clone() + .ok_or(Error::ConfigNameUnspecified)?, + deposit_message_root, + deposit_data_root, + }) + } } /// Attempts to load and decrypt a Keypair given path to the keystore.