Skip to content

Commit 7f3e93b

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 8cd670f commit 7f3e93b

File tree

3 files changed

+259
-0
lines changed

3 files changed

+259
-0
lines changed

src/payment/onchain.rs

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

21+
use lightning::ln::channelmanager::PaymentId;
22+
2123
#[cfg(not(feature = "uniffi"))]
2224
type FeeRate = bitcoin::FeeRate;
2325
#[cfg(feature = "uniffi")]
@@ -141,4 +143,31 @@ impl OnchainPayment {
141143
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
142144
self.wallet.bump_fee_rbf(payment_id, fee_rate_opt)
143145
}
146+
147+
/// Bumps the fee of a given UTXO using Child-Pays-For-Parent (CPFP) by creating a new transaction.
148+
///
149+
/// This method creates a new transaction that spends the specified UTXO with a higher fee rate,
150+
/// effectively increasing the priority of both the new transaction and the parent transaction
151+
/// it depends on. This is useful when a transaction is stuck in the mempool due to insufficient
152+
/// fees and you want to accelerate its confirmation.
153+
///
154+
/// CPFP works by creating a child transaction that spends one or more outputs from the parent
155+
/// transaction. Miners will consider the combined fees of both transactions when deciding
156+
/// which transactions to include in a block.
157+
///
158+
/// # Parameters
159+
/// * `payment_id` - The identifier of the payment whose UTXO should be fee-bumped
160+
///
161+
/// # Returns
162+
/// * `Ok(Txid)` - The transaction ID of the newly created CPFP transaction on success
163+
/// * `Err(Error)` - If the payment cannot be found, the UTXO is not suitable for CPFP,
164+
/// or if there's an error creating the transaction
165+
///
166+
/// # Note
167+
/// CPFP is specifically designed to work with unconfirmed UTXOs. The child transaction
168+
/// can spend outputs from unconfirmed parent transactions, allowing miners to consider
169+
/// the combined fees of both transactions when building a block.
170+
pub fn bump_fee_cpfp(&self, payment_id: PaymentId) -> Result<Txid, Error> {
171+
self.wallet.bump_fee_cpfp(payment_id)
172+
}
144173
}

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;
@@ -1279,6 +1280,131 @@ impl Wallet {
12791280

12801281
Ok(new_txid)
12811282
}
1283+
1284+
#[allow(deprecated)]
1285+
pub(crate) fn bump_fee_cpfp(&self, payment_id: PaymentId) -> Result<Txid, Error> {
1286+
let txid = Txid::from_slice(&payment_id.0).expect("32 bytes");
1287+
1288+
let payment = self.pending_payment_store.get(&payment_id).ok_or(Error::InvalidPaymentId)?;
1289+
1290+
if let PaymentKind::Onchain { status, .. } = &payment.details.kind {
1291+
match status {
1292+
ConfirmationStatus::Confirmed { .. } => {
1293+
log_error!(self.logger, "Transaction {} is already confirmed", txid);
1294+
return Err(Error::InvalidPaymentId);
1295+
},
1296+
ConfirmationStatus::Unconfirmed => {},
1297+
}
1298+
}
1299+
1300+
let mut locked_wallet = self.inner.lock().unwrap();
1301+
1302+
let wallet_tx = locked_wallet.get_tx(txid).ok_or(Error::InvalidPaymentId)?;
1303+
let transaction = &wallet_tx.tx_node.tx;
1304+
1305+
// Create the CPFP transaction using a high fee rate to get it confirmed quickly.
1306+
let mut our_vout: Option<u32> = None;
1307+
let mut our_value: Amount = Amount::ZERO;
1308+
1309+
for (vout_index, output) in transaction.output.iter().enumerate() {
1310+
let script = output.script_pubkey.clone();
1311+
1312+
if locked_wallet.is_mine(script) {
1313+
our_vout = Some(vout_index as u32);
1314+
our_value = output.value.into();
1315+
break;
1316+
}
1317+
}
1318+
1319+
let our_vout = our_vout.ok_or_else(|| {
1320+
log_error!(
1321+
self.logger,
1322+
"Could not find an output owned by this wallet in transaction {}",
1323+
txid
1324+
);
1325+
Error::InvalidPaymentId
1326+
})?;
1327+
1328+
let cpfp_outpoint = OutPoint::new(txid, our_vout);
1329+
1330+
let confirmation_target = ConfirmationTarget::OnchainPayment;
1331+
let estimated_fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
1332+
1333+
const CPFP_MULTIPLIER: f64 = 1.5;
1334+
let boosted_fee_rate = FeeRate::from_sat_per_kwu(
1335+
((estimated_fee_rate.to_sat_per_kwu() as f64) * CPFP_MULTIPLIER) as u64,
1336+
);
1337+
1338+
let mut psbt = {
1339+
let mut tx_builder = locked_wallet.build_tx();
1340+
tx_builder
1341+
.add_utxo(cpfp_outpoint)
1342+
.map_err(|e| {
1343+
log_error!(self.logger, "Failed to add CPFP UTXO {}: {}", cpfp_outpoint, e);
1344+
Error::InvalidPaymentId
1345+
})?
1346+
.drain_to(transaction.output[our_vout as usize].script_pubkey.clone())
1347+
.fee_rate(boosted_fee_rate);
1348+
1349+
match tx_builder.finish() {
1350+
Ok(psbt) => {
1351+
log_trace!(self.logger, "Created CPFP PSBT: {:?}", psbt);
1352+
psbt
1353+
},
1354+
Err(err) => {
1355+
log_error!(self.logger, "Failed to create CPFP transaction: {}", err);
1356+
return Err(err.into());
1357+
},
1358+
}
1359+
};
1360+
1361+
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
1362+
Ok(finalized) => {
1363+
if !finalized {
1364+
return Err(Error::OnchainTxCreationFailed);
1365+
}
1366+
},
1367+
Err(err) => {
1368+
log_error!(self.logger, "Failed to create transaction: {}", err);
1369+
return Err(err.into());
1370+
},
1371+
}
1372+
1373+
let mut locked_persister = self.persister.lock().unwrap();
1374+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
1375+
log_error!(self.logger, "Failed to persist wallet: {}", e);
1376+
Error::PersistenceFailed
1377+
})?;
1378+
1379+
let cpfp_tx = psbt.extract_tx().map_err(|e| {
1380+
log_error!(self.logger, "Failed to extract CPFP transaction: {}", e);
1381+
e
1382+
})?;
1383+
1384+
let cpfp_txid = cpfp_tx.compute_txid();
1385+
1386+
self.broadcaster.broadcast_transactions(&[&cpfp_tx]);
1387+
1388+
let new_fee = locked_wallet.calculate_fee(&cpfp_tx).unwrap_or(Amount::ZERO);
1389+
let new_fee_sats = new_fee.to_sat();
1390+
1391+
let payment_details = PaymentDetails {
1392+
id: PaymentId(cpfp_txid.to_byte_array()),
1393+
kind: PaymentKind::Onchain { txid: cpfp_txid, status: ConfirmationStatus::Unconfirmed },
1394+
amount_msat: Some(our_value.to_sat() * 1000),
1395+
fee_paid_msat: Some(new_fee_sats * 1000),
1396+
direction: PaymentDirection::Outbound,
1397+
status: PaymentStatus::Pending,
1398+
latest_update_timestamp: SystemTime::now()
1399+
.duration_since(UNIX_EPOCH)
1400+
.unwrap_or(Duration::from_secs(0))
1401+
.as_secs(),
1402+
};
1403+
self.payment_store.insert_or_update(payment_details)?;
1404+
1405+
log_info!(self.logger, "Created CPFP transaction {} to bump fee of {}", cpfp_txid, txid);
1406+
Ok(cpfp_txid)
1407+
}
12821408
}
12831409

