Skip to content

Commit 3b4bd99

Browse files
author
bay
committed
Handle edge case with few extremly small outputs
1 parent 9f8308f commit 3b4bd99

File tree

2 files changed

+189
-4
lines changed

2 files changed

+189
-4
lines changed

controller/tests/broken_change.rs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright 2019 The Grin Developers
2+
// Copyright 2024 The Mwc Developers
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Test sender transaction with no change output
16+
#[macro_use]
17+
extern crate log;
18+
extern crate mwc_wallet_controller as wallet;
19+
extern crate mwc_wallet_impls as impls;
20+
21+
use mwc_wallet_util::mwc_core as core;
22+
use mwc_wallet_util::mwc_core::global;
23+
24+
use impls::test_framework::{self, LocalWalletClient};
25+
use libwallet::{InitTxArgs, IssueInvoiceTxArgs, Slate};
26+
use mwc_wallet_libwallet as libwallet;
27+
use std::sync::atomic::Ordering;
28+
use std::thread;
29+
use std::time::Duration;
30+
31+
#[macro_use]
32+
mod common;
33+
use common::{clean_output_dir, create_wallet_proxy, setup};
34+
35+
fn broken_change_test_impl(
36+
test_dir: &str,
37+
put_into_change: u64,
38+
expected_outputs: usize,
39+
test_send: bool,
40+
) -> Result<(), wallet::Error> {
41+
global::set_local_chain_type(global::ChainTypes::AutomatedTesting);
42+
let mut wallet_proxy = create_wallet_proxy(test_dir.into());
43+
let chain = wallet_proxy.chain.clone();
44+
let stopper = wallet_proxy.running.clone();
45+
46+
create_wallet_and_add!(
47+
client1,
48+
wallet1,
49+
mask1_i,
50+
test_dir,
51+
"wallet1",
52+
None,
53+
&mut wallet_proxy,
54+
false
55+
);
56+
57+
let mask1 = (&mask1_i).as_ref();
58+
59+
create_wallet_and_add!(
60+
client2,
61+
wallet2,
62+
mask2_i,
63+
test_dir,
64+
"wallet2",
65+
None,
66+
&mut wallet_proxy,
67+
false
68+
);
69+
70+
let mask2 = (&mask2_i).as_ref();
71+
72+
// Set the wallet proxy listener running
73+
thread::spawn(move || {
74+
global::set_local_chain_type(global::ChainTypes::AutomatedTesting);
75+
if let Err(e) = wallet_proxy.run() {
76+
error!("Wallet Proxy error: {}", e);
77+
}
78+
});
79+
80+
// few values to keep things shorter
81+
let reward = core::consensus::reward(0, 1);
82+
83+
let inputs_num = 4;
84+
let output_num = 5;
85+
86+
// Mine into wallet 1
87+
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 4 + 3, false);
88+
let fee = core::libtx::tx_fee(inputs_num, output_num + 1, 1);
89+
90+
// send a single block's worth of transactions with minimal strategy
91+
let mut slate = Slate::blank(2, false);
92+
if test_send {
93+
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
94+
let args = InitTxArgs {
95+
src_acct_name: None,
96+
amount: reward * inputs_num as u64 - fee - put_into_change,
97+
minimum_confirmations: 2,
98+
max_outputs: 500,
99+
num_change_outputs: output_num as u32,
100+
selection_strategy_is_use_all: false,
101+
..Default::default()
102+
};
103+
slate = api.init_send_tx(m, &args, 1)?;
104+
slate = client1.send_tx_slate_direct("wallet2", &slate)?;
105+
api.tx_lock_outputs(m, &slate, None, 0)?;
106+
slate = api.finalize_tx(m, &slate)?;
107+
assert!(slate.tx.clone().unwrap().body.inputs.len() == inputs_num);
108+
assert!(slate.tx.clone().unwrap().body.outputs.len() == expected_outputs);
109+
api.post_tx(m, slate.tx_or_err()?, false)?;
110+
Ok(())
111+
})?;
112+
} else {
113+
// testing invoice
114+
wallet::controller::owner_single_use(Some(wallet2.clone()), mask2, None, |api, m| {
115+
// Wallet 2 inititates an invoice transaction, requesting payment
116+
let args = IssueInvoiceTxArgs {
117+
amount: reward * inputs_num as u64 - fee - put_into_change,
118+
..Default::default()
119+
};
120+
slate = api.issue_invoice_tx(m, &args)?;
121+
Ok(())
122+
})?;
123+
124+
wallet::controller::owner_single_use(Some(wallet1.clone()), mask1, None, |api, m| {
125+
// Wallet 1 receives the invoice transaction
126+
let args = InitTxArgs {
127+
src_acct_name: None,
128+
amount: slate.amount,
129+
minimum_confirmations: 2,
130+
max_outputs: 500,
131+
num_change_outputs: output_num as u32,
132+
selection_strategy_is_use_all: false,
133+
..Default::default()
134+
};
135+
slate = api.process_invoice_tx(m, &slate, &args)?;
136+
api.tx_lock_outputs(m, &slate, None, 1)?;
137+
assert!(slate.tx.clone().unwrap().body.inputs.len() == inputs_num);
138+
assert!(slate.tx.clone().unwrap().body.outputs.len() == expected_outputs);
139+
Ok(())
140+
})?;
141+
}
142+
143+
// let logging finish
144+
stopper.store(false, Ordering::Relaxed);
145+
thread::sleep(Duration::from_millis(200));
146+
Ok(())
147+
}
148+
149+
fn run_broken_change_test(
150+
test_dir: &str,
151+
put_into_change: u64,
152+
expected_outputs: usize,
153+
test_send: bool,
154+
) {
155+
setup(test_dir);
156+
if let Err(e) = broken_change_test_impl(&test_dir, put_into_change, expected_outputs, test_send)
157+
{
158+
panic!("Libwallet Error: {}", e);
159+
}
160+
clean_output_dir(test_dir);
161+
}
162+
163+
#[test]
164+
fn broken_change() {
165+
run_broken_change_test("test_output/broken_change1", 0, 1, true);
166+
run_broken_change_test("test_output/broken_change2", 0, 1, false);
167+
run_broken_change_test("test_output/broken_change3", 7, 2, true);
168+
run_broken_change_test("test_output/broken_change4", 100, 2, false);
169+
run_broken_change_test("test_output/broken_change5", 500_000_000, 6, true); // 0.5 MWC for 1 outputs is good
170+
run_broken_change_test("test_output/broken_change6", 499_999_999, 2, false); // 0.4999999 MWC for 5 outputs is below the threshold, expected to use 1 output instead of 5.
171+
}

