wallet can now optionally spend zero-confirmation txs (#188)

* wallet can now optionally spend zero-confirmation txs
* add rule to get_mineable_transactions based on total pool size
This commit is contained in:
AntiochP 2017-10-18 16:47:37 -04:00 committed by Ignotus Peverell
parent bab7bd7060
commit 4d7b46b0b9
8 changed files with 162 additions and 58 deletions

View file

@ -260,6 +260,20 @@ impl DirectedGraph {
pub fn get_roots(&self) -> Vec<core::hash::Hash> { pub fn get_roots(&self) -> Vec<core::hash::Hash> {
self.roots.iter().map(|x| x.transaction_hash).collect() 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<core::hash::Hash> {
let mut hashes = self.roots
.iter()
.map(|x| x.transaction_hash)
.collect::<Vec<_>>();
let non_root_hashes = self.vertices
.iter()
.map(|x| x.transaction_hash)
.collect::<Vec<_>>();
hashes.extend(&non_root_hashes);
return hashes
}
} }
/// Using transaction merkle_inputs_outputs to calculate a deterministic hash; /// Using transaction merkle_inputs_outputs to calculate a deterministic hash;

View file

@ -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<hash::Hash> { pub fn get_mineable_transactions(&self, num_to_fetch: u32) -> Vec<hash::Hash> {
let mut roots = self.graph.get_roots(); if self.graph.len_vertices() <= num_to_fetch as usize {
roots.truncate(num_to_fetch as usize); self.graph.get_vertices()
roots } else {
let mut roots = self.graph.get_roots();
roots.truncate(num_to_fetch as usize);
roots
}
} }
} }

View file

