From b85006ebe5c1932a90d5fd39a7b293b55d63e953 Mon Sep 17 00:00:00 2001 From: Ignotus Peverell Date: Thu, 12 Oct 2017 03:35:40 +0000 Subject: [PATCH] Wallet operation to burn some coins (#172) * Burn transaction for testing * Burn bug fixes, embed burn key in keychain * Better error logging in API, wallet fee calc fix --- api/src/rest.rs | 18 ++++-- core/src/core/build.rs | 6 +- keychain/src/extkey.rs | 10 +++- keychain/src/keychain.rs | 12 ++++ src/bin/grin.rs | 19 ++++++- wallet/src/lib.rs | 2 +- wallet/src/receiver.rs | 8 +-- wallet/src/sender.rs | 116 ++++++++++++++++++++++++++++----------- wallet/src/types.rs | 16 ++++++ 9 files changed, 158 insertions(+), 49 deletions(-) diff --git a/api/src/rest.rs b/api/src/rest.rs index ef263399e..d88135816 100644 --- a/api/src/rest.rs +++ b/api/src/rest.rs @@ -210,11 +210,19 @@ where let t: E::OP_IN = serde_json::from_reader(req.body.by_ref()).map_err(|e| { IronError::new(e, status::BadRequest) })?; - let res = self.endpoint.operation(self.operation.clone(), t)?; - let res_json = serde_json::to_string(&res).map_err(|e| { - IronError::new(e, status::InternalServerError) - })?; - Ok(Response::with((status::Ok, res_json))) + let res = self.endpoint.operation(self.operation.clone(), t); + match res { + Ok(resp) => { + let res_json = serde_json::to_string(&resp).map_err(|e| { + IronError::new(e, status::InternalServerError) + })?; + Ok(Response::with((status::Ok, res_json))) + } + Err(e) => { + error!("API operation: {:?}", e); + Err(IronError::from(e)) + } + } } } diff --git a/core/src/core/build.rs b/core/src/core/build.rs index ea8debb65..640d520b4 100644 --- a/core/src/core/build.rs +++ b/core/src/core/build.rs @@ -39,7 +39,7 @@ pub struct Context<'a> { /// Function type returned by the transaction combinators. Transforms a /// (Transaction, BlindSum) pair into another, provided some context. -type Append = for<'a> Fn(&'a mut Context, (Transaction, BlindSum)) -> (Transaction, BlindSum); +pub type Append = for<'a> Fn(&'a mut Context, (Transaction, BlindSum)) -> (Transaction, BlindSum); /// Adds an input with the provided value and blinding key to the transaction /// being built. @@ -50,8 +50,8 @@ pub fn input(value: u64, pubkey: Identifier) -> Box { }) } -/// Adds an output with the provided value and blinding key to the transaction -/// being built. +/// Adds an output with the provided value and key identifier from the +/// keychain. pub fn output(value: u64, pubkey: Identifier) -> Box { Box::new(move |build, (tx, sum)| -> (Transaction, BlindSum) { let commit = build.keychain.commit(value, &pubkey).unwrap(); diff --git a/keychain/src/extkey.rs b/keychain/src/extkey.rs index c91be7502..7fb568c04 100644 --- a/keychain/src/extkey.rs +++ b/keychain/src/extkey.rs @@ -135,6 +135,12 @@ impl Identifier { Identifier(identifier) } + pub fn from_pubkey(secp: &Secp256k1, pubkey: &PublicKey) -> Identifier { + let bytes = pubkey.serialize_vec(secp, true); + let identifier = blake2b(20, &[], &bytes[..]); + Identifier::from_bytes(&identifier.as_bytes()) + } + fn from_hex(hex: &str) -> Result { // TODO - error handling, don't unwrap here let bytes = util::from_hex(hex.to_string()).unwrap(); @@ -245,9 +251,7 @@ impl ExtendedKey { // corresponding to the underlying SecretKey pub fn identifier(&self, secp: &Secp256k1) -> Result { let pubkey = PublicKey::from_secret_key(secp, &self.key)?; - let bytes = pubkey.serialize_vec(secp, true); - let identifier = blake2b(20, &[], &bytes[..]); - Ok(Identifier::from_bytes(&identifier.as_bytes())) + Ok(Identifier::from_pubkey(secp, &pubkey)) } /// Derive an extended key from an extended key diff --git a/keychain/src/keychain.rs b/keychain/src/keychain.rs index 04280a52e..0b2d520e8 100644 --- a/keychain/src/keychain.rs +++ b/keychain/src/keychain.rs @@ -48,6 +48,10 @@ impl From for Error { pub struct Keychain { secp: Secp256k1, extkey: extkey::ExtendedKey, + + /// for tests and burn only, associate the zero fingerprint to a known + /// dummy private key + pub enable_burn_key: bool, } impl Keychain { @@ -61,6 +65,7 @@ impl Keychain { let keychain = Keychain { secp: secp, extkey: extkey, + enable_burn_key: false, }; Ok(keychain) } @@ -82,6 +87,13 @@ impl Keychain { // TODO - smarter lookups - can we cache key_id/fingerprint -> derivation // number somehow? fn derived_key(&self, pubkey: &Identifier) -> Result { + if self.enable_burn_key { + // for tests and burn only, associate the zero fingerprint to a known + // dummy private key + if pubkey.fingerprint().to_string() == "00000000" { + return Ok(SecretKey::from_slice(&self.secp, &[1; 32])?); + } + } for i in 1..10000 { let extkey = self.extkey.derive(&self.secp, i)?; if extkey.identifier(&self.secp)? == *pubkey { diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 9ceece06f..ceba883cd 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -200,6 +200,14 @@ fn main() { .long("dest") .takes_value(true))) + .subcommand(SubCommand::with_name("burn") + .about("** TESTING ONLY ** Burns the provided amount to a known \ + key. Similar to send but burns an output to allow single-party \ + transactions.") + .arg(Arg::with_name("amount") + .help("Amount to burn in the smallest denomination") + .index(1))) + .subcommand(SubCommand::with_name("info") .about("basic wallet info (outputs)"))) @@ -313,7 +321,7 @@ fn wallet_command(wallet_args: &ArgMatches) { // TODO do something closer to BIP39, eazy solution right now let seed = blake2::blake2b::blake2b(32, &[], hd_seed.as_bytes()); - let keychain = Keychain::from_seed(seed.as_bytes()).expect( + let mut keychain = Keychain::from_seed(seed.as_bytes()).expect( "Failed to initialize keychain from the provided seed.", ); @@ -371,6 +379,15 @@ fn wallet_command(wallet_args: &ArgMatches) { } wallet::issue_send_tx(&wallet_config, &keychain, amount, dest.to_string()).unwrap(); } + ("burn", Some(send_args)) => { + let amount = send_args + .value_of("amount") + .expect("Amount to burn required") + .parse() + .expect("Could not parse amount as a whole number."); + keychain.enable_burn_key = true; + wallet::issue_burn_tx(&wallet_config, &keychain, amount).unwrap(); + } ("info", Some(_)) => { wallet::show_info(&wallet_config, &keychain); } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index a931d4236..bc2bce4fc 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -38,5 +38,5 @@ mod types; pub use info::show_info; pub use receiver::{WalletReceiver, receive_json_tx}; -pub use sender::issue_send_tx; +pub use sender::{issue_send_tx, issue_burn_tx}; pub use types::{WalletConfig, WalletReceiveRequest, BlockFees, CbData}; diff --git a/wallet/src/receiver.rs b/wallet/src/receiver.rs index 2589992c3..e3f1c23eb 100644 --- a/wallet/src/receiver.rs +++ b/wallet/src/receiver.rs @@ -53,14 +53,14 @@ use core::consensus::reward; use core::core::{Block, Transaction, TxKernel, Output, build}; use core::ser; use api::{self, ApiEndpoint, Operation, ApiResult}; +use keychain::{BlindingFactor, Keychain}; use types::*; use util; -use keychain::{BlindingFactor, Keychain}; /// Dummy wrapper for the hex-encoded serialized transaction. #[derive(Serialize, Deserialize)] -struct TxWrapper { - tx_hex: String, +pub struct TxWrapper { + pub tx_hex: String, } /// Receive an already well formed JSON transaction issuance and finalize the @@ -243,7 +243,7 @@ fn receive_transaction( // so 80 is basically the minimum fee for a basic transaction // so lets use 100 for now (revisit this) - let fee_amount = 100; + let fee_amount = tx_fee(partial.inputs.len(), partial.outputs.len() + 1, None); let out_amount = amount - fee_amount; let (tx_final, _) = build::transaction(vec![ diff --git a/wallet/src/sender.rs b/wallet/src/sender.rs index b445d8429..eb7ae39de 100644 --- a/wallet/src/sender.rs +++ b/wallet/src/sender.rs @@ -15,8 +15,11 @@ use api; use checker; use core::core::{Transaction, build}; -use keychain::{BlindingFactor, Keychain}; +use core::ser; +use keychain::{BlindingFactor, Keychain, Fingerprint, Identifier}; +use receiver::TxWrapper; use types::*; +use util; /// Issue a new transaction to the provided sender by spending some of our /// wallet @@ -70,47 +73,96 @@ fn build_send_tx( return Err(Error::NotEnoughFunds((-change) as u64)); } - // build inputs using the appropriate derived pubkeys - let mut parts = vec![]; + // build transaction skeleton with inputs and change + let mut parts = inputs_and_change(&coins, keychain, fingerprint, wallet_data, amount)?; // This is more proof of concept than anything but here we set a // lock_height on the transaction being sent (based on current chain height via api). parts.push(build::with_lock_height(lock_height)); - // TODO add fees, which is likely going to make this iterative - // parts.push(build::with_fees(100)); - - for coin in &coins { - let pubkey = keychain.derive_pubkey(coin.n_child)?; - parts.push(build::input(coin.value, pubkey)); - } - - // derive an additional pubkey for change and build the change output - let change_derivation = wallet_data.next_child(fingerprint.clone()); - let change_key = keychain.derive_pubkey(change_derivation)?; - parts.push(build::output(change as u64, change_key.clone())); - - // we got that far, time to start tracking the new output, finalize tx - // and lock the outputs used - wallet_data.add_output(OutputData { - fingerprint: fingerprint.clone(), - identifier: change_key.clone(), - n_child: change_derivation, - value: change as u64, - status: OutputStatus::Unconfirmed, - height: 0, - lock_height: 0, - }); - - for coin in &coins { - wallet_data.lock_output(coin); - } - let result = build::transaction(parts, &keychain)?; Ok(result) })? } +pub fn issue_burn_tx( + config: &WalletConfig, + keychain: &Keychain, + amount: u64, +) -> Result<(), Error> { + + let _ = checker::refresh_outputs(config, keychain); + let fingerprint = keychain.clone().fingerprint(); + + // operate within a lock on wallet data + WalletData::with_wallet(&config.data_file_dir, |mut wallet_data| { + + // select all suitable outputs by passing largest amount + let (coins, _) = wallet_data.select(fingerprint.clone(), u64::max_value()); + + // build transaction skeleton with inputs and change + let mut parts = inputs_and_change(&coins, keychain, fingerprint, &mut wallet_data, amount)?; + + // add burn output and fees + parts.push(build::output(amount, Identifier::from_bytes(&[0; 20]))); + + // finalize the burn transaction and send + let (tx_burn, _) = build::transaction(parts, &keychain)?; + tx_burn.validate(&keychain.secp())?; + + let tx_hex = util::to_hex(ser::ser_vec(&tx_burn).unwrap()); + let url = format!("{}/v1/pool/push", config.check_node_api_http_addr.as_str()); + let _: () = api::client::post(url.as_str(), &TxWrapper { tx_hex: tx_hex }) + .map_err(|e| Error::Node(e))?; + Ok(()) + })? +} + +fn inputs_and_change(coins: &Vec, keychain: &Keychain, fingerprint: Fingerprint, wallet_data: &mut WalletData, amount: u64) -> Result>, Error> { + + let mut parts = vec![]; + + // calculate the total in inputs, fees and how much is left + let total: u64 = coins.iter().map(|c| c.value).sum(); + let fee = tx_fee(coins.len(), 2, None); + let shortage = (total as i64) - (amount as i64) - (fee as i64); + if shortage < 0 { + return Err(Error::NotEnoughFunds((-shortage) as u64)); + } + parts.push(build::with_fee(fee)); + let change = total - amount - fee; + + // build inputs using the appropriate derived pubkeys + for coin in coins { + let pubkey = keychain.derive_pubkey(coin.n_child)?; + parts.push(build::input(coin.value, pubkey)); + } + + // derive an additional pubkey for change and build the change output + let change_derivation = wallet_data.next_child(fingerprint.clone()); + let change_key = keychain.derive_pubkey(change_derivation)?; + parts.push(build::output(change, change_key.clone())); + + // we got that far, time to start tracking the new output + // and lock the outputs used + wallet_data.add_output(OutputData { + fingerprint: fingerprint.clone(), + identifier: change_key.clone(), + n_child: change_derivation, + value: change as u64, + status: OutputStatus::Unconfirmed, + height: 0, + lock_height: 0, + }); + + // lock the ouputs we're spending + for coin in coins { + wallet_data.lock_output(coin); + } + + Ok(parts) +} + #[cfg(test)] mod test { use core::core::build::{input, output, transaction}; diff --git a/wallet/src/types.rs b/wallet/src/types.rs index b643bedc1..54b7f24c3 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -32,6 +32,22 @@ use util; const DAT_FILE: &'static str = "wallet.dat"; const LOCK_FILE: &'static str = "wallet.lock"; +const DEFAULT_BASE_FEE: u64 = 10; + +/// Transaction fee calculation +pub fn tx_fee(input_len: usize, output_len: usize, base_fee: Option) -> u64 { + let use_base_fee = match base_fee { + Some(bf) => bf, + None => DEFAULT_BASE_FEE, + }; + let mut tx_weight = -1 * (input_len as i32) + 4 * (output_len as i32) + 1; + if tx_weight < 1 { + tx_weight = 1; + } + + (tx_weight as u64) * use_base_fee +} + /// Wallet errors, mostly wrappers around underlying crypto or I/O errors. #[derive(Debug)] pub enum Error {