Skip to content

Commit f25b925

Browse files
committed
Implement CPFP fee bumping for unconfirmed transactions
Add `Child-Pays-For-Parent` functionality to allow users to accelerate pending unconfirmed transactions by creating higher-fee child spends. This provides an alternative to Replace-by-Fee bumping when direct transaction replacement is not available or desired. - Creates new transactions spending from unconfirmed UTXOs with increased fees - Specifically designed for accelerating stuck unconfirmed transactions - Miners consider combined fees of parent and child transactions - Maintains payment tracking and wallet state consistency - Includes integration tests covering various CPFP scenarios - Provides clear error handling for unsuitable or confirmed UTXOs The feature is accessible via `bump_fee_cpfp(payment_id)` method.
1 parent 066c0e1 commit f25b925

File tree

4 files changed

+262
-1
lines changed

4 files changed

+262
-1
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ interface OnchainPayment {
266266
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
267267
[Throws=NodeError]
268268
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
269+
[Throws=NodeError]
270+
Txid bump_fee_cpfp(PaymentId payment_id);
269271
};
270272

271273
interface FeeRate {

src/payment/onchain.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ use crate::logger::{log_info, LdkLogger, Logger};
1717
use crate::types::{ChannelManager, Wallet};
1818
use crate::wallet::OnchainSendAmount;
1919

20+
use lightning::ln::channelmanager::PaymentId;
21+
2022
#[cfg(not(feature = "uniffi"))]
2123
type FeeRate = bitcoin::FeeRate;
2224
#[cfg(feature = "uniffi")]
@@ -120,4 +122,31 @@ impl OnchainPayment {
120122
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
121123
self.wallet.send_to_address(address, send_amount, fee_rate_opt)
122124
}
125+
126+
/// Bumps the fee of a given UTXO using Child-Pays-For-Parent (CPFP) by creating a new transaction.
127+
///
128+
/// This method creates a new transaction that spends the specified UTXO with a higher fee rate,
129+
/// effectively increasing the priority of both the new transaction and the parent transaction
130+
/// it depends on. This is useful when a transaction is stuck in the mempool due to insufficient
131+
/// fees and you want to accelerate its confirmation.
132+
///
133+
/// CPFP works by creating a child transaction that spends one or more outputs from the parent
134+
/// transaction. Miners will consider the combined fees of both transactions when deciding
135+
/// which transactions to include in a block.
136+
///
137+
/// # Parameters
138+
/// * `payment_id` - The identifier of the payment whose UTXO should be fee-bumped
139+
///
140+
/// # Returns
141+
/// * `Ok(Txid)` - The transaction ID of the newly created CPFP transaction on success
142+
/// * `Err(Error)` - If the payment cannot be found, the UTXO is not suitable for CPFP,
143+
/// or if there's an error creating the transaction
144+
///
145+
/// # Note
146+
/// CPFP is specifically designed to work with unconfirmed UTXOs. The child transaction
147+
/// can spend outputs from unconfirmed parent transactions, allowing miners to consider
148+
/// the combined fees of both transactions when building a block.
149+
pub fn bump_fee_cpfp(&self, payment_id: PaymentId) -> Result<Txid, Error> {
150+
self.wallet.bump_fee_cpfp(payment_id)
151+
}
123152
}

src/wallet/mod.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::future::Future;
99
use std::ops::Deref;
1010
use std::str::FromStr;
1111
use std::sync::{Arc, Mutex};
12+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
1213

