diff --git a/pool/src/graph.rs b/pool/src/graph.rs index 7ad7836cf..2f11aedef 100644 --- a/pool/src/graph.rs +++ b/pool/src/graph.rs @@ -260,6 +260,20 @@ impl DirectedGraph { pub fn get_roots(&self) -> Vec { self.roots.iter().map(|x| x.transaction_hash).collect() } + + /// Get list of all vertices in this graph including the roots + pub fn get_vertices(&self) -> Vec { + let mut hashes = self.roots + .iter() + .map(|x| x.transaction_hash) + .collect::>(); + let non_root_hashes = self.vertices + .iter() + .map(|x| x.transaction_hash) + .collect::>(); + hashes.extend(&non_root_hashes); + return hashes + } } /// Using transaction merkle_inputs_outputs to calculate a deterministic hash; diff --git a/pool/src/types.rs b/pool/src/types.rs index a779b5f7c..1de3a666d 100644 --- a/pool/src/types.rs +++ b/pool/src/types.rs @@ -304,11 +304,17 @@ impl Pool { } } - /// Simplest possible implementation: just return the roots + /// Currently a single rule for miner preference - + /// return all txs if less than num_to_fetch txs in the entire pool + /// otherwise return num_to_fetch of just the roots pub fn get_mineable_transactions(&self, num_to_fetch: u32) -> Vec { - let mut roots = self.graph.get_roots(); - roots.truncate(num_to_fetch as usize); - roots + if self.graph.len_vertices() <= num_to_fetch as usize { + self.graph.get_vertices() + } else { + let mut roots = self.graph.get_roots(); + roots.truncate(num_to_fetch as usize); + roots + } } } diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 1e2635d4a..e45ea9192 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -200,6 +200,12 @@ fn main() { .arg(Arg::with_name("amount") .help("Amount to send in the smallest denomination") .index(1)) + .arg(Arg::with_name("minimum_confirmations") + .help("Minimum number of confirmations required for an output to be spendable.") + .short("c") + .long("min_conf") + .default_value("1") + .takes_value(true)) .arg(Arg::with_name("dest") .help("Send the transaction to the provided server") .short("d") @@ -212,7 +218,13 @@ fn main() { transactions.") .arg(Arg::with_name("amount") .help("Amount to burn in the smallest denomination") - .index(1))) + .index(1)) + .arg(Arg::with_name("minimum_confirmations") + .help("Minimum number of confirmations required for an output to be spendable.") + .short("c") + .long("min_conf") + .default_value("1") + .takes_value(true))) .subcommand(SubCommand::with_name("info") .about("basic wallet info (outputs)"))) @@ -380,11 +392,22 @@ fn wallet_command(wallet_args: &ArgMatches) { .expect("Amount to send required") .parse() .expect("Could not parse amount as a whole number."); + let minimum_confirmations: u64 = send_args + .value_of("minimum_confirmations") + .unwrap_or("1") + .parse() + .expect("Could not parse minimum_confirmations as a whole number."); let mut dest = "stdout"; if let Some(d) = send_args.value_of("dest") { dest = d; } - wallet::issue_send_tx(&wallet_config, &keychain, amount, dest.to_string()).unwrap(); + wallet::issue_send_tx( + &wallet_config, + &keychain, + amount, + minimum_confirmations, + dest.to_string() + ).unwrap(); } ("burn", Some(send_args)) => { let amount = send_args @@ -392,7 +415,17 @@ fn wallet_command(wallet_args: &ArgMatches) { .expect("Amount to burn required") .parse() .expect("Could not parse amount as a whole number."); - wallet::issue_burn_tx(&wallet_config, &keychain, amount).unwrap(); + let minimum_confirmations: u64 = send_args + .value_of("minimum_confirmations") + .unwrap_or("1") + .parse() + .expect("Could not parse minimum_confirmations as a whole number."); + wallet::issue_burn_tx( + &wallet_config, + &keychain, + amount, + minimum_confirmations, + ).unwrap(); } ("info", Some(_)) => { wallet::show_info(&wallet_config, &keychain); diff --git a/wallet/src/checker.rs b/wallet/src/checker.rs index 7a6516be9..401bb1b98 100644 --- a/wallet/src/checker.rs +++ b/wallet/src/checker.rs @@ -20,16 +20,12 @@ use types::*; use keychain::Keychain; use util; -fn refresh_output(out: &mut OutputData, api_out: Option, tip: &api::Tip) { +fn refresh_output(out: &mut OutputData, api_out: Option) { if let Some(api_out) = api_out { out.height = api_out.height; out.lock_height = api_out.lock_height; - if out.status == OutputStatus::Locked { - // leave it Locked locally for now - } else if api_out.lock_height > tip.height { - out.status = OutputStatus::Immature; - } else { + if out.status != OutputStatus::Locked { out.status = OutputStatus::Unspent; } } else if vec![OutputStatus::Unspent, OutputStatus::Locked].contains(&out.status) { @@ -39,9 +35,10 @@ fn refresh_output(out: &mut OutputData, api_out: Option, tip: &api: /// Goes through the list of outputs that haven't been spent yet and check /// with a node whether their status has changed. -pub fn refresh_outputs(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> { - let tip = get_tip_from_node(config)?; - +pub fn refresh_outputs( + config: &WalletConfig, + keychain: &Keychain, +) -> Result<(), Error> { WalletData::with_wallet(&config.data_file_dir, |wallet_data| { // check each output that's not spent for mut out in wallet_data.outputs.values_mut().filter(|out| { @@ -50,7 +47,7 @@ pub fn refresh_outputs(config: &WalletConfig, keychain: &Keychain) -> Result<(), { // TODO check the pool for unconfirmed match get_output_from_node(config, keychain, out.value, out.n_child) { - Ok(api_out) => refresh_output(&mut out, api_out, &tip), + Ok(api_out) => refresh_output(&mut out, api_out), Err(_) => { // TODO find error with connection and return // error!(LOGGER, "Error contacting server node at {}. Is it running?", diff --git a/wallet/src/info.rs b/wallet/src/info.rs index 90299f880..1074528d5 100644 --- a/wallet/src/info.rs +++ b/wallet/src/info.rs @@ -23,8 +23,23 @@ pub fn show_info(config: &WalletConfig, keychain: &Keychain) { // operate within a lock on wallet data let _ = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + // get the current height via the api + // if we cannot get the current height use the max height known to the wallet + let current_height = match checker::get_tip_from_node(config) { + Ok(tip) => tip.height, + Err(_) => { + match wallet_data.outputs.values().map(|out| out.height).max() { + Some(height) => height, + None => 0, + } + } + }; + + // need to specify a default value here somehow + let minimum_confirmations = 1; + println!("Outputs - "); - println!("key_id, height, lock_height, status, zero_ok, value"); + println!("key_id, height, lock_height, status, spendable?, coinbase?, value"); println!("----------------------------------"); let mut outputs = wallet_data @@ -35,13 +50,14 @@ pub fn show_info(config: &WalletConfig, keychain: &Keychain) { outputs.sort_by_key(|out| out.n_child); for out in outputs { println!( - "{}, {}, {}, {:?}, {}, {}", + "{}, {}, {}, {:?}, {}, {}, {}", out.key_id, out.height, out.lock_height, out.status, - out.zero_ok, - out.value + out.eligible_to_spend(current_height, minimum_confirmations), + out.is_coinbase, + out.value, ); } }); diff --git a/wallet/src/receiver.rs b/wallet/src/receiver.rs index 46b301463..213d7ba92 100644 --- a/wallet/src/receiver.rs +++ b/wallet/src/receiver.rs @@ -172,10 +172,11 @@ impl ApiEndpoint for WalletReceiver { } /// Build a coinbase output and the corresponding kernel -fn receive_coinbase(config: &WalletConfig, - keychain: &Keychain, - block_fees: &BlockFees) - -> Result<(Output, TxKernel, BlockFees), Error> { +fn receive_coinbase( + config: &WalletConfig, + keychain: &Keychain, + block_fees: &BlockFees +) -> Result<(Output, TxKernel, BlockFees), Error> { let root_key_id = keychain.root_key_id(); // operate within a lock on wallet data @@ -205,7 +206,7 @@ fn receive_coinbase(config: &WalletConfig, status: OutputStatus::Unconfirmed, height: 0, lock_height: 0, - zero_ok: false, + is_coinbase: true, }); debug!( @@ -280,7 +281,7 @@ fn receive_transaction( status: OutputStatus::Unconfirmed, height: 0, lock_height: 0, - zero_ok: false, + is_coinbase: false, }); debug!( LOGGER, diff --git a/wallet/src/sender.rs b/wallet/src/sender.rs index 37f11ca71..c7781d6e8 100644 --- a/wallet/src/sender.rs +++ b/wallet/src/sender.rs @@ -31,14 +31,25 @@ pub fn issue_send_tx( config: &WalletConfig, keychain: &Keychain, amount: u64, + minimum_confirmations: u64, dest: String, ) -> Result<(), Error> { checker::refresh_outputs(config, keychain)?; let chain_tip = checker::get_tip_from_node(config)?; + let current_height = chain_tip.height; + + // proof of concept - set lock_height on the tx let lock_height = chain_tip.height; - let (tx, blind_sum) = build_send_tx(config, keychain, amount, lock_height)?; + let (tx, blind_sum) = build_send_tx( + config, + keychain, + amount, + current_height, + minimum_confirmations, + lock_height, + )?; let json_tx = partial_tx_to_json(amount, blind_sum, tx); if dest == "stdout" { @@ -64,6 +75,8 @@ fn build_send_tx( config: &WalletConfig, keychain: &Keychain, amount: u64, + current_height: u64, + minimum_confirmations: u64, lock_height: u64, ) -> Result<(Transaction, BlindingFactor), Error> { let key_id = keychain.clone().root_key_id(); @@ -71,8 +84,8 @@ fn build_send_tx( // operate within a lock on wallet data WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - // select some suitable outputs to spend from our local wallet - let (coins, _) = wallet_data.select(key_id.clone(), u64::max_value()); + // select some spendable coins from our local wallet + let coins = wallet_data.select(key_id.clone(), current_height, minimum_confirmations); // build transaction skeleton with inputs and change // TODO - should probably also check we are sending enough to cover the fees + non-zero output @@ -89,9 +102,17 @@ fn build_send_tx( })? } -pub fn issue_burn_tx(config: &WalletConfig, keychain: &Keychain, amount: u64) -> Result<(), Error> { +pub fn issue_burn_tx( + config: &WalletConfig, + keychain: &Keychain, + amount: u64, + minimum_confirmations: u64, +) -> Result<(), Error> { let keychain = &Keychain::burn_enabled(keychain, &Identifier::zero()); + let chain_tip = checker::get_tip_from_node(config)?; + let current_height = chain_tip.height; + let _ = checker::refresh_outputs(config, keychain); let key_id = keychain.root_key_id(); @@ -99,8 +120,8 @@ pub fn issue_burn_tx(config: &WalletConfig, keychain: &Keychain, amount: u64) -> // 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(key_id.clone(), u64::max_value()); + // select some spendable coins from the wallet + let coins = wallet_data.select(key_id.clone(), current_height, minimum_confirmations); // build transaction skeleton with inputs and change let mut parts = inputs_and_change(&coins, keychain, key_id, &mut wallet_data, amount)?; @@ -170,7 +191,7 @@ fn inputs_and_change( status: OutputStatus::Unconfirmed, height: 0, lock_height: 0, - zero_ok: true, + is_coinbase: false, }); // now lock the ouputs we're spending so we avoid accidental double spend attempt diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 9426869ea..269ab85c7 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -132,7 +132,6 @@ impl Default for WalletConfig { pub enum OutputStatus { Unconfirmed, Unspent, - Immature, Locked, Spent, } @@ -142,7 +141,6 @@ impl fmt::Display for OutputStatus { match *self { OutputStatus::Unconfirmed => write!(f, "Unconfirmed"), OutputStatus::Unspent => write!(f, "Unspent"), - OutputStatus::Immature => write!(f, "Immature"), OutputStatus::Locked => write!(f, "Locked"), OutputStatus::Spent => write!(f, "Spent"), } @@ -168,8 +166,8 @@ pub struct OutputData { pub height: u64, /// Height we are locked until pub lock_height: u64, - /// Can we spend with zero confirmations? (Did it originate from us, change output etc.) - pub zero_ok: bool, + /// Is this a coinbase output? Is it subject to coinbase locktime? + pub is_coinbase: bool, } impl OutputData { @@ -177,6 +175,29 @@ impl OutputData { fn lock(&mut self) { self.status = OutputStatus::Locked; } + + pub fn eligible_to_spend( + &self, + current_height: u64, + minimum_confirmations: u64 + ) -> bool { + if [ + OutputStatus::Spent, + OutputStatus::Locked, + ].contains(&self.status) { + return false; + } else if self.status == OutputStatus::Unconfirmed && self.is_coinbase { + return false; + } else if self.lock_height > current_height { + return false; + } else if self.status == OutputStatus::Unspent && self.height + minimum_confirmations <= current_height { + return true; + } else if self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 { + return true; + } else { + return false; + } + } } /// Wallet information tracking all our outputs. Based on HD derivation and @@ -313,27 +334,22 @@ impl WalletData { self.outputs.get(&key_id.to_hex()) } - /// Select a subset of unspent outputs to spend in a transaction - /// transferring the provided amount. - pub fn select(&self, root_key_id: keychain::Identifier, amount: u64) -> (Vec, i64) { - let mut to_spend = vec![]; - let mut input_total = 0; + /// Select spendable coins from the wallet + pub fn select( + &self, + root_key_id: keychain::Identifier, + current_height: u64, + minimum_confirmations: u64, + ) -> Vec { - for out in self.outputs.values() { - if out.root_key_id == root_key_id - && (out.status == OutputStatus::Unspent) - // the following will let us spend zero confirmation change outputs - // || (out.status == OutputStatus::Unconfirmed && out.zero_ok)) - { - to_spend.push(out.clone()); - input_total += out.value; - if input_total >= amount { - break; - } - } - } - // TODO - clean up our handling of i64 vs u64 so we are consistent - (to_spend, (input_total as i64) - (amount as i64)) + self.outputs + .values() + .filter(|out| { + out.root_key_id == root_key_id + && out.eligible_to_spend(current_height, minimum_confirmations) + }) + .map(|out| out.clone()) + .collect() } /// Next child index when we want to create a new output.