Skip to content

Commit 12f036c

Browse files
committed
WIP: Do RBF properly as according to bitcoin core policy
1 parent 8e6e69d commit 12f036c

File tree

6 files changed

+565
-193
lines changed

6 files changed

+565
-193
lines changed

src/coin_control.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,22 @@ impl<'g, C: ChainOracle<Error = Infallible>> CoinControl<'g, C> {
8282
let mut canonical = tx_graph
8383
.canonical_iter(chain, chain_tip)
8484
.map(|r| r.expect("infallible"))
85-
.map(|(txid, _, _)| txid)
85+
.flat_map(|(txid, tx, _)| {
86+
tx.input
87+
.iter()
88+
.map(|txin| txin.previous_output.txid)
89+
.chain([txid])
90+
.collect::<Vec<_>>()
91+
})
8692
.collect::<HashSet<Txid>>();
8793
let exclude = replace
8894
.into_iter()
8995
.filter_map(|txid| tx_graph.get_tx(txid))
9096
.flat_map(|tx| {
97+
let txid = tx.compute_txid();
9198
tx_graph
92-
.walk_conflicts(&tx, move |_, txid| Some(txid))
99+
.walk_descendants(txid, move |_, txid| Some(txid))
100+
.chain(core::iter::once(txid))
93101
.collect::<Vec<_>>()
94102
});
95103
for txid in exclude {
@@ -134,7 +142,7 @@ impl<'g, C: ChainOracle<Error = Infallible>> CoinControl<'g, C> {
134142
.tx_graph
135143
.get_tx_node(outpoint.txid)
136144
.ok_or(ExcludeInputReason::DoesNotExist)?;
137-
if self.canonical.contains(&tx_node.txid) {
145+
if !self.canonical.contains(&tx_node.txid) {
138146
return Err(ExcludeInputReason::NotCanonical);
139147
}
140148
if self.is_spent(outpoint) {

src/input.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ impl Input {
309309
false
310310
}
311311

312+
/// Confirmations of this tx.
313+
pub fn confirmations(&self, tip_height: absolute::Height) -> u32 {
314+
self.status.map_or(0, |status| {
315+
tip_height
316+
.to_consensus_u32()
317+
.saturating_sub(status.height.to_consensus_u32().saturating_sub(1))
318+
})
319+
}
320+
312321
/// Whether this output can be spent now.
313322
pub fn is_spendable_now(&self, tip_height: absolute::Height, tip_time: absolute::Time) -> bool {
314323
!self.is_immature(tip_height) && !self.is_timelocked(tip_height, tip_time)
@@ -405,6 +414,15 @@ impl InputGroup {
405414
.all(|input| input.is_spendable_now(tip_height, tip_time))
406415
}
407416

417+
/// Returns the tx confirmation count this is the smallest in this group.
418+
pub fn min_confirmations(&self, tip_height: absolute::Height) -> u32 {
419+
self.inputs()
420+
.iter()
421+
.map(|input| input.confirmations(tip_height))
422+
.min()
423+
.expect("group must not be empty")
424+
}
425+
408426
/// Total value of all contained inputs.
409427
pub fn value(&self) -> Amount {
410428
self.inputs().iter().map(|input| input.txout.value).sum()

src/input_candidates.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
use crate::collections::HashSet;
12
use alloc::vec::Vec;
23
use bdk_coin_select::Candidate;
4+
use bitcoin::OutPoint;
5+
use miniscript::bitcoin;
36

47
use crate::InputGroup;
58

69
/// Candidates ready for coin selection.
10+
#[must_use]
711
#[derive(Debug, Clone)]
812
pub struct InputCandidates {
913
must_select_count: usize,
@@ -40,6 +44,62 @@ impl InputCandidates {
4044
}
4145
}
4246

47+
/// Try create a new [`InputCandidates`] with the provided `outpoints` as "must spend".
48+
///
49+
/// TODO: This can be optimized later. Right API first.
50+
///
51+
/// # Error
52+
///
53+
/// Returns the original [`InputCandidates`] if any outpoint is not found.
54+
pub fn with_must_select(self, outpoints: HashSet<OutPoint>) -> Result<Self, Self> {
55+
let (must_select, can_select) = self.groups.iter().partition::<Vec<_>, _>(|group| {
56+
group
57+
.inputs()
58+
.iter()
59+
.any(|input| outpoints.contains(&input.prev_outpoint()))
60+
});
61+
62+
// `must_select` must contaon all outpoints.
63+
let must_select_map = must_select
64+
.iter()
65+
.flat_map(|group| group.inputs().iter().map(|input| input.prev_outpoint()))
66+
.collect::<HashSet<OutPoint>>();
67+
if !must_select_map.is_superset(&outpoints) {
68+
return Err(self);
69+
}
70+
71+
let must_select_count = must_select.len();
72+
let groups = must_select
73+
.into_iter()
74+
.chain(can_select)
75+
.cloned()
76+
.collect::<Vec<_>>();
77+
let cs_candidates = groups
78+
.iter()
79+
.map(|group| Candidate {
80+
value: group.value().to_sat(),
81+
weight: group.weight(),
82+
input_count: group.input_count(),
83+
is_segwit: group.is_segwit(),
84+
})
85+
.collect();
86+
Ok(InputCandidates {
87+
must_select_count,
88+
groups,
89+
cs_candidates,
90+
})
91+
}
92+
93+
/// Whether the outpoint is contained in our candidates.
94+
pub fn contains(&self, outpoint: OutPoint) -> bool {
95+
self.groups.iter().any(|group| {
96+
group
97+
.inputs()
98+
.iter()
99+
.any(|input| input.prev_outpoint() == outpoint)
100+
})
101+
}
102+
43103
/// Iterate all groups (both must_select and can_select).
44104
pub fn iter(&self) -> impl ExactSizeIterator<Item = (InputGroup, Candidate)> + '_ {
45105
self.groups

src/selection.rs

Lines changed: 12 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
use core::fmt::{Debug, Display};
22
use std::vec::Vec;
33

4-
use bdk_coin_select::float::Ordf32;
5-
use bdk_coin_select::metrics::LowestFee;
6-
use bdk_coin_select::{
7-
ChangePolicy, CoinSelector, DrainWeights, FeeRate, NoBnbSolution, Target, TargetFee,
8-
TargetOutputs,
9-
};
10-
use bitcoin::{absolute, transaction, Amount, TxOut};
4+
use bdk_coin_select::FeeRate;
5+
use bitcoin::{absolute, transaction};
116
use miniscript::bitcoin;
127
use miniscript::psbt::PsbtExt;
138

14-
use crate::{DefiniteDescriptor, Finalizer, Input, InputCandidates, Output};
9+
use crate::{Finalizer, Input, Output};
1510

1611
const FALLBACK_SEQUENCE: bitcoin::Sequence = bitcoin::Sequence::ENABLE_LOCKTIME_NO_RBF;
1712

13+
pub(crate) fn cs_feerate(feerate: bitcoin::FeeRate) -> bdk_coin_select::FeeRate {
14+
FeeRate::from_sat_per_wu(feerate.to_sat_per_kwu() as f32 / 1000.0)
15+
}
16+
1817
/// Final selection of inputs and outputs.
1918
#[derive(Debug, Clone)]
2019
pub struct Selection {
@@ -24,45 +23,6 @@ pub struct Selection {
2423
pub outputs: Vec<Output>,
2524
}
2625

27-
/// Parameters for creating tx.
28-
#[derive(Debug, Clone)]
29-
pub struct SelectionParams {
30-
/// To derive change output.
31-
///
32-
/// Will error if this is unsatisfiable descriptor.
33-
pub change_descriptor: DefiniteDescriptor,
34-
35-
/// Feerate target!
36-
pub target_feerate: bitcoin::FeeRate,
37-
38-
/// Uses `target_feerate` as a fallback.
39-
pub long_term_feerate: Option<bitcoin::FeeRate>,
40-
41-
/// Outputs that must be included.
42-
pub target_outputs: Vec<Output>,
43-
44-
/// Max rounds of branch-and-bound.
45-
/// TODO: Remove.
46-
pub max_rounds: usize,
47-
}
48-
49-
impl SelectionParams {
50-
/// With default params.
51-
pub fn new(
52-
change_descriptor: DefiniteDescriptor,
53-
target_outputs: Vec<Output>,
54-
target_feerate: bitcoin::FeeRate,
55-
) -> Self {
56-
Self {
57-
change_descriptor,
58-
target_feerate,
59-
long_term_feerate: None,
60-
target_outputs,
61-
max_rounds: 100_000,
62-
}
63-
}
64-
}
65-
6626
/// Parameters for creating a psbt.
6727
#[derive(Debug, Clone)]
6828
pub struct PsbtParams {
@@ -76,6 +36,9 @@ pub struct PsbtParams {
7636
/// It is best practive to set this to the latest block height to avoid fee sniping.
7737
pub fallback_locktime: absolute::LockTime,
7838

39+
/// Yes.
40+
pub fallback_sequence: bitcoin::Sequence,
41+
7942
/// Recommended.
8043
pub mandate_full_tx_for_segwit_v0: bool,
8144
}
@@ -85,40 +48,12 @@ impl Default for PsbtParams {
8548
Self {
8649
version: transaction::Version::TWO,
8750
fallback_locktime: absolute::LockTime::ZERO,
51+
fallback_sequence: FALLBACK_SEQUENCE,
8852
mandate_full_tx_for_segwit_v0: true,
8953
}
9054
}
9155
}
9256

93-
/// Selection Metrics.
94-
pub struct SelectionMetrics {
95-
/// Selection score.
96-
pub score: Ordf32,
97-
/// Whether there is a change output in this selection.
98-
pub has_change: bool,
99-
}
100-
101-
/// When create_tx fails.
102-
#[derive(Debug)]
103-
pub enum SelectionError {
104-
/// No solution.
105-
NoSolution(NoBnbSolution),
106-
/// Cannot satisfy change descriptor.
107-
CannotSatisfyChangeDescriptor(miniscript::Error),
108-
}
109-
110-
impl Display for SelectionError {
111-
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112-
match self {
113-
SelectionError::NoSolution(no_bnb_solution) => Display::fmt(&no_bnb_solution, f),
114-
SelectionError::CannotSatisfyChangeDescriptor(error) => Display::fmt(&error, f),
115-
}
116-
}
117-
}
118-
119-
#[cfg(feature = "std")]
120-
impl std::error::Error for SelectionError {}
121-
12257
/// Occurs when creating a psbt fails.
12358
#[derive(Debug)]
12459
pub enum CreatePsbtError {
@@ -160,100 +95,6 @@ impl core::fmt::Display for CreatePsbtError {
16095
impl std::error::Error for CreatePsbtError {}
16196

16297
impl Selection {
163-
/// Constructor
164-
pub fn new(
165-
candidates: InputCandidates,
166-
params: SelectionParams,
167-
) -> Result<(Self, SelectionMetrics), SelectionError> {
168-
fn convert_feerate(feerate: bitcoin::FeeRate) -> bdk_coin_select::FeeRate {
169-
FeeRate::from_sat_per_wu(feerate.to_sat_per_kwu() as f32 / 1000.0)
170-
}
171-
172-
let cs_candidates = candidates.coin_select_candidates();
173-
174-
let target_feerate = convert_feerate(params.target_feerate);
175-
let long_term_feerate =
176-
convert_feerate(params.long_term_feerate.unwrap_or(params.target_feerate));
177-
println!("target_feerate: {} sats/vb", target_feerate.as_sat_vb());
178-
179-
let target = Target {
180-
fee: TargetFee::from_feerate(target_feerate),
181-
outputs: TargetOutputs::fund_outputs(
182-
params
183-
.target_outputs
184-
.iter()
185-
.map(|output| (output.txout().weight().to_wu(), output.value.to_sat())),
186-
),
187-
};
188-
189-
let change_policy = ChangePolicy::min_value_and_waste(
190-
DrainWeights {
191-
output_weight: (TxOut {
192-
script_pubkey: params.change_descriptor.script_pubkey(),
193-
value: Amount::ZERO,
194-
})
195-
.weight()
196-
.to_wu(),
197-
spend_weight: params
198-
.change_descriptor
199-
.max_weight_to_satisfy()
200-
.map_err(SelectionError::CannotSatisfyChangeDescriptor)?
201-
.to_wu(),
202-
n_outputs: 1,
203-
},
204-
params
205-
.change_descriptor
206-
.script_pubkey()
207-
.minimal_non_dust()
208-
.to_sat(),
209-
target_feerate,
210-
long_term_feerate,
211-
);
212-
213-
let bnb_metric = LowestFee {
214-
target,
215-
long_term_feerate,
216-
change_policy,
217-
};
218-
219-
let mut selector = CoinSelector::new(cs_candidates);
220-
221-
// Select input candidates that must be spent.
222-
for index in 0..candidates.must_select_len() {
223-
selector.select(index);
224-
}
225-
226-
// We assume that this still works if the current selection is already a solution.
227-
let score = selector
228-
.run_bnb(bnb_metric, params.max_rounds)
229-
.map_err(SelectionError::NoSolution)?;
230-
231-
let maybe_drain = selector.drain(target, change_policy);
232-
Ok((
233-
Selection {
234-
inputs: selector
235-
.apply_selection(candidates.groups())
236-
.flat_map(|group| group.inputs())
237-
.cloned()
238-
.collect::<Vec<Input>>(),
239-
outputs: {
240-
let mut outputs = params.target_outputs;
241-
if maybe_drain.is_some() {
242-
outputs.push(Output::with_descriptor(
243-
params.change_descriptor,
244-
Amount::from_sat(maybe_drain.value),
245-
));
246-
}
247-
outputs
248-
},
249-
},
250-
SelectionMetrics {
251-
score,
252-
has_change: maybe_drain.is_some(),
253-
},
254-
))
255-
}
256-
25798
/// Returns none if there is a mismatch of units in `locktimes`.
25899
///
259100
/// As according to BIP-64...
@@ -298,7 +139,7 @@ impl Selection {
298139
.map(|input| bitcoin::TxIn {
299140
previous_output: input.prev_outpoint(),
300141
// TODO: Custom fallback sequence?
301-
sequence: input.sequence().unwrap_or(FALLBACK_SEQUENCE),
142+
sequence: input.sequence().unwrap_or(params.fallback_sequence),
302143
..Default::default()
303144
})
304145
.collect(),

0 commit comments

Comments
 (0)