Skip to content

Commit 5b92ca1

Browse files
committed
standalone cpfp logic using CpfpParams and rebuilding Wallet::create_cpfp_tx example
1 parent c6c9836 commit 5b92ca1

6 files changed

Lines changed: 214 additions & 245 deletions

File tree

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ readme = "README.md"
1313
[dependencies]
1414
miniscript = { version = "12", default-features = false }
1515
bdk_coin_select = "0.4.0"
16-
bdk_chain = { version = "0.23.0" }
1716

1817
[dev-dependencies]
1918
anyhow = "1"
2019
bdk_tx = { path = "." }
2120
bitcoin = { version = "0.32", features = ["rand-std"] }
2221
bdk_testenv = "0.13.0"
2322
bdk_bitcoind_rpc = "0.20.0"
23+
bdk_chain = { version = "0.23.0" }
2424

2525
[features]
2626
default = ["std"]
@@ -29,6 +29,9 @@ std = ["miniscript/std"]
2929
[[example]]
3030
name = "synopsis"
3131

32+
[[example]]
33+
name = "cpfp"
34+
3235
[[example]]
3336
name = "common"
3437
crate-type = ["lib"]

examples/common.rs

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ use bdk_chain::{
99
use bdk_coin_select::DrainWeights;
1010
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
1111
use bdk_tx::{
12-
CanonicalUnspents, Input, InputCandidates, RbfParams, Selection, TxStatus, TxWithStatus,
12+
CanonicalUnspents, CpfpParams, Input, InputCandidates, RbfParams, ScriptSource, Selection, TxStatus, TxWithStatus
1313
};
14-
use bitcoin::{absolute, Address, BlockHash, FeeRate, OutPoint, Transaction, Txid};
14+
use bitcoin::{absolute, Address, Amount, BlockHash, FeeRate, OutPoint, Transaction, Txid, Weight};
1515
use miniscript::{
1616
plan::{Assets, Plan},
1717
Descriptor, DescriptorPublicKey, ForEachKey,
@@ -204,35 +204,81 @@ impl Wallet {
204204
))
205205
}
206206

207-
pub fn create_cpfp_transaction(
207+
pub fn create_cpfp_tx(
208208
&mut self,
209209
parent_txids: impl IntoIterator<Item = Txid>,
210210
target_package_feerate: FeeRate,
211211
) -> anyhow::Result<Selection> {
212+
let parent_txids: Vec<Txid> = parent_txids.into_iter().collect();
213+
214+
// Check for empty parent_txids
215+
if parent_txids.is_empty() {
216+
return Err(anyhow::anyhow!("No parent transactions provided"));
217+
}
218+
212219
let assets = self.assets();
213220
let canon_utxos = CanonicalUnspents::new(self.canonical_txs());
221+
let graph = self.graph.graph();
214222

215-
let script_pubkey = self.next_address().unwrap().script_pubkey();
216223
let ownership_check =
217224
|outpoint: OutPoint| -> bool { self.graph.index.txout(outpoint).is_some() };
218225

219-
let cpfpset = canon_utxos.build_cpfp_set_from_txids(
220-
parent_txids,
221-
self.graph.graph(),
222-
ownership_check,
223-
)?;
224-
let plans = cpfpset
225-
.selected_outpoints
226-
.iter()
227-
.filter_map(|op| self.plan_of_output(*op, &assets));
228-
229-
let selection = cpfpset.create_cpfp_transaction(
230-
&canon_utxos,
226+
// Collect inputs and calculate package fee and weight
227+
let mut inputs = Vec::new();
228+
let mut package_fee = Amount::ZERO;
229+
let mut package_weight = Weight::ZERO;
230+
231+
for txid in parent_txids {
232+
let tx = canon_utxos
233+
.get_tx(&txid)
234+
.ok_or_else(|| anyhow::anyhow!("parent transaction {} not found", txid))?;
235+
236+
if canon_utxos.get_status(&txid).is_none() {
237+
package_fee += graph.calculate_fee(tx)?;
238+
package_weight += tx.weight();
239+
}
240+
241+
let mut found = false;
242+
243+
for (vout, _) in tx.output.iter().enumerate() {
244+
let outpoint = OutPoint::new(txid, vout as u32);
245+
246+
if canon_utxos.is_unspent(outpoint) && ownership_check(outpoint) {
247+
let plan = self
248+
.plan_of_output(outpoint, &assets)
249+
.ok_or_else(|| anyhow::anyhow!("no plan for outpoint {}", outpoint))?;
250+
let input = canon_utxos.try_get_unspent(outpoint, plan).ok_or_else(|| {
251+
anyhow::anyhow!("failed to get input for outpoint {}", outpoint)
252+
})?;
253+
inputs.push(input);
254+
found = true;
255+
break;
256+
}
257+
}
258+
259+
if !found {
260+
return Err(anyhow::anyhow!(
261+
"no owned unspent output found for txid {}",
262+
txid
263+
));
264+
}
265+
}
266+
267+
let script_pubkey = self
268+
.next_address()
269+
.ok_or_else(|| anyhow::anyhow!("failed to get next address"))?
270+
.script_pubkey();
271+
let output_script = ScriptSource::from_script(script_pubkey);
272+
273+
let cpfp_params = CpfpParams::new(
274+
package_fee,
275+
package_weight,
276+
inputs,
231277
target_package_feerate,
232-
&script_pubkey,
233-
plans,
234-
)?;
278+
output_script,
279+
);
235280

281+
let selection = cpfp_params.into_selection()?;
236282
Ok(selection)
237283
}
238284
}

examples/cpfp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ fn main() -> anyhow::Result<()> {
8585
}
8686