12841410
impl Listen for Wallet {

tests/integration_tests_rust.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
@@ -2627,3 +2628,106 @@ async fn onchain_fee_bump_rbf() {
26272628
assert_eq!(node_a_received_payment[0].amount_msat, Some(amount_to_send_sats * 1000));
26282629
assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded);
26292630
}
2631+
2632+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2633+
async fn test_fee_bump_cpfp() {
2634+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2635+
let chain_source = TestChainSource::Esplora(&electrsd);
2636+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
2637+
2638+
// Fund both nodes
2639+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2640+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2641+
2642+
let premine_amount_sat = 500_000;
2643+
premine_and_distribute_funds(
2644+
&bitcoind.client,
2645+
&electrsd.client,
2646+
vec![addr_a.clone(), addr_b.clone()],
2647+
Amount::from_sat(premine_amount_sat),
2648+
)
2649+
.await;
2650+
2651+
node_a.sync_wallets().unwrap();
2652+
node_b.sync_wallets().unwrap();
2653+
2654+
// Send a transaction from node_b to node_a that we'll later bump
2655+
let amount_to_send_sats = 100_000;
2656+
let txid =
2657+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
2658+
wait_for_tx(&electrsd.client, txid);
2659+
node_a.sync_wallets().unwrap();
2660+
node_b.sync_wallets().unwrap();
2661+
2662+
let payment_id = PaymentId(txid.to_byte_array());
2663+
let original_payment = node_b.payment(&payment_id).unwrap();
2664+
let original_fee = original_payment.fee_paid_msat.unwrap();
2665+
2666+
// Non-existent payment id
2667+
let fake_txid =
2668+
Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap();
2669+
let invalid_payment_id = PaymentId(fake_txid.to_byte_array());
2670+
assert_eq!(
2671+
Err(NodeError::InvalidPaymentId),
2672+
node_b.onchain_payment().bump_fee_cpfp(invalid_payment_id)
2673+
);
2674+
2675+
// Bump an outbound payment
2676+
assert_eq!(
2677+
Err(NodeError::InvalidPaymentId),
2678+
node_b.onchain_payment().bump_fee_cpfp(payment_id)
2679+
);
2680+
2681+
// Successful fee bump via CPFP
2682+
let new_txid = node_a.onchain_payment().bump_fee_cpfp(payment_id).unwrap();
2683+
wait_for_tx(&electrsd.client, new_txid);
2684+
2685+
// Sleep to allow for transaction propagation
2686+
std::thread::sleep(std::time::Duration::from_secs(5));
2687+
2688+
node_a.sync_wallets().unwrap();
2689+
node_b.sync_wallets().unwrap();
2690+
2691+
let new_payment_id = PaymentId(new_txid.to_byte_array());
2692+
let new_payment = node_a.payment(&new_payment_id).unwrap();
2693+
2694+
// Verify payment properties
2695+
assert_eq!(new_payment.amount_msat, Some(amount_to_send_sats * 1000));
2696+
assert_eq!(new_payment.direction, PaymentDirection::Outbound);
2697+
assert_eq!(new_payment.status, PaymentStatus::Pending);
2698+
2699+
// // Verify fee increased
2700+
assert!(
2701+
new_payment.fee_paid_msat > Some(original_fee),
2702+
"Fee should increase after RBF bump. Original: {}, New: {}",
2703+
original_fee,
2704+
new_payment.fee_paid_msat.unwrap()
2705+
);
2706+
2707+
// Confirm the transaction and try to bump again (should fail)
2708+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2709+
node_a.sync_wallets().unwrap();
2710+
node_b.sync_wallets().unwrap();
2711+
2712+
assert_eq!(
2713+
Err(NodeError::InvalidPaymentId),
2714+
node_a.onchain_payment().bump_fee_cpfp(payment_id)
2715+
);
2716+
2717+
// Verify final payment is confirmed
2718+
let final_payment = node_b.payment(&payment_id).unwrap();
2719+
assert_eq!(final_payment.status, PaymentStatus::Succeeded);
2720+
match final_payment.kind {
2721+
PaymentKind::Onchain { status, .. } => {
2722+
assert!(matches!(status, ConfirmationStatus::Confirmed { .. }));
2723+
},
2724+
_ => panic!("Unexpected payment kind"),
2725+
}
2726+
2727+
// Verify node A received the funds correctly
2728+
let node_a_received_payment =
2729+
node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { txid, .. }));
2730+
assert_eq!(node_a_received_payment.len(), 1);
2731+
assert_eq!(node_a_received_payment[0].amount_msat, Some(amount_to_send_sats * 1000));
2732+
assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded);
2733+
}

0 commit comments

Comments
 (0)