Skip to content

Commit 0632467

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 1cf1e6f commit 0632467

3 files changed

Lines changed: 237 additions & 0 deletions

File tree

src/payment/onchain.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,27 @@ impl OnchainPayment {
141141
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
142142
self.wallet.bump_fee_rbf(payment_id, fee_rate_opt)
143143
}
144+
145+
/// Bumps the fee of a given UTXO using Child-Pays-For-Parent (CPFP) by creating a new transaction.
146+
///
147+
/// This method creates a new transaction that spends the specified UTXO with a higher fee rate,
148+
/// effectively increasing the priority of both the new transaction and the parent transaction
149+
/// it depends on. This is useful when a transaction is stuck in the mempool due to insufficient
150+
/// fees and you want to accelerate its confirmation.
151+
///
152+
/// CPFP works by creating a child transaction that spends one or more outputs from the parent
153+
/// transaction. Miners will consider the combined fees of both transactions when deciding
154+
/// which transactions to include in a block.
155+
///
156+
/// # Parameters
157+
/// * `payment_id` - The identifier of the payment whose UTXO should be fee-bumped
158+
/// * `fee_rate` - The fee rate to use for the CPFP transaction, if not provided, a reasonable fee rate is used
159+
///
160+
/// Returns the [`Txid`] of the newly created CPFP transaction if successful.
161+
pub fn bump_fee_cpfp(
162+
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
163+
) -> Result<Txid, Error> {
164+
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
165+
self.wallet.bump_fee_cpfp(payment_id, fee_rate_opt)
166+
}
144167
}

src/wallet/mod.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,6 +1403,119 @@ impl Wallet {
14031403

14041404
Ok(new_txid)
14051405
}
1406+
1407+
#[allow(deprecated)]
1408+
pub(crate) fn bump_fee_cpfp(
1409+
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
1410+
) -> Result<Txid, Error> {
1411+
let txid = Txid::from_slice(&payment_id.0).expect("32 bytes");
1412+
1413+
let payment = self.pending_payment_store.get(&payment_id).ok_or(Error::InvalidPaymentId)?;
1414+
1415+
if let PaymentKind::Onchain { status, .. } = &payment.details.kind {
1416+
match status {
1417+
ConfirmationStatus::Confirmed { .. } => {
1418+
log_error!(self.logger, "Transaction {} is already confirmed", txid);
1419+
return Err(Error::InvalidPaymentId);
1420+
},
1421+
ConfirmationStatus::Unconfirmed => {},
1422+
}
1423+
}
1424+
1425+
let mut locked_wallet = self.inner.lock().unwrap();
1426+
1427+
let wallet_tx = locked_wallet.get_tx(txid).ok_or(Error::InvalidPaymentId)?;
1428+
let transaction = &wallet_tx.tx_node.tx;
1429+
1430+
// Create the CPFP transaction using a high fee rate to get it confirmed quickly.
1431+
let mut our_vout: Option<u32> = None;
1432+
1433+
for (vout_index, output) in transaction.output.iter().enumerate() {
1434+
let script = output.script_pubkey.clone();
1435+
1436+
if locked_wallet.is_mine(script) {
1437+
our_vout = Some(vout_index as u32);
1438+
break;
1439+
}
1440+
}
1441+
1442+
let our_vout = our_vout.ok_or_else(|| {
1443+
log_error!(
1444+
self.logger,
1445+
"Could not find an output owned by this wallet in transaction {}",
1446+
txid
1447+
);
1448+
Error::InvalidPaymentId
1449+
})?;
1450+
1451+
let cpfp_outpoint = OutPoint::new(txid, our_vout);
1452+
1453+
let confirmation_target = ConfirmationTarget::OnchainPayment;
1454+
let estimated_fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
1455+
1456+
const CPFP_MULTIPLIER: f64 = 1.5;
1457+
let boosted_fee_rate = fee_rate.unwrap_or_else(|| {
1458+
FeeRate::from_sat_per_kwu(
1459+
((estimated_fee_rate.to_sat_per_kwu() as f64) * CPFP_MULTIPLIER) as u64,
1460+
)
1461+
});
1462+
1463+
let mut psbt = {
1464+
let mut tx_builder = locked_wallet.build_tx();
1465+
tx_builder
1466+
.add_utxo(cpfp_outpoint)
1467+
.map_err(|e| {
1468+
log_error!(self.logger, "Failed to add CPFP UTXO {}: {}", cpfp_outpoint, e);
1469+
Error::InvalidPaymentId
1470+
})?
1471+
.drain_to(transaction.output[our_vout as usize].script_pubkey.clone())
1472+
.fee_rate(boosted_fee_rate);
1473+
1474+
match tx_builder.finish() {
1475+
Ok(psbt) => {
1476+
log_trace!(self.logger, "Created CPFP PSBT: {:?}", psbt);
1477+
psbt
1478+
},
1479+
Err(err) => {
1480+
log_error!(self.logger, "Failed to create CPFP transaction: {}", err);
1481+
return Err(err.into());
1482+
},
1483+
}
1484+
};
1485+
1486+
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
1487+
Ok(finalized) => {
1488+
if !finalized {
1489+
return Err(Error::OnchainTxCreationFailed);
1490+
}
1491+
},
1492+
Err(err) => {
1493+
log_error!(self.logger, "Failed to create transaction: {}", err);
1494+
return Err(err.into());
1495+
},
1496+
}
1497+
1498+
let mut locked_persister = self.persister.lock().unwrap();
1499+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
1500+
log_error!(self.logger, "Failed to persist wallet: {}", e);
1501+
Error::PersistenceFailed
1502+
})?;
1503+
1504+
let cpfp_tx = psbt.extract_tx().map_err(|e| {
1505+
log_error!(self.logger, "Failed to extract CPFP transaction: {}", e);
1506+
e
1507+
})?;
1508+
1509+
let cpfp_txid = cpfp_tx.compute_txid();
1510+
1511+
self.broadcaster.broadcast_transactions(&[(
1512+
&cpfp_tx,
1513+
lightning::chain::chaininterface::TransactionType::Sweep { channels: vec![] },
1514+
)]);
1515+
1516+
log_info!(self.logger, "Created CPFP transaction {} to bump fee of {}", cpfp_txid, txid);
1517+
Ok(cpfp_txid)
1518+
}
14061519
}
14071520

