add "smallest first" strategy for coin selection (#256)

This commit is contained in:
AntiochP 2017-11-10 14:33:36 -05:00 committed by GitHub
parent 8f33c7e0fe
commit 04eb400422
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 87 additions and 6 deletions

View file

@ -209,6 +209,13 @@ fn main() {
.long("min_conf") .long("min_conf")
.default_value("1") .default_value("1")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("selection_strategy")
.help("Coin/Output selection strategy.")
.short("s")
.long("selection")
.possible_values(&["default", "smallest"])
.default_value("default")
.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")
@ -397,9 +404,12 @@ fn wallet_command(wallet_args: &ArgMatches) {
.expect("Could not parse amount as a number with optional decimal point."); .expect("Could not parse amount as a number with optional decimal point.");
let minimum_confirmations: u64 = send_args let minimum_confirmations: u64 = send_args
.value_of("minimum_confirmations") .value_of("minimum_confirmations")
.unwrap_or("1") .unwrap()
.parse() .parse()
.expect("Could not parse minimum_confirmations as a whole number."); .expect("Could not parse minimum_confirmations as a whole number.");
let selection_strategy = send_args
.value_of("selection_strategy")
.expect("Selection strategy required");
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;
@ -410,6 +420,7 @@ fn wallet_command(wallet_args: &ArgMatches) {
amount, amount,
minimum_confirmations, minimum_confirmations,
dest.to_string(), dest.to_string(),
(selection_strategy == "default"),
); );
match result { match result {
Ok(_) => {}, //success messaged logged internally Ok(_) => {}, //success messaged logged internally
@ -425,7 +436,7 @@ fn wallet_command(wallet_args: &ArgMatches) {
.expect("Could not parse amount as number with optional decimal point."); .expect("Could not parse amount as number with optional decimal point.");
let minimum_confirmations: u64 = send_args let minimum_confirmations: u64 = send_args
.value_of("minimum_confirmations") .value_of("minimum_confirmations")
.unwrap_or("1") .unwrap()
.parse() .parse()
.expect("Could not parse minimum_confirmations as a whole number."); .expect("Could not parse minimum_confirmations as a whole number.");
wallet::issue_burn_tx(&wallet_config, &keychain, amount, minimum_confirmations) wallet::issue_burn_tx(&wallet_config, &keychain, amount, minimum_confirmations)

View file

@ -36,6 +36,7 @@ pub fn issue_send_tx(
amount: u64, amount: u64,
minimum_confirmations: u64, minimum_confirmations: u64,
dest: String, dest: String,
selection_strategy: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
checker::refresh_outputs(config, keychain)?; checker::refresh_outputs(config, keychain)?;
@ -52,6 +53,7 @@ pub fn issue_send_tx(
current_height, current_height,
minimum_confirmations, minimum_confirmations,
lock_height, lock_height,
selection_strategy,
)?; )?;
let partial_tx = build_partial_tx(amount, blind_sum, tx); let partial_tx = build_partial_tx(amount, blind_sum, tx);
@ -79,12 +81,19 @@ fn build_send_tx(
current_height: u64, current_height: u64,
minimum_confirmations: u64, minimum_confirmations: u64,
lock_height: u64, lock_height: u64,
default_strategy: bool,
) -> Result<(Transaction, BlindingFactor), Error> { ) -> Result<(Transaction, BlindingFactor), Error> {
let key_id = keychain.clone().root_key_id(); let key_id = keychain.clone().root_key_id();
// select some spendable coins from the wallet // select some spendable coins from the wallet
let coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { let coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| {
wallet_data.select(key_id.clone(), current_height, minimum_confirmations) wallet_data.select(
key_id.clone(),
amount,
current_height,
minimum_confirmations,
default_strategy,
)
})?; })?;
// build transaction skeleton with inputs and change // build transaction skeleton with inputs and change
@ -124,9 +133,11 @@ pub fn issue_burn_tx(
// select some spendable coins from the wallet // select some spendable coins from the wallet
let coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { let coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| {
wallet_data.select(key_id.clone(), current_height, minimum_confirmations) wallet_data.select(key_id.clone(), amount, current_height, minimum_confirmations, false)
})?; })?;
debug!(LOGGER, "selected some coins - {}", coins.len());
let mut parts = inputs_and_change(&coins, config, keychain, key_id, amount)?; let mut parts = inputs_and_change(&coins, config, keychain, key_id, amount)?;
// add burn output and fees // add burn output and fees

View file

@ -499,8 +499,67 @@ impl WalletData {
self.outputs.get(&key_id.to_hex()) self.outputs.get(&key_id.to_hex())
} }
/// Select spendable coins from the wallet /// Select spendable coins from the wallet.
/// Default strategy is "aggressive".
/// Non-default strategy is "smallest_first".
/// When we introduce additional strategies we should pass something other than a bool in.
pub fn select( pub fn select(
&self,
root_key_id: keychain::Identifier,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
default_strategy: bool,
) -> Vec<OutputData> {
if default_strategy {
self.select_aggressive(
root_key_id,
current_height,
minimum_confirmations,
)
} else {
self.select_smallest_first(
root_key_id,
amount,
current_height,
minimum_confirmations,
)
}
}
// Selects the smallest number of outputs after ordering them by value.
// Reduces "dust" but leaves larger outputs unspent if possible.
fn select_smallest_first(
&self,
root_key_id: keychain::Identifier,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
) -> Vec<OutputData> {
let mut eligible = self.outputs
.values()
.filter(|out| {
out.root_key_id == root_key_id
&& out.eligible_to_spend(current_height, minimum_confirmations)
})
.cloned()
.collect::<Vec<OutputData>>();
eligible.sort_by_key(|out| out.value);
let mut total_amount = 0;
eligible.iter()
.take_while(|out| {
let res = total_amount < amount;
total_amount += out.value;
res
})
.cloned()
.collect::<Vec<_>>()
}
// Selects all eligible outputs to spend to reduce UTXO set as much as possible (the default).
fn select_aggressive(
&self, &self,
root_key_id: keychain::Identifier, root_key_id: keychain::Identifier,
current_height: u64, current_height: u64,
@ -512,7 +571,7 @@ impl WalletData {
out.root_key_id == root_key_id out.root_key_id == root_key_id
&& out.eligible_to_spend(current_height, minimum_confirmations) && out.eligible_to_spend(current_height, minimum_confirmations)
}) })
.map(|out| out.clone()) .cloned()
.collect() .collect()
} }