Skip to content

Commit eff1ec6

Browse files
committed
Add CPFP feerate tests for same and higher target feerates
1 parent 5b92ca1 commit eff1ec6

File tree

5 files changed

+243
-124
lines changed

5 files changed

+243
-124
lines changed

examples/common.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ use bdk_chain::{
99
use bdk_coin_select::DrainWeights;
1010
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
1111
use bdk_tx::{
12-
CanonicalUnspents, CpfpParams, Input, InputCandidates, RbfParams, ScriptSource, Selection, TxStatus, TxWithStatus
12+
CanonicalUnspents, CpfpParams, Input, InputCandidates, RbfParams, ScriptSource, Selection,
13+
TxStatus, TxWithStatus,
14+
};
15+
use bitcoin::{
16+
absolute, Address, Amount, BlockHash, FeeRate, OutPoint, Transaction, TxOut, Txid, Weight,
1317
};
14-
use bitcoin::{absolute, Address, Amount, BlockHash, FeeRate, OutPoint, Transaction, Txid, Weight};
1518
use miniscript::{
1619
plan::{Assets, Plan},
1720
Descriptor, DescriptorPublicKey, ForEachKey,
@@ -270,13 +273,14 @@ impl Wallet {
270273
.script_pubkey();
271274
let output_script = ScriptSource::from_script(script_pubkey);
272275

273-
let cpfp_params = CpfpParams::new(
276+
let cpfp_params = CpfpParams {
274277
package_fee,
275278
package_weight,
276279
inputs,
280+
// inputs: inputs.into_iter().map(Into::into).collect(),
277281
target_package_feerate,
278282
output_script,
279-
);
283+
};
280284

281285
let selection = cpfp_params.into_selection()?;
282286
Ok(selection)

examples/cpfp.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use bdk_testenv::{bitcoincore_rpc::RpcApi, TestEnv};
44
use bdk_tx::{
55
filter_unspendable_now, group_by_spk, selection_algorithm_lowest_fee_bnb, ChangePolicyType,
6-
Output, PsbtParams, SelectorParams, Signer,
6+
Output, PsbtParams, ScriptSource, SelectorParams, Signer,
77
};
88
use bitcoin::{absolute::LockTime, key::Secp256k1, Amount, FeeRate, Sequence, Transaction};
99
use miniscript::Descriptor;
@@ -53,10 +53,11 @@ fn main() -> anyhow::Result<()> {
5353
addr.script_pubkey(),
5454
Amount::from_sat(49_000_000),
5555
)],
56-
internal.at_derivation_index(i)?,
56+
ScriptSource::Descriptor(Box::new(internal.at_derivation_index(i)?)),
5757
ChangePolicyType::NoDustAndLeastWaste {
5858
longterm_feerate: FeeRate::from_sat_per_vb_unchecked(1),
5959
},
60+
wallet.change_weight(),
6061
),
6162
)?;
6263
let mut parent_psbt = low_fee_selection.create_psbt(PsbtParams {

src/cpfp.rs

Lines changed: 28 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::{cs_feerate, Input, Output, ScriptSource, Selection};
2-
use alloc::vec::Vec;
3-
use bdk_coin_select::{Candidate, CoinSelector, Target, TargetFee, TargetOutputs};
1+
use crate::{Input, Output, ScriptSource, Selection};
2+
use alloc::{vec, vec::Vec};
3+
use bdk_coin_select::{Candidate, CoinSelector, DrainWeights, TargetOutputs};
44
use miniscript::bitcoin::{Amount, FeeRate, TxOut, Weight};
55

66
/// Parameters for creating a Child-Pays-For-Parent (CPFP) transaction.
@@ -30,33 +30,12 @@ pub struct CpfpParams {
3030
}
3131

3232
impl CpfpParams {
33-
/// Create a new [CpfpParams] instance.
34-
pub fn new(
35-
package_fee: Amount,
36-
package_weight: Weight,
37-
inputs: impl IntoIterator<Item = impl Into<Input>>,
38-
target_package_feerate: FeeRate,
39-
output_script: crate::ScriptSource,
40-
) -> Self {
41-
Self {
42-
package_fee,
43-
package_weight,
44-
inputs: inputs.into_iter().map(Into::into).collect(),
45-
target_package_feerate,
46-
output_script,
47-
}
48-
}
49-
5033
/// Convert the CPFP parameters into selection.
5134
///
5235
/// This method calculates the required child transaction fee to achieve the
5336
/// target package feerate and creates a selection with the appropriate inputs
5437
/// and outputs.
5538
pub fn into_selection(self) -> Result<Selection, CpfpError> {
56-
if self.inputs.is_empty() {
57-
return Err(CpfpError::NoSpendableOutputs);
58-
}
59-
6039
// Create candidates for coin selection
6140
let candidates = self
6241
.inputs
@@ -74,49 +53,30 @@ impl CpfpParams {
7453
let mut selector = CoinSelector::new(&candidates);
7554
selector.select_all();
7655

56+
let total_input_value = Amount::from_sat(selector.selected_value());
57+
7758
// Prepare output to calculate weight
7859
let script_pubkey = self.output_script.script();
7960
let output = TxOut {
8061
value: Amount::ZERO,
8162
script_pubkey: script_pubkey.clone(),
8263
};
83-
let output_weight = output.weight().to_wu();
84-
85-
// Calculate required child fee
86-
let child_weight = self.compute_child_tx_weight(&selector, output_weight);
87-
let child_fee = self.compute_child_fee(child_weight)?;
88-
89-
let total_input_value = Amount::from_sat(selector.selected_value());
90-
91-
let output_value = total_input_value
92-
.checked_sub(child_fee)
93-
.ok_or(CpfpError::InsufficientInputValue)?;
9464

95-
let dust_threshold = script_pubkey.minimal_non_dust();
96-
if output_value < dust_threshold {
97-
return Err(CpfpError::OutputBelowDustLimit);
98-
}
99-
100-
// Validate we achieve the target package feerate
101-
let actual_package_feerate = self.compute_package_feerate(child_fee, child_weight);
102-
if actual_package_feerate < self.target_package_feerate {
103-
return Err(CpfpError::InsufficientPackageFeerate {
104-
actual: actual_package_feerate,
105-
target: self.target_package_feerate,
106-
});
107-
}
65+
let target_outputs = TargetOutputs::fund_outputs(vec![(output.weight().to_wu(), 0)]);
66+
let cpfp_tx_weight = Weight::from_wu(selector.weight(target_outputs, DrainWeights::NONE));
10867

109-
// Verify the selection meets coin selection constraints
110-
let target = Target {
111-
fee: TargetFee {
112-
rate: cs_feerate(self.target_package_feerate),
113-
replace: None,
114-
},
115-
outputs: TargetOutputs::fund_outputs(vec![(output_weight, output_value.to_sat())]),
68+
// Calculate required child fee
69+
let total_package_weight = self.package_weight + cpfp_tx_weight;
70+
let required_total_package_fee = self.target_package_feerate * total_package_weight;
71+
let required_child_fee = required_total_package_fee - self.package_fee;
72+
73+
let output_value = match total_input_value.checked_sub(required_child_fee) {
74+
Some(value) => value,
75+
None => {
76+
let missing = required_child_fee - total_input_value;
77+
return Err(CpfpError::InsufficientInputValue { missing });
78+
}
11679
};
117-
if !selector.is_target_met(target) {
118-
return Err(CpfpError::InsufficientInputValue);
119-
}
12080

12181
let outputs = vec![Output::with_script(script_pubkey, output_value)];
12282

@@ -125,76 +85,28 @@ impl CpfpParams {
12585
outputs,
12686
})
12787
}
128-
129-
/// Computes the effective package feerate given the child fee and weight.
130-
pub fn compute_package_feerate(&self, child_fee: Amount, child_weight: Weight) -> FeeRate {
131-
let total_fee = self.package_fee + child_fee;
132-
let total_weight = self.package_weight + child_weight;
133-
134-
total_fee / total_weight
135-
}
136-
137-
/// Computes the required child fee to achieve target package feerate
138-
pub fn compute_child_fee(&self, child_weight: Weight) -> Result<Amount, CpfpError> {
139-
let total_target_weight = self.package_weight + child_weight;
140-
let required_package_fee = self.target_package_feerate * total_target_weight;
141-
142-
required_package_fee
143-
.checked_sub(self.package_fee)
144-
.ok_or(CpfpError::InvalidFeeCalculation)
145-
}
146-
147-
/// Computes the weight of the child transaction.
148-
///
149-
/// Uses the provided `selector` for input weights and `output_weight` for the output.
150-
fn compute_child_tx_weight(&self, selector: &CoinSelector, output_weight: u64) -> Weight {
151-
const BASE_TX_WEIGHT: u64 = 10 * 4; // version, locktime, input/output counts
152-
let input_weight = selector.input_weight();
153-
154-
Weight::from_wu(BASE_TX_WEIGHT + input_weight + output_weight)
155-
}
15688
}
15789

15890
/// CPFP errors.
15991
#[derive(Debug)]
16092
pub enum CpfpError {
161-
/// Output value is below the dust threshold.
162-
OutputBelowDustLimit,
163-
/// Total input value is insufficient.
164-
InsufficientInputValue,
165-
/// No spendable outputs were found.
166-
NoSpendableOutputs,
167-
/// Failed to compute a valid fee for the child transaction.
168-
InvalidFeeCalculation,
169-
/// The package feerate (parent + child) is lower than the target feerate.
170-
InsufficientPackageFeerate {
171-
/// The actual feerate of the package.
172-
actual: FeeRate,
173-
/// The target feerate that the package should meet or exceed.
174-
target: FeeRate,
93+
/// Total input value is insufficient to create a valid CPFP transaction.
94+
InsufficientInputValue {
95+
/// The additional amount needed to create a valid CPFP transaction
96+
missing: Amount,
17597
},
176-
/// Output script is invalid
177-
InvalidOutputScript,
17898
}
17999

180100
impl core::fmt::Display for CpfpError {
181101
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
182102
match self {
183-
Self::OutputBelowDustLimit => write!(f, "output value is below dust threshold"),
184-
Self::InsufficientInputValue => {
185-
write!(f, "input value insufficient to cover required fee")
186-
}
187-
Self::NoSpendableOutputs => {
188-
write!(f, "no spendable outputs found in parent transactions")
189-
}
190-
Self::InvalidFeeCalculation => {
191-
write!(f, "failed to calculate valid child transaction fee")
103+
Self::InsufficientInputValue { missing } => {
104+
write!(
105+
f,
106+
"input value insufficient: need {} more satoshis",
107+
missing.to_sat()
108+
)
192109
}
193-
Self::InsufficientPackageFeerate { actual, target } => write!(
194-
f,
195-
"package feerate {actual} is below target feerate {target}"
196-
),
197-
Self::InvalidOutputScript => write!(f, "output script is invalid or empty"),
198110
}
199111
}
200112
}

src/selector.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bdk_coin_select::{
22
ChangePolicy, DrainWeights, InsufficientFunds, Replace, Target, TargetFee, TargetOutputs,
33
};
4-
use bitcoin::{Amount, FeeRate, Transaction, TxOut, Weight};
4+
use bitcoin::{Amount, FeeRate, Transaction, Weight};
55
use miniscript::bitcoin;
66

77
use crate::{cs_feerate, InputCandidates, InputGroup, Output, ScriptSource, Selection};
@@ -251,7 +251,7 @@ impl<'c> Selector<'c> {
251251
.to_cs_change_policy()
252252
.map_err(SelectorError::Miniscript)?;
253253
let target_outputs = params.target_outputs;
254-
let change_descriptor = params.change_descriptor;
254+
let change_script = params.change_script;
255255
if target.value() > candidates.groups().map(|grp| grp.value().to_sat()).sum() {
256256
return Err(SelectorError::CannotMeetTarget(CannotMeetTarget));
257257
}

0 commit comments

Comments
 (0)