Skip to content

Commit 0f4b6c9

Browse files
committed
WIP: Make RBF easier to reason about
1 parent 12f036c commit 0f4b6c9

File tree

8 files changed

+254
-100
lines changed

8 files changed

+254
-100
lines changed

src/coin_control.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
};
77
use alloc::vec::Vec;
88
use bdk_chain::{BlockId, ChainOracle, ConfirmationBlockTime, TxGraph};
9-
use bitcoin::{absolute, OutPoint, Txid};
9+
use bitcoin::{absolute, OutPoint, Transaction, Txid};
1010
use miniscript::{bitcoin, plan::Plan};
1111

1212
/// Coin control.

src/input.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,22 @@ impl InputGroup {
423423
.expect("group must not be empty")
424424
}
425425

426+
/// Whether any contained input satisfies the predicate.
427+
pub fn any<F>(&self, f: F) -> bool
428+
where
429+
F: FnMut(&Input) -> bool,
430+
{
431+
self.inputs().iter().any(f)
432+
}
433+
434+
/// Whether all of the contained inputs satisfies the predicate.
435+
pub fn all<F>(&self, f: F) -> bool
436+
where
437+
F: FnMut(&Input) -> bool,
438+
{
439+
self.inputs().iter().all(f)
440+
}
441+
426442
/// Total value of all contained inputs.
427443
pub fn value(&self) -> Amount {
428444
self.inputs().iter().map(|input| input.txout.value).sum()

src/input_candidates.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,7 @@ impl InputCandidates {
5353
/// Returns the original [`InputCandidates`] if any outpoint is not found.
5454
pub fn with_must_select(self, outpoints: HashSet<OutPoint>) -> Result<Self, Self> {
5555
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()))
56+
group.any(|input| outpoints.contains(&input.prev_outpoint()))
6057
});
6158

6259
// `must_select` must contaon all outpoints.
@@ -90,6 +87,15 @@ impl InputCandidates {
9087
})
9188
}
9289