8787
// Create CPFP transaction to boost both parents
88-
let cpfp_selection = wallet.create_cpfp_transaction(
88+
let cpfp_selection = wallet.create_cpfp_tx(
8989
parent_txids.clone(),
9090
FeeRate::from_sat_per_vb_unchecked(10), // user specified
9191
)?;

src/canonical_unspents.rs

Lines changed: 9 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ 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+
58
use crate::{
6-
collections::HashMap, input::CoinbaseMismatch, CPFPError, CPFPSet, FromPsbtInputError, Input,
7-
RbfSet, TxStatus,
9+
collections::HashMap, input::CoinbaseMismatch, FromPsbtInputError, Input, RbfSet, TxStatus,
810
};
9-
use bdk_chain::TxGraph;
10-
use bitcoin::{psbt, Amount, OutPoint, Sequence, Transaction, TxOut, Txid, Weight};
11-
use miniscript::{bitcoin, plan::Plan};
1211

1312
/// Tx with confirmation status.
1413
pub type TxWithStatus<T> = (T, Option<TxStatus>);
@@ -21,22 +20,6 @@ pub struct CanonicalUnspents {
2120
spends: HashMap<OutPoint, Txid>,
2221
}
2322

24-
fn select_upper_middle_output(candidates: &[(OutPoint, &TxOut)]) -> OutPoint {
25-
let len = candidates.len();
26-
27-
let index = match len {
28-
1..=2 => len - 1, // select the largest for small sets
29-
3..=5 => len - 2, // select second largest for medium sets
30-
_ => {
31-
let upper_third_start = (len * 2) / 3;
32-
let upper_third_end = len - 1;
33-
(upper_third_start + upper_third_end) / 2
34-
}
35-
};
36-
37-
candidates[index].0
38-
}
39-
4023
impl CanonicalUnspents {
4124
/// Construct [`CanonicalUnspents`] from an iterator of txs with confirmation status.
4225
pub fn new<T>(canonical_txs: impl IntoIterator<Item = TxWithStatus<T>>) -> Self
@@ -143,83 +126,6 @@ impl CanonicalUnspents {
143126
)
144127
}
145128

146-
/// Constructs a '[CPFPSet]' from a set of parent transaction IDs.
147-
pub fn build_cpfp_set_from_txids<F>(
148-
&self,
149-
parent_txids: impl IntoIterator<Item = Txid>,
150-
graph: &TxGraph,
151-
ownership_check: F,
152-
) -> Result<CPFPSet, CPFPError>
153-
where
154-
F: Fn(OutPoint) -> bool + Clone,
155-
{
156-
let parent_txids: Vec<Txid> = parent_txids.into_iter().collect();
157-
158-
const MAX_ANCESTORS: usize = 25;
159-
if parent_txids.len() > MAX_ANCESTORS {
160-
return Err(CPFPError::ExcessUnconfirmedAncestor);
161-
}
162-
163-
let (parent_weight, parent_fee, selected_outpoints) = parent_txids.iter().try_fold(
164-
(Weight::ZERO, Amount::ZERO, Vec::new()),
165-
|(mut weight, mut fee, mut outpoints), txid| -> Result<_, CPFPError> {
166-
let tx = self.get_tx(txid).ok_or(CPFPError::MissingParent(*txid))?;
167-
168-
weight += tx.weight();
169-
fee += graph.calculate_fee(tx)?;
170-
171-
let selected_outpoint =
172-
self.select_owned_unspent_output(tx.clone(), ownership_check.clone())?;
173-
outpoints.push(selected_outpoint);
174-
175-
Ok((weight, fee, outpoints))
176-
},
177-
)?;
178-
179-
Ok(CPFPSet::new(parent_fee, parent_weight, selected_outpoints))
180-
}
181-
182-
/// Selects the unspent output from a given transaction
183-
pub fn select_owned_unspent_output<F>(
184-
&self,
185-
tx: Arc<Transaction>,
186-
ownership_check: F,
187-
) -> Result<OutPoint, CPFPError>
188-
where
189-
F: Fn(OutPoint) -> bool,
190-
{
191-
let txid = tx.compute_txid();
192-
let mut candidates: Vec<_> = tx
193-
.output
194-
.iter()
195-
.enumerate()
196-
.map(|(vout, txout)| {
197-
(
198-
OutPoint {
199-
txid,
200-
vout: vout as u32,
201-
},
202-
txout,
203-
)
204-
})
205-
.filter(|(op, tx_out)| {
206-
// Must be unspent, owned and above dust threshold
207-
self.is_unspent(*op)
208-
&& ownership_check(*op)
209-
&& tx_out.value >= tx_out.script_pubkey.minimal_non_dust()
210-
})
211-
.collect();
212-
213-
if candidates.is_empty() {
214-
return Err(CPFPError::NoUnspentOutput(txid));
215-
}
216-
217-
candidates.sort_by_key(|(_, txout)| txout.value);
218-
let selected_outpoint = select_upper_middle_output(&candidates);
219-
220-
Ok(selected_outpoint)
221-
}
222-
223129
/// Whether outpoint is a leaf (unspent).
224130
pub fn is_unspent(&self, outpoint: OutPoint) -> bool {
225131
if self.spends.contains_key(&outpoint) {
@@ -323,6 +229,11 @@ impl CanonicalUnspents {
323229
pub fn get_tx(&self, txid: &Txid) -> Option<&Arc<Transaction>> {
324230
self.txs.get(txid)
325231
}
232+
233+
/// Retrieves a status by its transaction ID.
234+
pub fn get_status(&self, txid: &Txid) -> Option<&TxStatus> {
235+
self.statuses.get(txid)
236+
}
326237
}
327238

328239
/// Canonical unspents error

0 commit comments

Comments
 (0)