Skip to content

Commit 23b1cb5

Browse files
committed
Merge #35: Fix: allow zero-fee transactions in LowestFee metric
67a8698 fix: allow zero-fee transactions in LowestFee metric (志宇) 92adec1 test: Upgrade `rand` and `proptest` (志宇) 575964f chore: make clippy happy (志宇) Pull request description: Change assertion from fee > 0 to fee >= 0 to handle valid zero-fee transactions. Add test case to verify the metric works correctly when target fee rate is zero. Also make clippy happy and upgrade test dependencies. Top commit has no ACKs. Tree-SHA512: 0bbfa373f82d8f67a463d579eaa981b09ec7eae0f876d447b88421d9e6c18bf6bf4ecff2a17940eb07413cbf33475d07a8d6d543a046a617f883755fa741c238
2 parents dda8638 + 67a8698 commit 23b1cb5

7 files changed

Lines changed: 76 additions & 21 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ default = ["std"]
2121
std = []
2222

2323
[dev-dependencies]
24-
rand = "0.8"
25-
proptest = "1.4"
24+
rand = "0.9"
25+
proptest = "1.7"
2626
bitcoin = "0.32"

src/coin_selector.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use super::*;
22
#[allow(unused)] // some bug in <= 1.48.0 sees this as unused when it isn't
33
use crate::float::FloatExt;
44
use crate::{bnb::BnbMetric, float::Ordf32, ChangePolicy, FeeRate, Target};
5-
use alloc::{borrow::Cow, collections::BTreeSet, vec::Vec};
5+
use alloc::{borrow::Cow, collections::BTreeSet};
66

77
/// [`CoinSelector`] selects/deselects coins from a set of canididate coins.
88
///
@@ -16,7 +16,7 @@ pub struct CoinSelector<'a> {
1616
candidates: &'a [Candidate],
1717
selected: Cow<'a, BTreeSet<usize>>,
1818
banned: Cow<'a, BTreeSet<usize>>,
19-
candidate_order: Cow<'a, Vec<usize>>,
19+
candidate_order: Cow<'a, [usize]>,
2020
}
2121

2222
impl<'a> CoinSelector<'a> {
@@ -578,6 +578,8 @@ impl<'a> CoinSelector<'a> {
578578
}
579579
}
580580

581+
// Allow this for now due to MSRV
582+
#[allow(clippy::uninlined_format_args)]
581583
impl core::fmt::Display for CoinSelector<'_> {
582584
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
583585
write!(f, "[")?;

