Skip to content

Commit 7ed847c

Browse files
committed
refactor(cpfp): remove coin selection and calculate fee from package rate
1 parent c6c4652 commit 7ed847c

6 files changed

Lines changed: 241 additions & 266 deletions

File tree

examples/common.rs

Lines changed: 17 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ use bdk_chain::{
99
use bdk_coin_select::DrainWeights;
1010
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
1111
use bdk_tx::{
12-
CPFPParams, CPFPSet, CanonicalUnspents, Input, InputCandidates, RbfParams, TxStatus,
13-
TxWithStatus,
12+
CanonicalUnspents, Input, InputCandidates, RbfParams, Selection, TxStatus, TxWithStatus,
1413
};
1514
use bitcoin::{absolute, Address, BlockHash, FeeRate, OutPoint, Transaction, Txid};
1615
use miniscript::{
@@ -89,8 +88,6 @@ impl Wallet {
8988
Ok((tip_height, tip_time))
9089
}
9190

92-
// TODO: Maybe create an `AssetsBuilder` or `AssetsExt` that makes it easier to add
93-
// assets from descriptors, etc.
9491
pub fn assets(&self) -> Assets {
9592
let index = &self.graph.index;
9693
let tip = self.chain.tip().block_id();
@@ -207,50 +204,28 @@ impl Wallet {
207204
))
208205
}
209206

210-
pub fn cpfp_candidates(
211-
&self,
207+
pub fn create_cpfp_transaction(
208+
&mut self,
212209
parent_txids: impl IntoIterator<Item = Txid>,
213-
tip_height: absolute::Height,
214-
target_feerate: FeeRate,
215-
) -> anyhow::Result<(InputCandidates, CPFPParams)> {
210+
target_package_feerate: FeeRate,
211+
) -> anyhow::Result<Selection> {
216212
let assets = self.assets();
217213
let canon_utxos = CanonicalUnspents::new(self.canonical_txs());
218-
let index = &self.graph.index;
219-
let parent_txids: Vec<Txid> = parent_txids.into_iter().collect();
220214

221-
let cpfpset = CPFPSet::new(parent_txids.clone(), self.graph.graph(), tip_height)?;
222-
let cpfp_params = cpfpset.selector_cpfp_params(target_feerate);
223-
224-
let must_select = cpfpset
225-
.must_select_largest_input_of_each_parent(&canon_utxos)?
226-
.into_iter()
227-
.map(|op| {
228-
let plan = self
229-
.plan_of_output(op, &assets)
230-
.ok_or_else(|| anyhow::anyhow!("failed to derive plan for outpoint {}", op))?;
231-
canon_utxos.try_get_unspent(op, plan).ok_or_else(|| {
232-
anyhow::anyhow!("failed to get unspent input for outpoint {}", op)
233-
})
234-
})
235-
.collect::<Result<Vec<Input>, _>>()?;
236-
237-
// Select other spendable outputs as optional candidates
238-
let can_select = index
239-
.outpoints()
215+
let script_pubkey = self.next_address().unwrap().script_pubkey();
216+
let cpfpset = canon_utxos.build_cpfp_set_from_txids(parent_txids, self.graph.graph())?;
217+
let plans = cpfpset
218+
.selected_outpoints
240219
.iter()
241-
.filter_map(|(_, op)| {
242-
if canon_utxos.is_unspent(*op) {
243-
self.plan_of_output(*op, &assets)
244-
.map(|plan| canon_utxos.try_get_unspent(*op, plan))
245-
} else {
246-
None
247-
}
248-
})
249-
.flatten();
220+
.filter_map(|op| self.plan_of_output(*op, &assets));
250221

251-
let candidates = InputCandidates::new(must_select, can_select)
252-
.filter(cpfpset.candidate_filter(&canon_utxos, tip_height));
222+
let selection = cpfpset.create_cpfp_transaction(
223+
&canon_utxos,
224+
target_package_feerate,
225+
&script_pubkey,
226+
plans,
227+
)?;
253228

254-
Ok((candidates, cpfp_params))
229+
Ok(selection)
255230
}
256231
}

examples/cpfp.rs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use bdk_tx::{
55
filter_unspendable_now, group_by_spk, selection_algorithm_lowest_fee_bnb, ChangePolicyType,
66
Output, PsbtParams, SelectorParams, Signer,
77
};
8-
use bitcoin::{key::Secp256k1, Amount, FeeRate, Sequence, Transaction};
8+
use bitcoin::{absolute::LockTime, key::Secp256k1, Amount, FeeRate, Sequence, Transaction};
99
use miniscript::Descriptor;
1010

1111
mod common;
@@ -40,7 +40,7 @@ fn main() -> anyhow::Result<()> {
4040
// Create two low-fee parent transactions
4141
let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
4242
let mut parent_txids = vec![];
43-
for i in 0..2 {
43+
for i in 0..3 {
4444
let low_fee_selection = wallet
4545
.all_candidates()
4646
.regroup(group_by_spk())
@@ -85,27 +85,14 @@ fn main() -> anyhow::Result<()> {
8585
}
8686

8787
// Create CPFP transaction to boost both parents
88-
let (cpfp_candidates, _) = wallet.cpfp_candidates(
88+
let cpfp_selection = wallet.create_cpfp_transaction(
8989
parent_txids.clone(),
90-
tip_height,
91-
FeeRate::from_sat_per_vb_unchecked(10),
90+
FeeRate::from_sat_per_vb_unchecked(10), // user specified
9291
)?;
93-
let cpfp_selection = cpfp_candidates
94-
.regroup(group_by_spk())
95-
.filter(filter_unspendable_now(tip_height, tip_time))
96-
.into_selection(
97-
selection_algorithm_lowest_fee_bnb(FeeRate::from_sat_per_vb_unchecked(1), 100_000),
98-
SelectorParams::new(
99-
FeeRate::from_sat_per_vb_unchecked(30), // Higher fee rate for 10 sat/vB combined
100-
vec![], // No additional outputs, maximize change
101-
internal.at_derivation_index(2)?,
102-
ChangePolicyType::NoDustAndLeastWaste {
103-
longterm_feerate: FeeRate::from_sat_per_vb_unchecked(1),
104-
},
105-
),
106-
)?;
92+
10793
let mut cpfp_psbt = cpfp_selection.create_psbt(PsbtParams {
10894
fallback_sequence: Sequence::MAX,
95+
fallback_locktime: LockTime::ZERO,
10996
..Default::default()
11097
})?;
11198
let cpfp_finalizer = cpfp_selection.into_finalizer();

examples/synopsis.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ fn main() -> anyhow::Result<()> {
142142
change_weight: wallet.change_weight(),
143143
// This ensures that we satisfy mempool-replacement policy rules 4 and 6.
144144
replace: Some(rbf_params),
145-
cpfp: None,
146145
},
147146
)?;
148147

src/canonical_unspents.rs

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ use alloc::sync::Arc;
22
use alloc::vec::Vec;
33
use core::fmt;
44

5-
use bitcoin::{psbt, OutPoint, Sequence, Transaction, TxOut, Txid};
6-
use miniscript::{bitcoin, plan::Plan};
7-
85
use crate::{
9-
collections::HashMap, input::CoinbaseMismatch, FromPsbtInputError, Input, RbfSet, TxStatus,
6+
collections::HashMap, input::CoinbaseMismatch, CPFPError, CPFPSet, FromPsbtInputError, Input,
7+
RbfSet, TxStatus,
108
};
9+
use bdk_chain::TxGraph;
10+
use bitcoin::{psbt, Amount, OutPoint, Sequence, Transaction, TxOut, Txid, Weight};
11+
use miniscript::{bitcoin, plan::Plan};
1112

1213
/// Tx with confirmation status.
1314
pub type TxWithStatus<T> = (T, Option<TxStatus>);
@@ -126,6 +127,64 @@ impl CanonicalUnspents {
126127
)
127128
}
128129

130+
/// Constructs a '[CPFPSet]' from a set of parent transaction IDs.
131+
pub fn build_cpfp_set_from_txids(
132+
&self,
133+
parent_txids: impl IntoIterator<Item = Txid>,
134+
graph: &TxGraph,
135+
) -> Result<CPFPSet, CPFPError> {
136+
let parent_txids: Vec<Txid> = parent_txids.into_iter().collect();
137+
138+
const MAX_ANCESTORS: usize = 25;
139+
if parent_txids.len() > MAX_ANCESTORS {
140+
return Err(CPFPError::ExcessUnconfirmedAncestor);
141+
}
142+
143+
let (parent_weight, parent_fee, selected_outpoints) = parent_txids.iter().try_fold(
144+
(Weight::ZERO, Amount::ZERO, Vec::new()),
145+
|(mut weight, mut fee, mut outpoints), txid| -> Result<_, CPFPError> {
146+
let tx = self.get_tx(txid).ok_or(CPFPError::MissingParent(*txid))?;
147+
148+
weight += tx.weight();
149+
fee += graph.calculate_fee(tx)?;
150+
151+
let base_outpoint = self.select_largest_unspent_output(tx.clone())?;
152+
outpoints.push(base_outpoint);
153+
154+
Ok((weight, fee, outpoints))
155+
},
156+
)?;
157+
158+
Ok(CPFPSet::new(parent_fee, parent_weight, selected_outpoints))
159+
}
160+
161+
/// Selects the largest unspent output from a given transaction
162+
pub fn select_largest_unspent_output(
163+
&self,
164+
tx: Arc<Transaction>,
165+
) -> Result<OutPoint, CPFPError> {
166+
let txid = tx.compute_txid();
167+
let outpoint = tx
168+
.output
169+
.iter()
170+
.enumerate()
171+
.map(|(vout, txout)| {
172+
(
173+
OutPoint {
174+
txid,
175+
vout: vout as u32,
176+
},
177+
txout,
178+
)
179+
})
180+
.filter(|(op, _)| self.is_unspent(*op))
181+
.max_by_key(|(_, txout)| txout.value)
182+
.map(|(op, _)| op)
183+
.ok_or(CPFPError::NoUnspentOutput(txid))?;
184+
185+
Ok(outpoint)
186+
}
187+
129188
/// Whether outpoint is a leaf (unspent).
130189
pub fn is_unspent(&self, outpoint: OutPoint) -> bool {
131190
if self.spends.contains_key(&outpoint) {
@@ -229,11 +288,6 @@ impl CanonicalUnspents {
229288
pub fn get_tx(&self, txid: &Txid) -> Option<&Arc<Transaction>> {
230289
self.txs.get(txid)
231290
}
232-
233-
/// Retrieves the transaction ID that spends a given output, if any.
234-
pub fn get_spend(&self, outpoint: &OutPoint) -> Option<&Txid> {
235-
self.spends.get(outpoint)
236-
}
237291
}
238292

239293
/// Canonical unspents error

0 commit comments

Comments
 (0)