Skip to content

Commit c6c4652

Browse files
committed
chore: example for cpfp support
1 parent 8d74694 commit c6c4652

5 files changed

Lines changed: 191 additions & 19 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 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" }
1617

1718
[dev-dependencies]
1819
anyhow = "1"
1920
bdk_tx = { path = "." }
2021
bitcoin = { version = "0.32", features = ["rand-std"] }
2122
bdk_testenv = "0.13.0"
2223
bdk_bitcoind_rpc = "0.20.0"
23-
bdk_chain = { version = "0.23.0" }
2424

2525
[features]
2626
default = ["std"]

examples/common.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![allow(dead_code)]
2+
13
use std::sync::Arc;
24

35
use bdk_bitcoind_rpc::{Emitter, NO_EXPECTED_MEMPOOL_TXIDS};
@@ -212,7 +214,7 @@ impl Wallet {
212214
target_feerate: FeeRate,
213215
) -> anyhow::Result<(InputCandidates, CPFPParams)> {
214216
let assets = self.assets();
215-
let mut canon_utxos = CanonicalUnspents::new(self.canonical_txs());
217+
let canon_utxos = CanonicalUnspents::new(self.canonical_txs());
216218
let index = &self.graph.index;
217219
let parent_txids: Vec<Txid> = parent_txids.into_iter().collect();
218220

examples/cpfp.rs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#![allow(dead_code)]
2+
3+
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
4+
use bdk_tx::{
5+
filter_unspendable_now, group_by_spk, selection_algorithm_lowest_fee_bnb, ChangePolicyType,
6+
Output, PsbtParams, SelectorParams, Signer,
7+
};
8+
use bitcoin::{key::Secp256k1, Amount, FeeRate, Sequence, Transaction};
9+
use miniscript::Descriptor;
10+
11+
mod common;
12+
use common::Wallet;
13+
14+
fn main() -> anyhow::Result<()> {
15+
let secp = Secp256k1::new();
16+
let (external, external_keymap) =
17+
Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[3])?;
18+
let (internal, internal_keymap) =
19+
Descriptor::parse_descriptor(&secp, bdk_testenv::utils::DESCRIPTORS[4])?;
20+
21+
let signer = Signer(external_keymap.into_iter().chain(internal_keymap).collect());
22+
23+
let env = TestEnv::new()?;
24+
let genesis_hash = env.genesis_hash()?;
25+
env.mine_blocks(101, None)?;
26+
27+
let mut wallet = Wallet::new(genesis_hash, external, internal.clone())?;
28+
wallet.sync(&env)?;
29+
30+
let addr = wallet.next_address().expect("must derive address");
31+
println!("Wallet address: {addr}");
32+
33+
// Fund the wallet with two transactions
34+
env.send(&addr, Amount::from_sat(100_000_000))?;
35+
env.send(&addr, Amount::from_sat(100_000_000))?;
36+
env.mine_blocks(1, None)?;
37+
wallet.sync(&env)?;
38+
println!("Balance: {}", wallet.balance());
39+
40+
// Create two low-fee parent transactions
41+
let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
42+
let mut parent_txids = vec![];
43+
for i in 0..2 {
44+
let low_fee_selection = wallet
45+
.all_candidates()
46+
.regroup(group_by_spk())
47+
.filter(filter_unspendable_now(tip_height, tip_time))
48+
.into_selection(
49+
selection_algorithm_lowest_fee_bnb(FeeRate::from_sat_per_vb_unchecked(1), 100_000),
50+
SelectorParams::new(
51+
FeeRate::from_sat_per_vb_unchecked(1),
52+
vec![Output::with_script(
53+
addr.script_pubkey(),
54+
Amount::from_sat(49_000_000),
55+
)],
56+
internal.at_derivation_index(i)?,
57+
ChangePolicyType::NoDustAndLeastWaste {
58+
longterm_feerate: FeeRate::from_sat_per_vb_unchecked(1),
59+
},
60+
),
61+
)?;
62+
let mut parent_psbt = low_fee_selection.create_psbt(PsbtParams {
63+
fallback_sequence: Sequence::MAX,
64+
..Default::default()
65+
})?;
66+
let parent_finalizer = low_fee_selection.into_finalizer();
67+
parent_psbt.sign(&signer, &secp).expect("failed to sign");
68+
assert!(parent_finalizer.finalize(&mut parent_psbt).is_finalized());
69+
let parent_tx = parent_psbt.extract_tx()?;
70+
let parent_txid = env.rpc_client().send_raw_transaction(&parent_tx)?;
71+
println!("Parent tx {} broadcasted: {}", i + 1, parent_txid);
72+
parent_txids.push(parent_txid);
73+
wallet.sync(&env)?;
74+
}
75+
println!("Balance after parent txs: {}", wallet.balance());
76+
77+
// Verify parent transactions are in mempool
78+
let mempool = env.rpc_client().get_raw_mempool()?;
79+
for (i, txid) in parent_txids.iter().enumerate() {
80+
if mempool.contains(txid) {
81+
println!("Parent TX {} {} is in mempool", i + 1, txid);
82+
} else {
83+
println!("Parent TX {} {} is NOT in mempool", i + 1, txid);
84+
}
85+
}
86+
87+
// Create CPFP transaction to boost both parents
88+
let (cpfp_candidates, _) = wallet.cpfp_candidates(
89+
parent_txids.clone(),
90+
tip_height,
91+
FeeRate::from_sat_per_vb_unchecked(10),
92+
)?;
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+
)?;
107+
let mut cpfp_psbt = cpfp_selection.create_psbt(PsbtParams {
108+
fallback_sequence: Sequence::MAX,
109+
..Default::default()
110+
})?;
111+
let cpfp_finalizer = cpfp_selection.into_finalizer();
112+
cpfp_psbt.sign(&signer, &secp).expect("failed to sign");
113+
assert!(cpfp_finalizer.finalize(&mut cpfp_psbt).is_finalized());
114+
let cpfp_tx = cpfp_psbt.extract_tx()?;
115+
let cpfp_txid = env.rpc_client().send_raw_transaction(&cpfp_tx)?;
116+
117+
wallet.sync(&env)?;
118+
println!("Balance after CPFP: {}", wallet.balance());
119+
120+
// Verify all transactions are in mempool
121+
let mempool = env.rpc_client().get_raw_mempool()?;
122+
println!("\nChecking transactions in mempool:");
123+
for (i, txid) in parent_txids.iter().enumerate() {
124+
if mempool.contains(txid) {
125+
println!("Parent TX {} {} is in mempool", i + 1, txid);
126+
} else {
127+
println!("Parent TX {} {} is NOT in mempool", i + 1, txid);
128+
}
129+
}
130+
if mempool.contains(&cpfp_txid) {
131+
println!("CPFP TX {cpfp_txid} is in mempool");
132+
} else {
133+
println!("CPFP TX {cpfp_txid} is NOT in mempool");
134+
}
135+
136+
// Verify child spends parents
137+
for (i, parent_txid) in parent_txids.iter().enumerate() {
138+
let parent_tx = env.rpc_client().get_raw_transaction(parent_txid, None)?;
139+
if child_spends_parent(&parent_tx, &cpfp_tx) {
140+
println!("CPFP transaction spends an output of parent {}.", i + 1);
141+
} else {
142+
println!(
143+
"CPFP transaction does NOT spend outputs of parent {}.",
144+
i + 1
145+
);
146+
}
147+
}
148+
149+
println!("\n=== MINING BLOCK TO CONFIRM TRANSACTIONS ===");
150+
let block_hashes = env.mine_blocks(1, None)?; // Revert to None, rely on mempool
151+
println!("Mined block: {}", block_hashes[0]);
152+
wallet.sync(&env)?;
153+
154+
println!("Final wallet balance: {}", wallet.balance());
155+
156+
println!("\nChecking transactions in mempool again:");
157+
let mempool = env.rpc_client().get_raw_mempool()?;
158+
for (i, txid) in parent_txids.iter().enumerate() {
159+
if mempool.contains(txid) {
160+
println!("Parent TX {} {} is in mempool", i + 1, txid);
161+
} else {
162+
println!("Parent TX {} {} is NOT in mempool", i + 1, txid);
163+
}
164+
}
165+
if mempool.contains(&cpfp_txid) {
166+
println!("CPFP TX {cpfp_txid} is in mempool");
167+
} else {
168+
println!("CPFP TX {cpfp_txid} is NOT in mempool");
169+
}
170+
Ok(())
171+
}
172+
173+
fn child_spends_parent(parent_tx: &Transaction, child_tx: &Transaction) -> bool {
174+
let parent_txid = parent_tx.compute_txid();
175+
child_tx
176+
.input
177+
.iter()
178+
.any(|input| input.previous_output.txid == parent_txid)
179+
}

