Skip to content

Commit 5ad7b68

Browse files
committed
chore: example for cpfp support
1 parent 2820fa0 commit 5ad7b68

5 files changed

Lines changed: 195 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};
@@ -187,7 +189,7 @@ impl Wallet {
187189
target_feerate: FeeRate,
188190
) -> anyhow::Result<(InputCandidates, CPFPParams)> {
189191
let assets = self.assets();
190-
let mut canon_utxos = CanonicalUnspents::new(self.canonical_txs());
192+
let canon_utxos = CanonicalUnspents::new(self.canonical_txs());
191193
let index = &self.graph.index;
192194
let parent_txids: Vec<Txid> = parent_txids.into_iter().collect();
193195

examples/cpfp.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
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+
let funding_txid1 = env.send(&addr, Amount::from_sat(100_000_000))?;
35+
let funding_txid2 = env.send(&addr, Amount::from_sat(100_000_000))?;
36+
env.mine_blocks(1, None)?;
37+
wallet.sync(&env)?;
38+
println!(
39+
"Received funding transactions: {}, {}",
40+
funding_txid1, funding_txid2
41+
);
42+
println!("Balance: {}", wallet.balance());
43+
44+
// Create two low-fee parent transactions
45+
let (tip_height, tip_time) = wallet.tip_info(env.rpc_client())?;
46+
let mut parent_txids = vec![];
47+
for i in 0..2 {
48+
let low_fee_selection = wallet
49+
.all_candidates()
50+
.regroup(group_by_spk())
51+
.filter(filter_unspendable_now(tip_height, tip_time))
52+
.into_selection(
53+
selection_algorithm_lowest_fee_bnb(FeeRate::from_sat_per_vb_unchecked(1), 100_000),
54+
SelectorParams::new(
55+
FeeRate::from_sat_per_vb_unchecked(1),
56+
vec![Output::with_script(
57+
addr.script_pubkey(),
58+
Amount::from_sat(49_000_000),
59+
)],
60+
internal.at_derivation_index(i)?,
61+
ChangePolicyType::NoDustAndLeastWaste {
62+
longterm_feerate: FeeRate::from_sat_per_vb_unchecked(1),
63+
},
64+
),
65+
)?;
66+
let mut parent_psbt = low_fee_selection.create_psbt(PsbtParams {
67+
fallback_sequence: Sequence::MAX,
68+
..Default::default()
69+
})?;
70+
let parent_finalizer = low_fee_selection.into_finalizer();
71+
parent_psbt.sign(&signer, &secp).expect("failed to sign");
72+
assert!(parent_finalizer.finalize(&mut parent_psbt).is_finalized());
73+
let parent_tx = parent_psbt.extract_tx()?;
74+
let parent_txid = env.rpc_client().send_raw_transaction(&parent_tx)?;
75+
println!("Parent tx {} broadcasted: {}", i + 1, parent_txid);
76+
parent_txids.push(parent_txid);
77+
wallet.sync(&env)?;
78+
}
79+
println!("Balance after parent txs: {}", wallet.balance());
80+
81+
// Verify parent transactions are in mempool
82+
let mempool = env.rpc_client().get_raw_mempool()?;
83+
for (i, txid) in parent_txids.iter().enumerate() {
84+
if mempool.contains(txid) {
85+
println!("Parent TX {} {} is in mempool", i + 1, txid);
86+
} else {
87+
println!("Parent TX {} {} is NOT in mempool", i + 1, txid);
88+
}
89+
}
90+
91+
// Create CPFP transaction to boost both parents
92+
let (cpfp_candidates, _) = wallet.cpfp_candidates(
93+
parent_txids.clone(),
94+
tip_height,
95+
FeeRate::from_sat_per_vb_unchecked(10),
96+
)?;
97+
let cpfp_selection = cpfp_candidates
98+
.regroup(group_by_spk())
99+
.filter(filter_unspendable_now(tip_height, tip_time))
100+
.into_selection(
101+
selection_algorithm_lowest_fee_bnb(FeeRate::from_sat_per_vb_unchecked(1), 100_000),
102+
SelectorParams::new(
103+
FeeRate::from_sat_per_vb_unchecked(30), // Higher fee rate for 10 sat/vB combined
104+
vec![], // No additional outputs, maximize change
105+
internal.at_derivation_index(2)?,
106+
ChangePolicyType::NoDustAndLeastWaste {
107+
longterm_feerate: FeeRate::from_sat_per_vb_unchecked(1),
108+
},
109+
),
110+
)?;
111+
let mut cpfp_psbt = cpfp_selection.create_psbt(PsbtParams {
112+
fallback_sequence: Sequence::MAX,
113+
..Default::default()
114+
})?;
115+
let cpfp_finalizer = cpfp_selection.into_finalizer();
116+
cpfp_psbt.sign(&signer, &secp).expect("failed to sign");
117+
assert!(cpfp_finalizer.finalize(&mut cpfp_psbt).is_finalized());
118+
let cpfp_tx = cpfp_psbt.extract_tx()?;
119+
let cpfp_txid = env.rpc_client().send_raw_transaction(&cpfp_tx)?;
120+
121+
wallet.sync(&env)?;
122+
println!("Balance after CPFP: {}", wallet.balance());
123+
124+
// Verify all transactions are in mempool
125+
let mempool = env.rpc_client().get_raw_mempool()?;
126+
println!("\nChecking transactions in mempool:");
127+
for (i, txid) in parent_txids.iter().enumerate() {
128+
if mempool.contains(txid) {
129+
println!("Parent TX {} {} is in mempool", i + 1, txid);
130+
} else {
131+
println!("Parent TX {} {} is NOT in mempool", i + 1, txid);
132+
}
133+
}
134+
if mempool.contains(&cpfp_txid) {
135+
println!("CPFP TX {} is in mempool", cpfp_txid);
136+
} else {
137+
println!("CPFP TX {} is NOT in mempool", cpfp_txid);
138+
}
139+
140+
// Verify child spends parents
141+
for (i, parent_txid) in parent_txids.iter().enumerate() {
142+
let parent_tx = env.rpc_client().get_raw_transaction(parent_txid, None)?;
143+
if child_spends_parent(&parent_tx, &cpfp_tx) {
144+
println!("CPFP transaction spends an output of parent {}.", i + 1);
145+
} else {
146+
println!(
147+
"CPFP transaction does NOT spend outputs of parent {}.",
148+
i + 1
149+
);
150+
}
151+
}
152+
153+
println!("\n=== MINING BLOCK TO CONFIRM TRANSACTIONS ===");
154+
let block_hashes = env.mine_blocks(1, None)?; // Revert to None, rely on mempool
155+
println!("Mined block: {}", block_hashes[0]);
156+
wallet.sync(&env)?;
157+
158+
println!("Final wallet balance: {}", wallet.balance());
159+
160+
println!("\nChecking transactions in mempool again:");
161+
let mempool = env.rpc_client().get_raw_mempool()?;
162+
for (i, txid) in parent_txids.iter().enumerate() {
163+
if mempool.contains(txid) {
164+
println!("Parent TX {} {} is in mempool", i + 1, txid);
165+
} else {
166+
println!("Parent TX {} {} is NOT in mempool", i + 1, txid);
167+
}
168+
}
169+
if mempool.contains(&cpfp_txid) {
170+
println!("CPFP TX {} is in mempool", cpfp_txid);
171+
} else {
172+
println!("CPFP TX {} is NOT in mempool", cpfp_txid);
173+
}
174+
Ok(())
175+
}
176+
177+
fn child_spends_parent(parent_tx: &Transaction, child_tx: &Transaction) -> bool {
178+
let parent_txid = parent_tx.compute_txid();
179+
child_tx
180+
.input
181+
.iter()
182+
.any(|input| input.previous_output.txid == parent_txid)
183+
}

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)