90+
/// Like [`InputCandidates::with_must_select`], but with a policy closure.
91+
pub fn with_must_select_policy<P>(self, mut policy: P) -> Result<Self, Self>
92+
where
93+
P: FnMut(&Self) -> HashSet<OutPoint>,
94+
{
95+
let outpoints = policy(&self);
96+
self.with_must_select(outpoints)
97+
}
98+
9399
/// Whether the outpoint is contained in our candidates.
94100
pub fn contains(&self, outpoint: OutPoint) -> bool {
95101
self.groups.iter().any(|group| {

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod finalizer;
1414
mod input; // TODO: Move into `bdk_tx_core`.
1515
mod input_candidates;
1616
mod output; // TODO: Move into `bdk_tx_core`.
17+
mod rbf;
1718
mod selection;
1819
mod selector;
1920
mod signer;
@@ -26,6 +27,7 @@ pub use miniscript;
2627
pub use miniscript::bitcoin;
2728
use miniscript::{DefiniteDescriptorKey, Descriptor};
2829
pub use output::*;
30+
pub use rbf::*;
2931
pub use selection::*;
3032
pub use selector::*;
3133
pub use signer::*;

src/output.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -107,25 +107,4 @@ impl Output {
107107
script_pubkey: self.script_pubkey_source.script(),
108108
}
109109
}
110-
111-
/// To coin select drain (change) output weights.
112-
///
113-
/// Returns `None` if no descriptor is avaliable or the output is unspendable.
114-
pub fn to_drain_weights(&self) -> Option<bdk_coin_select::DrainWeights> {
115-
let descriptor = self.descriptor()?;
116-
Some(bdk_coin_select::DrainWeights {
117-
output_weight: self.txout().weight().to_wu(),
118-
spend_weight: descriptor.max_weight_to_satisfy().ok()?.to_wu(),
119-
n_outputs: 1,
120-
})
121-
}
122-
123-
/// To coin select target outputs.
124-
pub fn to_target_outputs(&self) -> bdk_coin_select::TargetOutputs {
125-
bdk_coin_select::TargetOutputs {
126-
value_sum: self.txout().value.to_sat(),
127-
weight_sum: self.txout().weight().to_wu(),
128-
n_outputs: 1,
129-
}
130-
}
131110
}

src/rbf.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use alloc::vec::Vec;
2+
use bitcoin::{absolute, Amount, OutPoint, Transaction, TxOut, Txid};
3+
use miniscript::bitcoin;
4+
5+
use crate::collections::{HashMap, HashSet};
6+
use crate::{InputCandidates, InputGroup, RbfParams};
7+
8+
/// Set of txs to replace.
9+
pub struct RbfSet<'t> {
10+
txs: HashMap<Txid, &'t Transaction>,
11+
prev_txouts: HashMap<OutPoint, TxOut>,
12+
}
13+
14+
impl<'t> RbfSet<'t> {
15+
/// Create.
16+
///
17+
/// Returns `None` if we have missing `prev_txouts` for the `txs`.
18+
///
19+
/// If any transactions in `txs` are ancestors or descendants of others in `txs`, be sure to
20+
/// include any intermediary transactions needed to resolve those dependencies.
21+
///
22+
/// TODO: Error if trying to replace coinbase.
23+
pub fn new<T, O>(txs: T, prev_txouts: O) -> Option<Self>
24+
where
25+
T: IntoIterator<Item = &'t Transaction>,
26+
O: IntoIterator<Item = (OutPoint, TxOut)>,
27+
{
28+
let set = Self {
29+
txs: txs.into_iter().map(|tx| (tx.compute_txid(), tx)).collect(),
30+
prev_txouts: prev_txouts.into_iter().collect(),
31+
};
32+
let no_missing_previous_txouts = set
33+
.txs
34+
.values()
35+
.flat_map(|tx| tx.input.iter().map(|txin| txin.previous_output))
36+
.all(|op: OutPoint| set.prev_txouts.contains_key(&op));
37+
if no_missing_previous_txouts {
38+
Some(set)
39+
} else {
40+
None
41+
}
42+
}
43+
44+
/// Txids of the original txs that are to be replaced.
45+
///
46+
/// Used for modifying canonicalization to exclude the original txs.
47+
pub fn txids(&self) -> impl ExactSizeIterator<Item = Txid> + '_ {
48+
self.txs.keys().copied()
49+
}
50+
51+
/// Filters input candidates according to rule 2.
52+
///
53+
/// According to rule 2, we cannot spend unconfirmed txs in the replacement unless it
54+
/// was a spend that was already part of the original tx.
55+
pub fn candidate_filter(
56+
&self,
57+
tip_height: absolute::Height,
58+
) -> impl Fn(&InputGroup) -> bool + '_ {
59+
let prev_spends = self
60+
.txs
61+
.values()
62+
.flat_map(|tx| {
63+
tx.input
64+
.iter()
65+
.map(|txin| txin.previous_output)
66+
.collect::<Vec<_>>()
67+
})
68+
.collect::<HashSet<OutPoint>>();
69+
move |group| {
70+
group.all(|input| {
71+
prev_spends.contains(&input.prev_outpoint()) || input.confirmations(tip_height) > 0
72+
})
73+
}
74+
}
75+
76+
/// Returns a policy that selects the largest input of every original tx.
77+
///
78+
/// This guarantees that the txs are replaced.
79+
pub fn must_select_largest_input_per_tx(
80+
&self,
81+
) -> impl FnMut(&InputCandidates) -> HashSet<OutPoint> + '_ {
82+
|input_candidates| {
83+
let mut must_select = HashSet::new();
84+
85+
for original_tx in self.txs.values() {
86+
let mut largest_value = Amount::ZERO;
87+
let mut largest_value_not_canonical = Amount::ZERO;
88+
let mut largest_spend = Option::<OutPoint>::None;
89+
let original_tx_spends = original_tx.input.iter().map(|txin| txin.previous_output);
90+
for spend in original_tx_spends {
91+
// If this spends from another original tx , we do not consider it as replacing
92+
// the parent will replace this one.
93+
if self.txs.contains_key(&spend.txid) {
94+
continue;
95+
}
96+
let txout = self.prev_txouts.get(&spend).expect("must have prev txout");
97+
98+
// not canonical
99+
if !input_candidates.contains(spend) {
100+
if largest_value == Amount::ZERO
101+
&& txout.value > largest_value_not_canonical
102+
{
103+
largest_value_not_canonical = txout.value;
104+
largest_spend = Some(spend);
105+
}
106+
continue;
107+
}
108+
109+
if txout.value > largest_value {
110+
largest_value = txout.value;
111+
largest_spend = Some(spend);
112+
}
113+
}
114+
let largest_spend = largest_spend.expect("tx must have atleast one input");
115+
must_select.insert(largest_spend);
116+
}
117+
118+
must_select
119+
}
120+
}
121+
122+
fn _fee(&self, tx: &'t Transaction) -> Amount {
123+
let output_sum: Amount = tx.output.iter().map(|txout| txout.value).sum();
124+
let input_sum: Amount = tx
125+
.input
126+
.iter()
127+
.map(|txin| {
128+
self.prev_txouts
129+
.get(&txin.previous_output)
130+
.expect("prev output must exist")
131+
.value
132+
})
133+
.sum();
134+
input_sum - output_sum
135+
}
136+
137+
/// Coin selector RBF parameters.
138+
pub fn selector_rbf_params(&self) -> RbfParams {
139+
RbfParams::new(self.txs.values().map(|tx| (*tx, self._fee(tx))))
140+
}
141+
}

