Skip to content

Commit e986959

Browse files
committed
unify CpfpError to InsufficientInputValue, use CoinSelector for weight
1 parent e6ebdc0 commit e986959

File tree

2 files changed

+30
-117
lines changed

2 files changed

+30
-117
lines changed

examples/common.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,14 @@ impl Wallet {
246246
.script_pubkey();
247247
let output_script = ScriptSource::from_script(script_pubkey);
248248

249-
let cpfp_params = CpfpParams::new(
249+
let cpfp_params = CpfpParams {
250250
package_fee,
251251
package_weight,
252252
inputs,
253+
// inputs: inputs.into_iter().map(Into::into).collect(),
253254
target_package_feerate,
254255
output_script,
255-
);
256+
};
256257

257258
let selection = cpfp_params.into_selection()?;
258259
Ok(selection)

src/cpfp.rs

Lines changed: 27 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::{cs_feerate, Input, Output, ScriptSource, Selection};
1+
use crate::{Input, Output, ScriptSource, Selection};
22
use alloc::vec::Vec;
3-
use bdk_coin_select::{Candidate, CoinSelector, Target, TargetFee, TargetOutputs};
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
}

0 commit comments

Comments
 (0)