src/cpfp.rs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,8 @@ pub struct CPFPSet {
2121
/// CPFP errors.
2222
#[derive(Debug)]
2323
pub enum CPFPError {
24-
/// A specified parent transaction ID does not exist in the transaction graph.
24+
/// A specified parent transaction ID does not exist.
2525
MissingParent(Txid),
26-
/// A previous transaction (prevout) referenced by a parent transaction is missing.
27-
MissingPrevTx(Txid),
28-
/// An output referenced by an outpoint in a parent transaction is missing.
29-
MissingPrevTxOut(OutPoint),
30-
/// No parent transactions were provided for the CPFP operation.
31-
NoParents,
3226
/// A parent transaction has no unspent outputs available to spend in the CPFP transaction.
3327
NoUnspentOutput(Txid),
3428
/// The number of unconfirmed ancestors exceeds the Bitcoin protocol limit (25).
@@ -40,15 +34,12 @@ pub enum CPFPError {
4034
impl core::fmt::Display for CPFPError {
4135
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4236
match self {
43-
Self::MissingParent(txid) => write!(f, "parent transaction {} not found", txid),
44-
Self::MissingPrevTx(txid) => write!(f, "previous transaction {} not found", txid),
45-
Self::MissingPrevTxOut(outpoint) => write!(f, "previous output {} not found", outpoint),
46-
Self::NoParents => write!(f, "no parent transactions provided"),
37+
Self::MissingParent(txid) => write!(f, "parent transaction {txid} not found"),
4738
Self::ExcessUnconfirmedAncestor => write!(f, "too many unconfirmed ancestor"),
4839
Self::NoUnspentOutput(txid) => {
49-
write!(f, "no unspent output found for parent transaction {}", txid)
40+
write!(f, "no unspent output found for parent transaction {txid}")
5041
}
51-
Self::CalculateFee(err) => write!(f, "failed to calculate fee: {}", err),
42+
Self::CalculateFee(err) => write!(f, "failed to calculate fee: {err}"),
5243
}
5344
}
5445
}
@@ -105,7 +96,7 @@ impl CPFPSet {
10596

10697
const MAX_ANCESTORS: usize = 25;
10798
if txs.len() > MAX_ANCESTORS {
108-
return Err(CPFPError::NoParents);
99+
return Err(CPFPError::ExcessUnconfirmedAncestor);
109100
}
110101

111102
Ok(Self {
@@ -139,7 +130,7 @@ impl CPFPSet {
139130
.map(|txout| txout.value)
140131
.unwrap_or(Amount::ZERO)
141132
})
142-
.ok_or_else(|| CPFPError::NoUnspentOutput(*txid))?;
133+
.ok_or(CPFPError::NoUnspentOutput(*txid))?;
143134

144135
must_select.insert(outpoint);
145136
}

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ extern crate alloc;
1313
extern crate std;
1414

1515
mod canonical_unspents;
16+
mod cpfp;
1617
mod finalizer;
1718
mod input;
1819
mod input_candidates;
@@ -21,9 +22,9 @@ mod rbf;
2122
mod selection;
2223
mod selector;
2324
mod signer;
24-
mod cpfp;
2525

2626
pub use canonical_unspents::*;
27+
pub use cpfp::*;
2728
pub use finalizer::*;
2829
pub use input::*;
2930
pub use input_candidates::*;
@@ -35,7 +36,6 @@ pub use rbf::*;
3536
pub use selection::*;
3637
pub use selector::*;
3738
pub use signer::*;
38-
pub use cpfp::*;
3939

4040
#[cfg(feature = "std")]
4141
pub(crate) mod collections {

0 commit comments

Comments
 (0)