src/metrics/lowest_fee.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ impl BnbMetric for LowestFee {
3232
let drain = cs.drain(self.target, self.change_policy);
3333
let fee_for_the_tx = cs.fee(self.target.value(), drain.value);
3434
assert!(
35-
fee_for_the_tx > 0,
36-
"must not be called unless selection has met target"
35+
fee_for_the_tx >= 0,
36+
"must not be called unless selection has met target: fee={}",
37+
fee_for_the_tx
3738
);
3839
// `spend_fee` rounds up here. We could use floats but I felt it was just better to
3940
// accept the extra 1 sat penality to having a change output

tests/bnb.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ use proptest::{prelude::*, proptest, test_runner::*};
1010

1111
fn test_wv(mut rng: impl RngCore) -> impl Iterator<Item = Candidate> {
1212
core::iter::repeat_with(move || {
13-
let value = rng.gen_range(0..1_000);
13+
let value = rng.random_range(0..1_000);
1414
let mut candidate = Candidate {
1515
value,
1616
weight: 100,
17-
input_count: rng.gen_range(1..2),
18-
is_segwit: rng.gen_bool(0.5),
17+
input_count: rng.random_range(1..2),
18+
is_segwit: rng.random_bool(0.5),
1919
};
2020
// HACK: set is_segwit = true for all these tests because you can't actually lower bound
2121
// things easily with how segwit inputs interfere with their weights. We can't modify the
@@ -100,7 +100,7 @@ fn bnb_finds_an_exact_solution_in_n_iter() {
100100
.last()
101101
.expect("it found a solution");
102102

103-
assert_eq!(rounds, 3150);
103+
assert_eq!(rounds, 3194);
104104
assert_eq!(best.input_weight(), solution_weight);
105105
assert_eq!(best.selected_value(), target_value, "score={:?}", score);
106106
}
@@ -134,9 +134,9 @@ fn bnb_finds_solution_if_possible_in_n_iter() {
134134
.last()
135135
.expect("found a solution");
136136

137-
assert_eq!(rounds, 193);
137+
assert_eq!(rounds, 164);
138138
let excess = sol.excess(target, Drain::NONE);
139-
assert_eq!(excess, 1);
139+
assert_eq!(excess, 0);
140140
}
141141

142142
proptest! {

tests/changeless.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ use rand::{prelude::IteratorRandom, Rng, RngCore};
1111

1212
fn test_wv(mut rng: impl RngCore) -> impl Iterator<Item = Candidate> {
1313
core::iter::repeat_with(move || {
14-
let value = rng.gen_range(0..1_000);
14+
let value = rng.random_range(0..1_000);
1515
Candidate {
1616
value,
17-
weight: rng.gen_range(0..100),
18-
input_count: rng.gen_range(1..2),
17+
weight: rng.random_range(0..100),
18+
input_count: rng.random_range(1..2),
1919
is_segwit: false,
2020
}
2121
})

tests/common.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,10 @@ impl StrategyParams {
243243
pub fn gen_candidates(n: usize) -> Vec<Candidate> {
244244
let mut rng = TestRng::deterministic_rng(RngAlgorithm::ChaCha);
245245
core::iter::repeat_with(move || {
246-
let value = rng.gen_range(1..500_001);
247-
let weight = rng.gen_range(1..2001);
248-
let input_count = rng.gen_range(1..3);
249-
let is_segwit = rng.gen_bool(0.01);
246+
let value = rng.random_range(1..500_001);
247+
let weight = rng.random_range(1..2001);
248+
let input_count = rng.random_range(1..3);
249+
let is_segwit = rng.random_bool(0.01);
250250

251251
Candidate {
252252
value,
@@ -465,11 +465,11 @@ pub fn compare_against_benchmarks<M: BnbMetric + Clone>(
465465
}
466466

467467
#[allow(unused)]
468-
fn randomly_satisfy_target<'a>(
468+
fn randomly_satisfy_target<'a, R: rand::Rng>(
469469
cs: &CoinSelector<'a>,
470470
target: Target,
471471
change_policy: ChangePolicy,
472-
rng: &mut impl RngCore,
472+
rng: &mut R,
473473
mut metric: impl BnbMetric,
474474
) -> CoinSelector<'a> {
475475
let mut cs = cs.clone();

tests/lowest_fee.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,55 @@ fn adding_another_input_to_remove_change() {
267267
assert!(score <= best_solution_score);
268268
assert_eq!(cs.selected_indices(), best_solution.selected_indices());
269269
}
270+
271+
#[test]
272+
fn zero_fee_tx() {
273+
let target_feerate = FeeRate::ZERO;
274+
let long_term_feerate = FeeRate::DEFAULT_MIN_RELAY;
275+
276+
let target = Target {
277+
fee: TargetFee {
278+
rate: target_feerate,
279+
replace: None,
280+
},
281+
outputs: TargetOutputs {
282+
value_sum: 99_870,
283+
weight_sum: 200 - TX_FIXED_FIELD_WEIGHT - 1,
284+
n_outputs: 1,
285+
},
286+
};
287+
288+
let candidates = vec![
289+
Candidate {
290+
value: 100_000,
291+
weight: 100,
292+
input_count: 1,
293+
is_segwit: true,
294+
},
295+
Candidate {
296+
value: 50_000,
297+
weight: 100,
298+
input_count: 1,
299+
is_segwit: true,
300+
},
301+
];
302+
303+
let drain_weights = DrainWeights {
304+
output_weight: 100,
305+
spend_weight: 1_000,
306+
n_outputs: 1,
307+
};
308+
309+
let mut cs = CoinSelector::new(&candidates);
310+
let metric = LowestFee {
311+
target,
312+
long_term_feerate,
313+
change_policy: ChangePolicy::min_value_and_waste(
314+
drain_weights,
315+
50,
316+
target_feerate,
317+
long_term_feerate,
318+
),
319+
};
320+
let (_score, _rounds) = common::bnb_search(&mut cs, metric, 1000).expect("must find solution");
321+
}

0 commit comments

Comments
 (0)