@ -200,6 +200,12 @@ fn main() {
.arg(Arg::with_name("amount") .arg(Arg::with_name("amount")
.help("Amount to send in the smallest denomination") .help("Amount to send 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))
.arg(Arg::with_name("dest") .arg(Arg::with_name("dest")
.help("Send the transaction to the provided server") .help("Send the transaction to the provided server")
.short("d") .short("d")
@ -212,7 +218,13 @@ fn main() {
transactions.") transactions.")
.arg(Arg::with_name("amount") .arg(Arg::with_name("amount")
.help("Amount to burn in the smallest denomination") .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") .subcommand(SubCommand::with_name("info")
.about("basic wallet info (outputs)"))) .about("basic wallet info (outputs)")))
@ -380,11 +392,22 @@ fn wallet_command(wallet_args: &ArgMatches) {
.expect("Amount to send required") .expect("Amount to send required")
.parse() .parse()
.expect("Could not parse amount as a whole number."); .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"; let mut dest = "stdout";
if let Some(d) = send_args.value_of("dest") { if let Some(d) = send_args.value_of("dest") {
dest = d; 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)) => { ("burn", Some(send_args)) => {
let amount = send_args let amount = send_args
@ -392,7 +415,17 @@ fn wallet_command(wallet_args: &ArgMatches) {
.expect("Amount to burn required") .expect("Amount to burn required")
.parse() .parse()
.expect("Could not parse amount as a whole number."); .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(_)) => { ("info", Some(_)) => {
wallet::show_info(&wallet_config, &keychain); wallet::show_info(&wallet_config, &keychain);

View file

@ -20,16 +20,12 @@ use types::*;
use keychain::Keychain; use keychain::Keychain;
use util; use util;
fn refresh_output(out: &mut OutputData, api_out: Option<api::Output>, tip: &api::Tip) { fn refresh_output(out: &mut OutputData, api_out: Option<api::Output>) {
if let Some(api_out) = api_out { if let Some(api_out) = api_out {
out.height = api_out.height; out.height = api_out.height;
out.lock_height = api_out.lock_height; out.lock_height = api_out.lock_height;
if out.status == OutputStatus::Locked { if out.status != OutputStatus::Locked {
// leave it Locked locally for now
} else if api_out.lock_height > tip.height {
out.status = OutputStatus::Immature;
} else {
out.status = OutputStatus::Unspent; out.status = OutputStatus::Unspent;
} }
} else if vec![OutputStatus::Unspent, OutputStatus::Locked].contains(&out.status) { } else if vec![OutputStatus::Unspent, OutputStatus::Locked].contains(&out.status) {
@ -39,9 +35,10 @@ fn refresh_output(out: &mut OutputData, api_out: Option<api::Output>, tip: &api:
/// Goes through the list of outputs that haven't been spent yet and check /// Goes through the list of outputs that haven't been spent yet and check
/// with a node whether their status has changed. /// with a node whether their status has changed.
pub fn refresh_outputs(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> { pub fn refresh_outputs(
let tip = get_tip_from_node(config)?; config: &WalletConfig,
keychain: &Keychain,
) -> Result<(), Error> {
WalletData::with_wallet(&config.data_file_dir, |wallet_data| { WalletData::with_wallet(&config.data_file_dir, |wallet_data| {
// check each output that's not spent // check each output that's not spent
for mut out in wallet_data.outputs.values_mut().filter(|out| { 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 // TODO check the pool for unconfirmed
match get_output_from_node(config, keychain, out.value, out.n_child) { 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(_) => { Err(_) => {
// TODO find error with connection and return // TODO find error with connection and return
// error!(LOGGER, "Error contacting server node at {}. Is it running?", // error!(LOGGER, "Error contacting server node at {}. Is it running?",

View file

@ -23,8 +23,23 @@ pub fn show_info(config: &WalletConfig, keychain: &Keychain) {
// operate within a lock on wallet data // operate within a lock on wallet data
let _ = WalletData::with_wallet(&config.data_file_dir, |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!("Outputs - ");
println!("key_id, height, lock_height, status, zero_ok, value"); println!("key_id, height, lock_height, status, spendable?, coinbase?, value");
println!("----------------------------------"); println!("----------------------------------");
let mut outputs = wallet_data 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); outputs.sort_by_key(|out| out.n_child);
for out in outputs { for out in outputs {
println!( println!(
"{}, {}, {}, {:?}, {}, {}", "{}, {}, {}, {:?}, {}, {}, {}",
out.key_id, out.key_id,
out.height, out.height,
out.lock_height, out.lock_height,
out.status, out.status,
out.zero_ok, out.eligible_to_spend(current_height, minimum_confirmations),
out.value out.is_coinbase,
out.value,
); );
} }
}); });

View file

@ -172,10 +172,11 @@ impl ApiEndpoint for WalletReceiver {
} }
/// Build a coinbase output and the corresponding kernel /// Build a coinbase output and the corresponding kernel
fn receive_coinbase(config: &WalletConfig, fn receive_coinbase(
keychain: &Keychain, config: &WalletConfig,
block_fees: &BlockFees) keychain: &Keychain,
-> Result<(Output, TxKernel, BlockFees), Error> { block_fees: &BlockFees
) -> Result<(Output, TxKernel, BlockFees), Error> {
let root_key_id = keychain.root_key_id(); let root_key_id = keychain.root_key_id();
// operate within a lock on wallet data // operate within a lock on wallet data
@ -205,7 +206,7 @@ fn receive_coinbase(config: &WalletConfig,
status: OutputStatus::Unconfirmed, status: OutputStatus::Unconfirmed,
height: 0, height: 0,
lock_height: 0, lock_height: 0,
zero_ok: false, is_coinbase: true,
}); });
debug!( debug!(
@ -280,7 +281,7 @@ fn receive_transaction(
status: OutputStatus::Unconfirmed, status: OutputStatus::Unconfirmed,
height: 0, height: 0,
lock_height: 0, lock_height: 0,
zero_ok: false, is_coinbase: false,
}); });
debug!( debug!(
LOGGER, LOGGER,

View file

@ -31,14 +31,25 @@ pub fn issue_send_tx(
config: &WalletConfig, config: &WalletConfig,
keychain: &Keychain, keychain: &Keychain,
amount: u64, amount: u64,
minimum_confirmations: u64,
dest: String, dest: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
checker::refresh_outputs(config, keychain)?; checker::refresh_outputs(config, keychain)?;
let chain_tip = checker::get_tip_from_node(config)?; 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 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); let json_tx = partial_tx_to_json(amount, blind_sum, tx);
if dest == "stdout" { if dest == "stdout" {
@ -64,6 +75,8 @@ fn build_send_tx(
config: &WalletConfig, config: &WalletConfig,
keychain: &Keychain, keychain: &Keychain,
amount: u64, amount: u64,
current_height: u64,
minimum_confirmations: u64,
lock_height: u64, lock_height: u64,
) -> Result<(Transaction, BlindingFactor), Error> { ) -> Result<(Transaction, BlindingFactor), Error> {
let key_id = keychain.clone().root_key_id(); let key_id = keychain.clone().root_key_id();
@ -71,8 +84,8 @@ fn build_send_tx(
// operate within a lock on wallet data // operate within a lock on wallet data
WalletData::with_wallet(&config.data_file_dir, |wallet_data| { WalletData::with_wallet(&config.data_file_dir, |wallet_data| {
// select some suitable outputs to spend from our local wallet // select some spendable coins from our local wallet
let (coins, _) = wallet_data.select(key_id.clone(), u64::max_value()); let coins = wallet_data.select(key_id.clone(), current_height, minimum_confirmations);
// build transaction skeleton with inputs and change // build transaction skeleton with inputs and change
// TODO - should probably also check we are sending enough to cover the fees + non-zero output // 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 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 _ = checker::refresh_outputs(config, keychain);
let key_id = keychain.root_key_id(); 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 // operate within a lock on wallet data
WalletData::with_wallet(&config.data_file_dir, |mut wallet_data| { WalletData::with_wallet(&config.data_file_dir, |mut wallet_data| {
// select all suitable outputs by passing largest amount // select some spendable coins from the wallet
let (coins, _) = wallet_data.select(key_id.clone(), u64::max_value()); let coins = wallet_data.select(key_id.clone(), current_height, minimum_confirmations);
// build transaction skeleton with inputs and change // build transaction skeleton with inputs and change
let mut parts = inputs_and_change(&coins, keychain, key_id, &mut wallet_data, amount)?; 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, status: OutputStatus::Unconfirmed,
height: 0, height: 0,
lock_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 // now lock the ouputs we're spending so we avoid accidental double spend attempt

View file

@ -132,7 +132,6 @@ impl Default for WalletConfig {
pub enum OutputStatus { pub enum OutputStatus {
Unconfirmed, Unconfirmed,
Unspent, Unspent,
Immature,
Locked, Locked,
Spent, Spent,
} }
@ -142,7 +141,6 @@ impl fmt::Display for OutputStatus {
match *self { match *self {
OutputStatus::Unconfirmed => write!(f, "Unconfirmed"), OutputStatus::Unconfirmed => write!(f, "Unconfirmed"),
OutputStatus::Unspent => write!(f, "Unspent"), OutputStatus::Unspent => write!(f, "Unspent"),
OutputStatus::Immature => write!(f, "Immature"),
OutputStatus::Locked => write!(f, "Locked"), OutputStatus::Locked => write!(f, "Locked"),
OutputStatus::Spent => write!(f, "Spent"), OutputStatus::Spent => write!(f, "Spent"),
} }
@ -168,8 +166,8 @@ pub struct OutputData {
pub height: u64, pub height: u64,
/// Height we are locked until /// Height we are locked until
pub lock_height: u64, pub lock_height: u64,
/// Can we spend with zero confirmations? (Did it originate from us, change output etc.) /// Is this a coinbase output? Is it subject to coinbase locktime?
pub zero_ok: bool, pub is_coinbase: bool,
} }
impl OutputData { impl OutputData {
@ -177,6 +175,29 @@ impl OutputData {
fn lock(&mut self) { fn lock(&mut self) {
self.status = OutputStatus::Locked; 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 /// Wallet information tracking all our outputs. Based on HD derivation and
@ -313,27 +334,22 @@ impl WalletData {
self.outputs.get(&key_id.to_hex()) self.outputs.get(&key_id.to_hex())
} }
/// Select a subset of unspent outputs to spend in a transaction /// Select spendable coins from the wallet
/// transferring the provided amount. pub fn select(
pub fn select(&self, root_key_id: keychain::Identifier, amount: u64) -> (Vec<OutputData>, i64) { &self,
let mut to_spend = vec![]; root_key_id: keychain::Identifier,
let mut input_total = 0; current_height: u64,
minimum_confirmations: u64,
) -> Vec<OutputData> {
for out in self.outputs.values() { self.outputs
if out.root_key_id == root_key_id .values()
&& (out.status == OutputStatus::Unspent) .filter(|out| {
// the following will let us spend zero confirmation change outputs out.root_key_id == root_key_id
// || (out.status == OutputStatus::Unconfirmed && out.zero_ok)) && out.eligible_to_spend(current_height, minimum_confirmations)
{ })
to_spend.push(out.clone()); .map(|out| out.clone())
input_total += out.value; .collect()
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))
} }
/// Next child index when we want to create a new output. /// Next child index when we want to create a new output.