fix: wallet coin selection respects max_block_weight (#2546)

* fix #2510: wallet coin selection respects max_block_weight

Deprecate "soft" max_outputs limit and introduce "hard" max_outputs
limit based on max_block_weight.

* Fix tests
This commit is contained in:
Ivan Sorokin
2019-02-13 00:06:25 +01:00
committed by Ignotus Peverell
parent 1d0c04cf0b
commit aa4f44b79a
17 changed files with 39 additions and 82 deletions
-2
View File
@@ -378,7 +378,6 @@ impl LocalServerContainer {
let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None);
let client_w = HTTPWalletCommAdapter::new();
let max_outputs = 500;
let change_outputs = 1;
let mut wallet = LMDBBackend::new(config.clone(), "", client_n)
@@ -389,7 +388,6 @@ impl LocalServerContainer {
None,
amount,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy == "all",
None,
-1
View File
@@ -963,7 +963,6 @@ fn replicate_tx_fluff_failure() {
let (mut slate, lock_fn) = api.initiate_tx(
None, amount, // amount
2, // minimum confirmations
500, // max outputs
1000, // num change outputs
true, // select all outputs
None,
-4
View File
@@ -389,9 +389,6 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
// fluff
let fluff = args.is_present("fluff");
// max_outputs
let max_outputs = 500;
Ok(command::SendArgs {
amount: amount,
message: message,
@@ -402,7 +399,6 @@ pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, ParseErro
dest: dest.to_owned(),
change_outputs: change_outputs,
fluff: fluff,
max_outputs: max_outputs,
})
}
-3
View File
@@ -205,7 +205,6 @@ pub struct SendArgs {
pub dest: String,
pub change_outputs: usize,
pub fluff: bool,
pub max_outputs: usize,
}
pub fn send(
@@ -223,7 +222,6 @@ pub fn send(
None,
args.amount,
args.minimum_confirmations,
args.max_outputs,
args.change_outputs,
strategy == "all",
)
@@ -237,7 +235,6 @@ pub fn send(
None,
args.amount,
args.minimum_confirmations,
args.max_outputs,
args.change_outputs,
args.selection_strategy == "all",
args.message.clone(),
-1
View File
@@ -325,7 +325,6 @@ where
None,
args.amount,
args.minimum_confirmations,
args.max_outputs,
args.num_change_outputs,
args.selection_strategy_is_use_all,
args.message,
+2 -17
View File
@@ -535,15 +535,10 @@ where
/// * `amount` - The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`)
/// * `minimum_confirmations` - The minimum number of confirmations an output
/// should have in order to be included in the transaction.
/// * `max_outputs` - By default, the wallet selects as many inputs as possible in a
/// transaction, to reduce the Output set and the fees. The wallet will attempt to spend
/// include up to `max_outputs` in a transaction, however if this is not enough to cover
/// the whole amount, the wallet will include more outputs. This parameter should be considered
/// a soft limit.
/// * `num_change_outputs` - The target number of change outputs to create in the transaction.
/// The actual number created will be `num_change_outputs` + whatever remainder is needed.
/// * `selection_strategy_is_use_all` - If `true`, attempt to use up as many outputs as
/// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps
/// possible to create the transaction. This helps
/// to reduce the size of the UTXO set and the amount of data stored in the wallet, and
/// minimizes fees. This will generally result in many inputs and a large change output(s),
/// usually much larger than the amount being sent. If `false`, the transaction will include
@@ -600,7 +595,6 @@ where
/// None,
/// amount, // amount
/// 10, // minimum confirmations
/// 500, // max outputs
/// 1, // num change outputs
/// true, // select all outputs
/// Some("Have some Grins. Love, Yeastplume".to_owned()),
@@ -619,7 +613,6 @@ where
src_acct_name: Option<&str>,
amount: u64,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
message: Option<String>,
@@ -651,7 +644,6 @@ where
&mut *w,
&mut slate,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
@@ -681,15 +673,10 @@ where
/// * `amount` - The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`)
/// * `minimum_confirmations` - The minimum number of confirmations an output
/// should have in order to be included in the transaction.
/// * `max_outputs` - By default, the wallet selects as many inputs as possible in a
/// transaction, to reduce the Output set and the fees. The wallet will attempt to spend
/// include up to `max_outputs` in a transaction, however if this is not enough to cover
/// the whole amount, the wallet will include more outputs. This parameter should be considered
/// a soft limit.
/// * `num_change_outputs` - The target number of change outputs to create in the transaction.
/// The actual number created will be `num_change_outputs` + whatever remainder is needed.
/// * `selection_strategy_is_use_all` - If `true`, attempt to use up as many outputs as
/// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps
/// possible to estimate the transaction. This helps
/// to reduce the size of the UTXO set and the amount of data stored in the wallet, and
/// minimizes fees. This will generally result in many inputs and a large change output(s),
/// usually much larger than the amount being sent. If `false`, the transaction will include
@@ -706,7 +693,6 @@ where
src_acct_name: Option<&str>,
amount: u64,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
) -> Result<
@@ -732,7 +718,6 @@ where
&mut *w,
amount,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
+19 -32
View File
@@ -16,11 +16,13 @@
use crate::core::core::{amount_to_hr_string, Transaction};
use crate::core::libtx::{build, tx_fee};
use crate::core::{consensus, global};
use crate::keychain::{Identifier, Keychain};
use crate::libwallet::error::{Error, ErrorKind};
use crate::libwallet::internal::keys;
use crate::libwallet::slate::Slate;
use crate::libwallet::types::*;
use std::cmp::min;
use std::collections::HashMap;
use std::marker::PhantomData;
@@ -33,7 +35,6 @@ pub fn build_send_tx<T: ?Sized, C, K>(
wallet: &mut T,
slate: &mut Slate,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: Identifier,
@@ -49,7 +50,6 @@ where
slate.height,
minimum_confirmations,
slate.lock_height,
max_outputs,
change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
@@ -220,6 +220,16 @@ where
Ok((key_id, context, Box::new(wallet_add_fn)))
}
/// Calculate maximal amount of inputs in transaction given amount of outputs
fn calculate_max_inputs_in_block(num_outputs: usize) -> usize {
let coinbase_weight = consensus::BLOCK_OUTPUT_WEIGHT + consensus::BLOCK_KERNEL_WEIGHT;
global::max_block_weight().saturating_sub(
coinbase_weight
+ consensus::BLOCK_OUTPUT_WEIGHT.saturating_mul(num_outputs)
+ consensus::BLOCK_KERNEL_WEIGHT,
) / consensus::BLOCK_INPUT_WEIGHT
}
/// Builds a transaction to send to someone from the HD seed associated with the
/// wallet and the amount to send. Handles reading through the wallet data file,
/// selecting outputs to spend and building the change.
@@ -229,7 +239,6 @@ pub fn select_send_tx<T: ?Sized, C, K>(
current_height: u64,
minimum_confirmations: u64,
lock_height: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
@@ -252,7 +261,6 @@ where
amount,
current_height,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
@@ -275,7 +283,6 @@ pub fn select_coins_and_fee<T: ?Sized, C, K>(
amount: u64,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
@@ -299,7 +306,7 @@ where
amount,
current_height,
minimum_confirmations,
max_outputs,
calculate_max_inputs_in_block(change_outputs),
selection_strategy_is_use_all,
parent_key_id,
);
@@ -361,7 +368,7 @@ where
amount_with_fee,
current_height,
minimum_confirmations,
max_outputs,
calculate_max_inputs_in_block(num_outputs),
selection_strategy_is_use_all,
parent_key_id,
)
@@ -476,40 +483,20 @@ where
})
.collect::<Vec<OutputData>>();
let max_available = eligible.len();
// max_available can not be bigger than max_outputs
let max_available = min(eligible.len(), max_outputs);
// sort eligible outputs by increasing value
eligible.sort_by_key(|out| out.value);
// use a sliding window to identify potential sets of possible outputs to spend
// Case of amount > total amount of max_outputs(500):
// The limit exists because by default, we always select as many inputs as
// possible in a transaction, to reduce both the Output set and the fees.
// But that only makes sense up to a point, hence the limit to avoid being too
// greedy. But if max_outputs(500) is actually not enough to cover the whole
// amount, the wallet should allow going over it to satisfy what the user
// wants to send. So the wallet considers max_outputs more of a soft limit.
if eligible.len() > max_outputs {
for window in eligible.windows(max_outputs) {
if max_available > 0 {
for window in eligible.windows(max_available) {
let windowed_eligibles = window.iter().cloned().collect::<Vec<_>>();
if let Some(outputs) = select_from(amount, select_all, windowed_eligibles) {
return (max_available, outputs);
}
}
// Not exist in any window of which total amount >= amount.
// Then take coins from the smallest one up to the total amount of selected
// coins = the amount.
if let Some(outputs) = select_from(amount, false, eligible.clone()) {
debug!(
"Extending maximum number of outputs. {} outputs selected.",
outputs.len()
);
return (max_available, outputs);
}
} else {
if let Some(outputs) = select_from(amount, select_all, eligible.clone()) {
return (max_available, outputs);
}
}
// we failed to find a suitable set of outputs to spend,
@@ -518,7 +505,7 @@ where
eligible.reverse();
(
max_available,
eligible.iter().take(max_outputs).cloned().collect(),
eligible.iter().take(max_available).cloned().collect(),
)
}
-4
View File
@@ -47,7 +47,6 @@ pub fn estimate_send_tx<T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
@@ -80,7 +79,6 @@ where
amount,
current_height,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
parent_key_id,
@@ -93,7 +91,6 @@ pub fn add_inputs_to_slate<T: ?Sized, C, K>(
wallet: &mut T,
slate: &mut Slate,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
@@ -119,7 +116,6 @@ where
wallet,
slate,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
parent_key_id.clone(),
-2
View File
@@ -710,8 +710,6 @@ pub struct SendTXArgs {
pub method: String,
/// destination url
pub dest: String,
/// Max number of outputs
pub max_outputs: usize,
/// Number of change outputs to generate
pub num_change_outputs: usize,
/// whether to use all outputs (combine)
-1
View File
@@ -198,7 +198,6 @@ where
None, // account
amount, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
-1
View File
@@ -180,7 +180,6 @@ fn accounts_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
let (mut slate, lock_fn) = api.initiate_tx(
None, reward, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
-1
View File
@@ -158,7 +158,6 @@ fn check_repair_impl(test_dir: &str) -> Result<(), libwallet::Error> {
None,
reward * 2, // amount
cm, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None, // optional message
-1
View File
@@ -105,7 +105,6 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
Some("mining"),
reward * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
Some(message.to_owned()), // optional message
-2
View File
@@ -104,7 +104,6 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
Some("mining"),
reward * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
@@ -200,7 +199,6 @@ fn file_repost_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
None,
amount * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
-4
View File
@@ -237,7 +237,6 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
let (slate_i, lock_fn) = sender_api.initiate_tx(
None, amount, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
@@ -259,7 +258,6 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
None,
amount * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
@@ -281,7 +279,6 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
None,
amount * 3, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
@@ -309,7 +306,6 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
None,
amount * 3, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
-1
View File
@@ -88,7 +88,6 @@ fn self_send_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
Some("mining"),
reward * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
+18 -5
View File
@@ -99,7 +99,6 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
let (slate_i, lock_fn) = sender_api.initiate_tx(
None, amount, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
@@ -232,7 +231,6 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
None,
amount * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
)?;
@@ -243,7 +241,6 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
None,
amount * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
false, // select the smallest amount of outputs
)?;
@@ -261,7 +258,6 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
None,
amount * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,
@@ -312,6 +308,24 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
Ok(())
})?;
// Initiate transaction with weight more that
// core::global::max_block_weight() should fail
let invalid_amount_of_outputs =
core::global::max_block_weight() / core::consensus::BLOCK_OUTPUT_WEIGHT + 1;
let res = wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
// note this will increment the block count as part of the transaction "posting"
sender_api.initiate_tx(
None,
amount, // amount
2, // minimum confirmations
invalid_amount_of_outputs, // num change outputs
true, // select all outputs
None,
)?;
Ok(())
});
assert!(res.is_err());
// let logging finish
thread::sleep(Duration::from_millis(200));
Ok(())
@@ -358,7 +372,6 @@ fn tx_rollback(test_dir: &str) -> Result<(), libwallet::Error> {
let (slate_i, lock_fn) = sender_api.initiate_tx(
None, amount, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
None,