1314
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
1415
use bdk_wallet::descriptor::ExtendedDescriptor;
@@ -978,6 +979,131 @@ impl Wallet {
978979

979980
None
980981
}
982+
983+
#[allow(deprecated)]
984+
pub(crate) fn bump_fee_cpfp(&self, payment_id: PaymentId) -> Result<Txid, Error> {
985+
let txid = Txid::from_slice(&payment_id.0).expect("32 bytes");
986+
987+
let payment = self.pending_payment_store.get(&payment_id).ok_or(Error::InvalidPaymentId)?;
988+
989+
if let PaymentKind::Onchain { status, .. } = &payment.details.kind {
990+
match status {
991+
ConfirmationStatus::Confirmed { .. } => {
992+
log_error!(self.logger, "Transaction {} is already confirmed", txid);
993+
return Err(Error::InvalidPaymentId);
994+
},
995+
ConfirmationStatus::Unconfirmed => {},
996+
}
997+
}
998+
999+
let mut locked_wallet = self.inner.lock().unwrap();
1000+
1001+
let wallet_tx = locked_wallet.get_tx(txid).ok_or(Error::InvalidPaymentId)?;
1002+
let transaction = &wallet_tx.tx_node.tx;
1003+
1004+
// Create the CPFP transaction using a high fee rate to get it confirmed quickly.
1005+
let mut our_vout: Option<u32> = None;
1006+
let mut our_value: Amount = Amount::ZERO;
1007+
1008+
for (vout_index, output) in transaction.output.iter().enumerate() {
1009+
let script = output.script_pubkey.clone();
1010+
1011+
if locked_wallet.is_mine(script) {
1012+
our_vout = Some(vout_index as u32);
1013+
our_value = output.value.into();
1014+
break;
1015+
}
1016+
}
1017+
1018+
let our_vout = our_vout.ok_or_else(|| {
1019+
log_error!(
1020+
self.logger,
1021+
"Could not find an output owned by this wallet in transaction {}",
1022+
txid
1023+
);
1024+
Error::InvalidPaymentId
1025+
})?;
1026+
1027+
let cpfp_outpoint = OutPoint::new(txid, our_vout);
1028+
1029+
let confirmation_target = ConfirmationTarget::OnchainPayment;
1030+
let estimated_fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
1031+
1032+
const CPFP_MULTIPLIER: f64 = 1.5;
1033+
let boosted_fee_rate = FeeRate::from_sat_per_kwu(
1034+
((estimated_fee_rate.to_sat_per_kwu() as f64) * CPFP_MULTIPLIER) as u64,
1035+
);
1036+
1037+
let mut psbt = {
1038+
let mut tx_builder = locked_wallet.build_tx();
1039+
tx_builder
1040+
.add_utxo(cpfp_outpoint)
1041+
.map_err(|e| {
1042+
log_error!(self.logger, "Failed to add CPFP UTXO {}: {}", cpfp_outpoint, e);
1043+
Error::InvalidPaymentId
1044+
})?
1045+
.drain_to(transaction.output[our_vout as usize].script_pubkey.clone())
1046+
.fee_rate(boosted_fee_rate);
1047+
1048+
match tx_builder.finish() {
1049+
Ok(psbt) => {
1050+
log_trace!(self.logger, "Created CPFP PSBT: {:?}", psbt);
1051+
psbt
1052+
},
1053+
Err(err) => {
1054+
log_error!(self.logger, "Failed to create CPFP transaction: {}", err);
1055+
return Err(err.into());
1056+
},
1057+
}
1058+
};
1059+
1060+
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
1061+
Ok(finalized) => {
1062+
if !finalized {
1063+
return Err(Error::OnchainTxCreationFailed);
1064+
}
1065+
},
1066+
Err(err) => {
1067+
log_error!(self.logger, "Failed to create transaction: {}", err);
1068+
return Err(err.into());
1069+
},
1070+
}
1071+
1072+
let mut locked_persister = self.persister.lock().unwrap();
1073+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
1074+
log_error!(self.logger, "Failed to persist wallet: {}", e);
1075+
Error::PersistenceFailed
1076+
})?;
1077+
1078+
let cpfp_tx = psbt.extract_tx().map_err(|e| {
1079+
log_error!(self.logger, "Failed to extract CPFP transaction: {}", e);
1080+
e
1081+
})?;
1082+
1083+
let cpfp_txid = cpfp_tx.compute_txid();
1084+
1085+
self.broadcaster.broadcast_transactions(&[&cpfp_tx]);
1086+
1087+
let new_fee = locked_wallet.calculate_fee(&cpfp_tx).unwrap_or(Amount::ZERO);
1088+
let new_fee_sats = new_fee.to_sat();
1089+
1090+
let payment_details = PaymentDetails {
1091+
id: PaymentId(cpfp_txid.to_byte_array()),
1092+
kind: PaymentKind::Onchain { txid: cpfp_txid, status: ConfirmationStatus::Unconfirmed },
1093+
amount_msat: Some(our_value.to_sat() * 1000),
1094+
fee_paid_msat: Some(new_fee_sats * 1000),
1095+
direction: PaymentDirection::Outbound,
1096+
status: PaymentStatus::Pending,
1097+
latest_update_timestamp: SystemTime::now()
1098+
.duration_since(UNIX_EPOCH)
1099+
.unwrap_or(Duration::from_secs(0))
1100+
.as_secs(),
1101+
};
1102+
self.payment_store.insert_or_update(payment_details)?;
1103+
1104+
log_info!(self.logger, "Created CPFP transaction {} to bump fee of {}", cpfp_txid, txid);
1105+
Ok(cpfp_txid)
1106+
}
9811107
}
9821108

