Skip to content

Commit 8279525

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 8279525

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed

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
@@ -1279,6 +1279,119 @@ impl Wallet {
12791279

12801280
Ok(new_txid)
12811281
}
1282+
1283+
#[allow(deprecated)]
1284+
pub(crate) fn bump_fee_cpfp(
1285+
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
1286+
) -> Result<Txid, Error> {
1287+
let txid = Txid::from_slice(&payment_id.0).expect("32 bytes");
1288+
1289+
let payment = self.pending_payment_store.get(&payment_id).ok_or(Error::InvalidPaymentId)?;
1290+
1291+
if let PaymentKind::Onchain { status, .. } = &payment.details.kind {
1292+
match status {
1293+
ConfirmationStatus::Confirmed { .. } => {
1294+
log_error!(self.logger, "Transaction {} is already confirmed", txid);
1295+
return Err(Error::InvalidPaymentId);
1296+
},
1297+
ConfirmationStatus::Unconfirmed => {},
1298+
}
1299+
}
1300+
1301+
let mut locked_wallet = self.inner.lock().unwrap();
1302+
1303+
let wallet_tx = locked_wallet.get_tx(txid).ok_or(Error::InvalidPaymentId)?;
1304+
let transaction = &wallet_tx.tx_node.tx;
1305+
1306+
// Create the CPFP transaction using a high fee rate to get it confirmed quickly.
1307+
let mut our_vout: Option<u32> = None;
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+
break;
1315+
}
1316+
}
1317+
1318+
let our_vout = our_vout.ok_or_else(|| {
1319+
log_error!(
1320+
self.logger,
1321+
"Could not find an output owned by this wallet in transaction {}",
1322+
txid
1323+
);
1324+
Error::InvalidPaymentId
1325+
})?;
1326+
1327+
let cpfp_outpoint = OutPoint::new(txid, our_vout);
1328+
1329+
let confirmation_target = ConfirmationTarget::OnchainPayment;
1330+
let estimated_fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
1331+
1332+
const CPFP_MULTIPLIER: f64 = 1.5;
1333+
let boosted_fee_rate = fee_rate.unwrap_or_else(|| {
1334+
FeeRate::from_sat_per_kwu(
1335+
((estimated_fee_rate.to_sat_per_kwu() as f64) * CPFP_MULTIPLIER) as u64,
1336+
)
1337+
});
1338+
1339+
let mut psbt = {
1340+
let mut tx_builder = locked_wallet.build_tx();
1341+
tx_builder
1342+
.add_utxo(cpfp_outpoint)
1343+
.map_err(|e| {
1344+
log_error!(self.logger, "Failed to add CPFP UTXO {}: {}", cpfp_outpoint, e);
1345+
Error::InvalidPaymentId
1346+
})?
1347+
.drain_to(transaction.output[our_vout as usize].script_pubkey.clone())
1348+
.fee_rate(boosted_fee_rate);
1349+
1350+
match tx_builder.finish() {
1351+
Ok(psbt) => {
1352+
log_trace!(self.logger, "Created CPFP PSBT: {:?}", psbt);
1353+
psbt
1354+
},
1355+
Err(err) => {
1356+
log_error!(self.logger, "Failed to create CPFP transaction: {}", err);
1357+
return Err(err.into());
1358+
},
1359+
}
1360+
};
1361+
1362+
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
1363+
Ok(finalized) => {
1364+
if !finalized {
1365+
return Err(Error::OnchainTxCreationFailed);
1366+
}
1367+
},
1368+
Err(err) => {
1369+
log_error!(self.logger, "Failed to create transaction: {}", err);
1370+
return Err(err.into());
1371+
},
1372+
}
1373+
1374+
let mut locked_persister = self.persister.lock().unwrap();
1375+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
1376+
log_error!(self.logger, "Failed to persist wallet: {}", e);
1377+
Error::PersistenceFailed
1378+
})?;
1379+
1380+
let cpfp_tx = psbt.extract_tx().map_err(|e| {
1381+
log_error!(self.logger, "Failed to extract CPFP transaction: {}", e);
1382+
e
1383+
})?;
1384+
1385+
let cpfp_txid = cpfp_tx.compute_txid();
1386+
1387+
self.broadcaster.broadcast_transactions(&[(
1388+
&cpfp_tx,
1389+
lightning::chain::chaininterface::TransactionType::Sweep { channels: vec![] },
1390+
)]);
1391+
1392+
log_info!(self.logger, "Created CPFP transaction {} to bump fee of {}", cpfp_txid, txid);
1393+
Ok(cpfp_txid)
1394+
}
12821395
}
12831396

12841397
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)]
@@ -2627,3 +2628,103 @@ 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 = random_chain_source(&bitcoind, &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).await;
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, None)
2673+
);
2674+
2675+
// Successful fee bump via CPFP
2676+
let new_txid = node_a.onchain_payment().bump_fee_cpfp(payment_id, None).unwrap();
2677+
wait_for_tx(&electrsd.client, new_txid).await;
2678+
2679+
// Sleep to allow for transaction propagation
2680+
std::thread::sleep(std::time::Duration::from_secs(5));
2681+
2682+
node_a.sync_wallets().unwrap();
2683+
node_b.sync_wallets().unwrap();
2684+
2685+
let new_payment_id = PaymentId(new_txid.to_byte_array());
2686+
let new_payment = node_a.payment(&new_payment_id).unwrap();
2687+
2688+
// Verify payment properties
2689+
assert_eq!(new_payment.direction, PaymentDirection::Outbound);
2690+
assert_eq!(new_payment.status, PaymentStatus::Pending);
2691+
2692+
// Verify fee increased
2693+
assert!(
2694+
new_payment.fee_paid_msat > Some(original_fee),
2695+
"Fee should increase after CPFP bump. Original: {}, New: {}",
2696+
original_fee,
2697+
new_payment.fee_paid_msat.unwrap()
2698+
);
2699+
2700+
// Confirm the transaction and try to bump again (should fail)
2701+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
2702+
node_a.sync_wallets().unwrap();
2703+
node_b.sync_wallets().unwrap();
2704+
2705+
assert_eq!(
2706+
Err(NodeError::InvalidPaymentId),
2707+
node_a.onchain_payment().bump_fee_cpfp(payment_id, None)
2708+
);
2709+
2710+
// Verify final payment is confirmed
2711+
let final_payment = node_b.payment(&payment_id).unwrap();
2712+
assert_eq!(final_payment.status, PaymentStatus::Succeeded);
2713+
match final_payment.kind {
2714+
PaymentKind::Onchain { status, .. } => {
2715+
assert!(matches!(status, ConfirmationStatus::Confirmed { .. }));
2716+
},
2717+
_ => panic!("Unexpected payment kind"),
2718+
}
2719+
2720+
// Verify the inbound payment (parent tx) is confirmed with the original amount.
2721+
let inbound_payment = node_a.payment(&payment_id).unwrap();
2722+
assert_eq!(inbound_payment.amount_msat, Some(amount_to_send_sats * 1000));
2723+
assert_eq!(inbound_payment.direction, PaymentDirection::Inbound);
2724+
assert_eq!(inbound_payment.status, PaymentStatus::Succeeded);
2725+
2726+
// Verify the CPFP child tx (self-spend) is also confirmed.
2727+
let cpfp_payment = node_a.payment(&new_payment_id).unwrap();
2728+
assert_eq!(cpfp_payment.direction, PaymentDirection::Outbound);
2729+
assert_eq!(cpfp_payment.status, PaymentStatus::Succeeded);
2730+
}

0 commit comments

Comments
 (0)