From 79168b3063a44d0fb1254c2562eac6f549fa3e90 Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Mon, 25 Nov 2019 15:52:53 +1100 Subject: [PATCH] Add refund-deposit-contract to lcli --- lcli/src/deploy_deposit_contract.rs | 48 ++++++----- lcli/src/main.rs | 46 ++++++++++- lcli/src/refund_deposit_contract.rs | 121 ++++++++++++++++++++++++++++ 3 files changed, 192 insertions(+), 23 deletions(-) create mode 100644 lcli/src/refund_deposit_contract.rs diff --git a/lcli/src/deploy_deposit_contract.rs b/lcli/src/deploy_deposit_contract.rs index be2f657632..40ef1ca90c 100644 --- a/lcli/src/deploy_deposit_contract.rs +++ b/lcli/src/deploy_deposit_contract.rs @@ -36,28 +36,7 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< .expect("should locate home directory") }); - let password = if let Some(password_path) = matches.value_of("password") { - Some( - File::open(password_path) - .map_err(|e| format!("Unable to open password file: {:?}", e)) - .and_then(|mut file| { - let mut password = String::new(); - file.read_to_string(&mut password) - .map_err(|e| format!("Unable to read password file to string: {:?}", e)) - .map(|_| password) - }) - .map(|password| { - // Trim the linefeed from the end. - if password.ends_with("\n") { - password[0..password.len() - 1].to_string() - } else { - password - } - })?, - ) - } else { - None - }; + let password = parse_password(matches)?; let endpoint = matches .value_of("endpoint") @@ -121,3 +100,28 @@ pub fn run(mut env: Environment, matches: &ArgMatches) -> Result< Ok(()) } + +pub fn parse_password(matches: &ArgMatches) -> Result, String> { + if let Some(password_path) = matches.value_of("password") { + Ok(Some( + File::open(password_path) + .map_err(|e| format!("Unable to open password file: {:?}", e)) + .and_then(|mut file| { + let mut password = String::new(); + file.read_to_string(&mut password) + .map_err(|e| format!("Unable to read password file to string: {:?}", e)) + .map(|_| password) + }) + .map(|password| { + // Trim the linefeed from the end. + if password.ends_with("\n") { + password[0..password.len() - 1].to_string() + } else { + password + } + })?, + )) + } else { + Ok(None) + } +} diff --git a/lcli/src/main.rs b/lcli/src/main.rs index 6dc341ddf1..1560759515 100644 --- a/lcli/src/main.rs +++ b/lcli/src/main.rs @@ -4,6 +4,7 @@ extern crate log; mod deploy_deposit_contract; mod parse_hex; mod pycli; +mod refund_deposit_contract; mod transition_blocks; use clap::{App, Arg, SubCommand}; @@ -158,6 +159,45 @@ fn main() { .help("The password file to unlock the eth1 account (see --index)"), ) ) + .subcommand( + SubCommand::with_name("refund-deposit-contract") + .about( + "Calls the steal() function on a testnet eth1 contract.", + ) + .arg( + Arg::with_name("testnet-dir") + .short("d") + .long("testnet-dir") + .value_name("PATH") + .takes_value(true) + .help("The testnet dir. Defaults to ~/.lighthouse/testnet"), + ) + .arg( + Arg::with_name("endpoint") + .short("e") + .long("endpoint") + .value_name("HTTP_SERVER") + .takes_value(true) + .default_value("http://localhost:8545") + .help("The URL to the eth1 JSON-RPC http API."), + ) + .arg( + Arg::with_name("password") + .long("password") + .value_name("FILE") + .takes_value(true) + .help("The password file to unlock the eth1 account (see --index)"), + ) + .arg( + Arg::with_name("account-index") + .short("i") + .long("account-index") + .value_name("INDEX") + .takes_value(true) + .default_value("0") + .help("The eth1 accounts[] index which will send the transaction"), + ) + ) .subcommand( SubCommand::with_name("pycli") .about("TODO") @@ -229,7 +269,11 @@ fn main() { .unwrap_or_else(|e| error!("Failed to run pycli: {}", e)), ("deploy-deposit-contract", Some(matches)) => { deploy_deposit_contract::run::(env, matches) - .unwrap_or_else(|e| error!("Failed to run deploy-deposit-contract commmand: {}", e)) + .unwrap_or_else(|e| error!("Failed to run deploy-deposit-contract command: {}", e)) + } + ("refund-deposit-contract", Some(matches)) => { + refund_deposit_contract::run::(env, matches) + .unwrap_or_else(|e| error!("Failed to run refund-deposit-contract command: {}", e)) } (other, _) => error!("Unknown subcommand {}. See --help.", other), } diff --git a/lcli/src/refund_deposit_contract.rs b/lcli/src/refund_deposit_contract.rs new file mode 100644 index 0000000000..64bb9dcc1b --- /dev/null +++ b/lcli/src/refund_deposit_contract.rs @@ -0,0 +1,121 @@ +use crate::deploy_deposit_contract::parse_password; +use clap::ArgMatches; +use environment::Environment; +use eth2_testnet::Eth2TestnetDir; +use futures::{future, Future}; +use std::path::PathBuf; +use types::EthSpec; +use web3::{ + transports::Http, + types::{Address, TransactionRequest, U256}, + Web3, +}; + +pub const DEFAULT_DATA_DIR: &str = ".lighthouse/testnet"; + +/// `keccak("steal()")[0..4]` +pub const DEPOSIT_ROOT_FN_SIGNATURE: &[u8] = &[0xcf, 0x7a, 0x89, 0x65]; + +pub fn run(mut env: Environment, matches: &ArgMatches) -> Result<(), String> { + let endpoint = matches + .value_of("endpoint") + .ok_or_else(|| "Endpoint not specified")?; + + let account_index = matches + .value_of("account-index") + .ok_or_else(|| "No account-index".to_string())? + .parse::() + .map_err(|e| format!("Unable to parse account-index: {}", e))?; + + let password_opt = parse_password(matches)?; + + let testnet_dir = matches + .value_of("testnet-dir") + .ok_or_else(|| ()) + .and_then(|dir| dir.parse::().map_err(|_| ())) + .unwrap_or_else(|_| { + dirs::home_dir() + .map(|mut home| { + home.push(DEFAULT_DATA_DIR); + home + }) + .expect("should locate home directory") + }); + + let eth2_testnet_dir = Eth2TestnetDir::load(testnet_dir)?; + + let (_event_loop, transport) = Http::new(&endpoint).map_err(|e| { + format!( + "Failed to start HTTP transport connected to ganache: {:?}", + e + ) + })?; + + let web3_1 = Web3::new(transport); + let web3_2 = web3_1.clone(); + + // Convert from `types::Address` to `web3::types::Address`. + let deposit_contract = Address::from_slice( + eth2_testnet_dir + .deposit_contract_address()? + .as_fixed_bytes(), + ); + + let future = web3_1 + .eth() + .accounts() + .map_err(|e| format!("Failed to get accounts: {:?}", e)) + .and_then(move |accounts| { + accounts + .get(account_index) + .cloned() + .ok_or_else(|| "Insufficient accounts for deposit".to_string()) + }) + .and_then(move |from_address| { + let future: Box + Send> = + if let Some(password) = password_opt { + // Unlock for only a single transaction. + let duration = None; + + let future = web3_1 + .personal() + .unlock_account(from_address, &password, duration) + .then(move |result| match result { + Ok(true) => Ok(from_address), + Ok(false) => Err("Eth1 node refused to unlock account".to_string()), + Err(e) => Err(format!("Eth1 unlock request failed: {:?}", e)), + }); + + Box::new(future) + } else { + Box::new(future::ok(from_address)) + }; + + future + }) + .and_then(move |from| { + let tx_request = TransactionRequest { + from, + to: Some(deposit_contract), + gas: Some(U256::from(400_000)), + gas_price: None, + value: Some(U256::zero()), + data: Some(DEPOSIT_ROOT_FN_SIGNATURE.into()), + nonce: None, + condition: None, + }; + + web3_2 + .eth() + .send_transaction(tx_request) + .map_err(|e| format!("Failed to call deposit fn: {:?}", e)) + }) + .map(move |tx| info!("Refund transaction submitted: eth1_tx_hash: {:?}", tx)) + .map_err(move |e| error!("Unable to submit refund transaction: error: {}", e)); + + env.runtime() + .block_on(future) + .map_err(|()| format!("Failed to send transaction"))?; + + Ok(()) +}