14081521
impl Listen for Wallet {

tests/integration_tests_rust.rs

Lines changed: 101 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)]
@@ -2809,3 +2810,103 @@ async fn splice_in_with_all_balance() {
28092810
node_a.stop().unwrap();
28102811
node_b.stop().unwrap();
28112812
}
2813+
2814+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
2815+
async fn test_fee_bump_cpfp() {
2816+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
2817+
let chain_source = random_chain_source(&bitcoind, &electrsd);
2818+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
2819+
2820+
// Fund both nodes
2821+
let addr_a = node_a.onchain_payment().new_address().unwrap();
2822+
let addr_b = node_b.onchain_payment().new_address().unwrap();
2823+
2824+
let premine_amount_sat = 500_000;
2825+
premine_and_distribute_funds(
2826+
&bitcoind.client,
2827+
&electrsd.client,
2828+
vec![addr_a.clone(), addr_b.clone()],
2829+
Amount::from_sat(premine_amount_sat),
2830+
)
2831+
.await;
2832+
2833+
node_a.sync_wallets().unwrap();
2834+
node_b.sync_wallets().unwrap();
2835+
2836+
// Send a transaction from node_b to node_a that we'll later bump
2837+
let amount_to_send_sats = 100_000;
2838+
let txid =
2839+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
2840+
wait_for_tx(&electrsd.client, txid).await;
2841+
node_a.sync_wallets().unwrap();
2842+
node_b.sync_wallets().unwrap();
2843+
2844+
let payment_id = PaymentId(txid.to_byte_array());
2845+
let original_payment = node_b.payment(&payment_id).unwrap();
2846+
let original_fee = original_payment.fee_paid_msat.unwrap();
2847+
2848+
// Non-existent payment id
2849+
let fake_txid =
2850+
Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap();
2851+
let invalid_payment_id = PaymentId(fake_txid.to_byte_array());
2852+
assert_eq!(
2853+
Err(NodeError::InvalidPaymentId),
2854+
node_b.onchain_payment().bump_fee_cpfp(invalid_payment_id, None)
2855+
);
2856+
2857+
// Successful fee bump via CPFP
2858+
let new_txid = node_a.onchain_payment().bump_fee_cpfp(payment_id, None).unwrap();
2859+
wait_for_tx(&electrsd.client, new_txid).await;
2860+
2861+
// Sleep to allow for transaction propagation
2862+
std::thread::sleep(std::time::Duration::from_secs(5));
2863+
2864+
node_a.sync_wallets().unwrap();
2865+
node_b.sync_wallets().unwrap();
2866+
2867+
let new_payment_id = PaymentId(new_txid.to_byte_array());
2868+
let new_payment = node_a.payment(&new_payment_id).unwrap();
2869+
2870+
// Verify payment properties
2871+
assert_eq!(new_payment.direction, PaymentDirection::Outbound);
2872+
assert_eq!(new_payment.status, PaymentStatus::Pending);
2873+
2874+
// Verify fee increased
2875+
assert!(
2876+
new_payment.fee_paid_msat > Some(original_fee),
2877+
"Fee should increase after CPFP bump. Original: {}, New: {}",
2878+
original_fee,
2879+
new_payment.fee_paid_msat.unwrap()
2880+
);
2881+
2882+
// Confirm the transaction and try to bump again (should fail)
2883+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2884+
node_a.sync_wallets().unwrap();
2885+
node_b.sync_wallets().unwrap();
2886+
2887+
assert_eq!(
2888+
Err(NodeError::InvalidPaymentId),
2889+
node_a.onchain_payment().bump_fee_cpfp(payment_id, None)
2890+
);
2891+
2892+
// Verify final payment is confirmed
2893+
let final_payment = node_b.payment(&payment_id).unwrap();
2894+
assert_eq!(final_payment.status, PaymentStatus::Succeeded);
2895+
match final_payment.kind {
2896+
PaymentKind::Onchain { status, .. } => {
2897+
assert!(matches!(status, ConfirmationStatus::Confirmed { .. }));
2898+
},
2899+
_ => panic!("Unexpected payment kind"),
2900+
}
2901+
2902+
// Verify the inbound payment (parent tx) is confirmed with the original amount.
2903+
let inbound_payment = node_a.payment(&payment_id).unwrap();
2904+
assert_eq!(inbound_payment.amount_msat, Some(amount_to_send_sats * 1000));
2905+
assert_eq!(inbound_payment.direction, PaymentDirection::Inbound);
2906+
assert_eq!(inbound_payment.status, PaymentStatus::Succeeded);
2907+
2908+
// Verify the CPFP child tx (self-spend) is also confirmed.
2909+
let cpfp_payment = node_a.payment(&new_payment_id).unwrap();
2910+
assert_eq!(cpfp_payment.direction, PaymentDirection::Outbound);
2911+
assert_eq!(cpfp_payment.status, PaymentStatus::Succeeded);
2912+
}

0 commit comments

Comments
 (0)