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> {
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;

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> {
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
}
}
}

View file

@ -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);

View file

@ -20,16 +20,12 @@ use types::*;
use keychain::Keychain;
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 {
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<api::Output>, 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?",

View file

@ -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,
);
}
});

View file

@ -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,

View file

@ -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

View file

@ -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<OutputData>, 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<OutputData> {
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.