src/selector.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use bdk_coin_select::{
22
float::Ordf32, metrics::LowestFee, ChangePolicy, DrainWeights, InsufficientFunds,
33
NoBnbSolution, Replace, Target, TargetFee, TargetOutputs,
44
};
5-
use bitcoin::{Amount, FeeRate, TxOut, Weight};
5+
use bitcoin::{Amount, FeeRate, Transaction, TxOut, Weight};
66
use miniscript::bitcoin;
77

88
use crate::{cs_feerate, DefiniteDescriptor, InputCandidates, InputGroup, Output, Selection};
@@ -54,10 +54,23 @@ pub struct SelectorParams {
5454
/// Rbf original tx stats.
5555
#[derive(Debug, Clone, Copy)]
5656
pub struct OriginalTxStats {
57-
/// Total fee amount of the original tx.
58-
pub fee: Amount,
5957
/// Total weight of the original tx.
6058
pub weight: Weight,
59+
/// Total fee amount of the original tx.
60+
pub fee: Amount,
61+
}
62+
63+
impl From<(Weight, Amount)> for OriginalTxStats {
64+
fn from((weight, fee): (Weight, Amount)) -> Self {
65+
Self { weight, fee }
66+
}
67+
}
68+
69+
impl From<(&Transaction, Amount)> for OriginalTxStats {
70+
fn from((tx, fee): (&Transaction, Amount)) -> Self {
71+
let weight = tx.weight();
72+
Self { weight, fee }
73+
}
6174
}
6275

6376
/// Rbf params.
@@ -93,6 +106,18 @@ impl OriginalTxStats {
93106
}
94107

95108
impl RbfParams {
109+
/// Construct RBF parameters.
110+
pub fn new<I>(tx_to_replace: I) -> Self
111+
where
112+
I: IntoIterator,
113+
I::Item: Into<OriginalTxStats>,
114+
{
115+
Self {
116+
original_txs: tx_to_replace.into_iter().map(Into::into).collect(),
117+
incremental_relay_feerate: FeeRate::from_sat_per_vb_unchecked(1),
118+
}
119+
}
120+
96121
/// To coin select `Replace` params.
97122
pub fn to_cs_replace(&self) -> Replace {
98123
let replace = Replace {

0 commit comments

Comments
 (0)