9831109
impl Listen for Wallet {

tests/integration_tests_rust.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::sync::Arc;
1414
use bitcoin::address::NetworkUnchecked;
1515
use bitcoin::hashes::sha256::Hash as Sha256Hash;
1616
use bitcoin::hashes::Hash;
17-
use bitcoin::{Address, Amount, ScriptBuf};
17+
use bitcoin::{Address, Amount, ScriptBuf, Txid};
1818
use common::logging::{init_log_logger, validate_log_entry, MultiNodeLogger, TestLogWriter};
1919
use common::{
2020
bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle,
@@ -39,6 +39,7 @@ use lightning::routing::gossip::{NodeAlias, NodeId};
3939
use lightning::routing::router::RouteParametersConfig;
4040
use lightning_invoice::{Bolt11InvoiceDescription, Description};
4141
use lightning_types::payment::{PaymentHash, PaymentPreimage};
42+
4243
use log::LevelFilter;
4344

4445
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
@@ -2501,3 +2502,106 @@ async fn persistence_backwards_compatibility() {
25012502

25022503
node_new.stop().unwrap();
25032504
}
2505+
2506+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2507+
async fn test_fee_bump_cpfp() {
2508+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2509+
let chain_source = TestChainSource::Esplora(&electrsd);
2510+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
2511+
2512+
// Fund both nodes
2513+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2514+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2515+
2516+
let premine_amount_sat = 500_000;
2517+
premine_and_distribute_funds(
2518+
&bitcoind.client,
2519+
&electrsd.client,
2520+
vec![addr_a.clone(), addr_b.clone()],
2521+
Amount::from_sat(premine_amount_sat),
2522+
)
2523+
.await;
2524+
2525+
node_a.sync_wallets().unwrap();
2526+
node_b.sync_wallets().unwrap();
2527+
2528+
// Send a transaction from node_b to node_a that we'll later bump
2529+
let amount_to_send_sats = 100_000;
2530+
let txid =
2531+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
2532+
wait_for_tx(&electrsd.client, txid);
2533+
node_a.sync_wallets().unwrap();
2534+
node_b.sync_wallets().unwrap();
2535+
2536+
let payment_id = PaymentId(txid.to_byte_array());
2537+
let original_payment = node_b.payment(&payment_id).unwrap();
2538+
let original_fee = original_payment.fee_paid_msat.unwrap();
2539+
2540+
// Non-existent payment id
2541+
let fake_txid =
2542+
Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap();
2543+
let invalid_payment_id = PaymentId(fake_txid.to_byte_array());
2544+
assert_eq!(
2545+
Err(NodeError::InvalidPaymentId),
2546+
node_b.onchain_payment().bump_fee_cpfp(invalid_payment_id)
2547+
);
2548+
2549+
// Bump an outbound payment
2550+
assert_eq!(
2551+
Err(NodeError::InvalidPaymentId),
2552+
node_b.onchain_payment().bump_fee_cpfp(payment_id)
2553+
);
2554+
2555+
// Successful fee bump via CPFP
2556+
let new_txid = node_a.onchain_payment().bump_fee_cpfp(payment_id).unwrap();
2557+
wait_for_tx(&electrsd.client, new_txid);
2558+
2559+
// Sleep to allow for transaction propagation
2560+
std::thread::sleep(std::time::Duration::from_secs(5));
2561+
2562+
node_a.sync_wallets().unwrap();
2563+
node_b.sync_wallets().unwrap();
2564+
2565+
let new_payment_id = PaymentId(new_txid.to_byte_array());
2566+
let new_payment = node_a.payment(&new_payment_id).unwrap();
2567+
2568+
// Verify payment properties
2569+
assert_eq!(new_payment.amount_msat, Some(amount_to_send_sats * 1000));
2570+
assert_eq!(new_payment.direction, PaymentDirection::Outbound);
2571+
assert_eq!(new_payment.status, PaymentStatus::Pending);
2572+
2573+
// // Verify fee increased
2574+
assert!(
2575+
new_payment.fee_paid_msat > Some(original_fee),
2576+
"Fee should increase after RBF bump. Original: {}, New: {}",
2577+
original_fee,
2578+
new_payment.fee_paid_msat.unwrap()
2579+
);
2580+
2581+
// Confirm the transaction and try to bump again (should fail)
2582+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2583+
node_a.sync_wallets().unwrap();
2584+
node_b.sync_wallets().unwrap();
2585+
2586+
assert_eq!(
2587+
Err(NodeError::InvalidPaymentId),
2588+
node_a.onchain_payment().bump_fee_cpfp(payment_id)
2589+
);
2590+
2591+
// Verify final payment is confirmed
2592+
let final_payment = node_b.payment(&payment_id).unwrap();
2593+
assert_eq!(final_payment.status, PaymentStatus::Succeeded);
2594+
match final_payment.kind {
2595+
PaymentKind::Onchain { status, .. } => {
2596+
assert!(matches!(status, ConfirmationStatus::Confirmed { .. }));
2597+
},
2598+
_ => panic!("Unexpected payment kind"),
2599+
}
2600+
2601+
// Verify node A received the funds correctly
2602+
let node_a_received_payment =
2603+
node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { txid, .. }));
2604+
assert_eq!(node_a_received_payment.len(), 1);
2605+
assert_eq!(node_a_received_payment[0].amount_msat, Some(amount_to_send_sats * 1000));
2606+
assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded);
2607+
}

0 commit comments

Comments
 (0)