libwallet/src/internal/selection.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -661,16 +661,17 @@ where
661661
false => amount + fee,
662662
};
663663

664-
// We need to add a change address or amount with fee is more than total
664+
// If change required...
665665
if total != amount_with_fee {
666666
// reseting to trigger first retry cycle
667667
total = 0;
668668

669669
let num_outputs = change_outputs + routputs;
670-
fee = calc_fees(coins.len(), num_outputs, min_fee, &fixed_fee)?;
670+
671+
// Here fixed fee ignored with purpose, it is not a case here, fixed can be broken
671672
amount_with_fee = match amount_includes_fee {
672673
true => amount,
673-
false => amount + fee,
674+
false => amount + calc_fees(coins.len(), num_outputs, min_fee, &None)?,
674675
};
675676

676677
let (optimal_outputs_num, min_outputs_num) =
@@ -706,14 +707,15 @@ where
706707
fee = calc_fees(coins.len(), num_outputs, min_fee, &fixed_fee)?;
707708

708709
total = coins.iter().map(|c| c.value).sum();
710+
let prev_amount_with_fee = amount_with_fee;
709711
amount_with_fee = match amount_includes_fee {
710712
true => amount,
711713
false => amount + fee,
712714
};
713715

714716
// Checking if new solution is better (has more outputs)
715717
// Don't checking outputs limit because light overcounting is fine
716-
if coins.len() <= coins_len {
718+
if coins.len() <= coins_len && prev_amount_with_fee >= amount_with_fee {
717719
break;
718720
}
719721
}
@@ -786,6 +788,18 @@ where
786788
if change == 0 {
787789
debug!("No change (sending exactly amount + fee), no change outputs to build");
788790
} else {
791+
// we don't want part_change to be zero. Also we really don't want many small change outputs
792+
// If change less then 0.1 MWC, let's make it in one output. 1-in 2-out Tx fee will is: 4*2+1-1 = 0.008 MWC
793+
// We don't want many small outputs like that
794+
let num_change_outputs = if num_change_outputs > 1
795+
&& change / (num_change_outputs as u64) < 100_000_000
796+
{
797+
warn!("Transaction requested has {} change outputs with change amount {}. We don't want create many small outputs, the number of change outputs is reduced to 1", num_change_outputs, change);
798+
1
799+
} else {
800+
num_change_outputs
801+
};
802+
789803
debug!(
790804
"Building change outputs: total change: {} ({} outputs)",
791805
change, num_change_outputs

0 commit comments

Comments
 (0)