Skip to content

Commit 5c9976b

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 8bd7f4d commit 5c9976b

File tree

4 files changed

+276
-3
lines changed

4 files changed

+276
-3
lines changed

bindings/ldk_node.udl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ interface OnchainPayment {
231231
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
232232
[Throws=NodeError]
233233
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
234+
[Throws=NodeError]
235+
Txid bump_fee_cpfp(PaymentId payment_id);
234236
};
235237

236238
interface FeeRate {

src/payment/onchain.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::types::{ChannelManager, Wallet};
1414
use crate::wallet::OnchainSendAmount;
1515

1616
use bitcoin::{Address, Txid};
17+
use lightning::ln::channelmanager::PaymentId;
1718

1819
use std::sync::{Arc, RwLock};
1920

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

src/wallet/mod.rs

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger};
1212

1313
use crate::fee_estimator::{ConfirmationTarget, FeeEstimator};
1414
use crate::payment::store::ConfirmationStatus;
15-
use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus};
15+
use crate::payment::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
1616
use crate::types::PaymentStore;
1717
use crate::Error;
1818

@@ -46,13 +46,14 @@ use bitcoin::secp256k1::ecdh::SharedSecret;
4646
use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
4747
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing};
4848
use bitcoin::{
49-
Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash,
49+
Address, Amount, FeeRate, Network, OutPoint, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash,
5050
WitnessProgram, WitnessVersion,
5151
};
5252

5353
use std::ops::Deref;
5454
use std::str::FromStr;
5555
use std::sync::{Arc, Mutex};
56+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
5657

5758
pub(crate) enum OnchainSendAmount {
5859
ExactRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 },
@@ -568,6 +569,146 @@ where
568569

569570
Ok(txid)
570571
}
572+
573+
pub(crate) fn bump_fee_cpfp(&self, payment_id: PaymentId) -> Result<Txid, Error> {
574+
let txid = Txid::from_slice(&payment_id.0).expect("32 bytes");
575+
576+
let payment = self.payment_store.get(&payment_id).ok_or(Error::InvalidPaymentId)?;
577+
if payment.direction != PaymentDirection::Inbound {
578+
log_error!(self.logger, "Transaction {} is not an inbound payment", txid);
579+
return Err(Error::InvalidPaymentId);
580+
}
581+
582+
if let PaymentKind::Onchain { status, .. } = &payment.kind {
583+
match status {
584+
ConfirmationStatus::Confirmed { .. } => {
585+
log_error!(self.logger, "Transaction {} is already confirmed", txid);
586+
return Err(Error::InvalidPaymentId);
587+
},
588+
ConfirmationStatus::Unconfirmed => {},
589+
}
590+
}
591+
592+
let mut locked_wallet = self.inner.lock().unwrap();
593+
594+
let wallet_tx = locked_wallet.get_tx(txid).ok_or(Error::InvalidPaymentId)?;
595+
let transaction = &wallet_tx.tx_node.tx;
596+
let (sent, received) = locked_wallet.sent_and_received(transaction);
597+
598+
if sent > received {
599+
log_error!(
600+
self.logger,
601+
"Transaction {} is not an inbound payment (sent: {}, received: {})",
602+
txid,
603+
sent,
604+
received
605+
);
606+
return Err(Error::InvalidPaymentId);
607+
}
608+
609+
// Create the CPFP transaction using a high fee rate to get it confirmed quickly.
610+
let mut our_vout: Option<u32> = None;
611+
let mut our_value: Amount = Amount::ZERO;
612+
613+
for (vout_index, output) in transaction.output.iter().enumerate() {
614+
let script = output.script_pubkey.clone();
615+
616+
if locked_wallet.is_mine(script) {
617+
our_vout = Some(vout_index as u32);
618+
our_value = output.value.into();
619+
break;
620+
}
621+
}
622+
623+
let our_vout = our_vout.ok_or_else(|| {
624+
log_error!(
625+
self.logger,
626+
"Could not find an output owned by this wallet in transaction {}",
627+
txid
628+
);
629+
Error::InvalidPaymentId
630+
})?;
631+
632+
let cpfp_outpoint = OutPoint::new(txid, our_vout);
633+
634+
let confirmation_target = ConfirmationTarget::OnchainPayment;
635+
let estimated_fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);
636+
637+
const CPFP_MULTIPLIER: f64 = 1.5;
638+
let boosted_fee_rate = FeeRate::from_sat_per_kwu(
639+
((estimated_fee_rate.to_sat_per_kwu() as f64) * CPFP_MULTIPLIER) as u64,
640+
);
641+
642+
let mut psbt = {
643+
let mut tx_builder = locked_wallet.build_tx();
644+
tx_builder
645+
.add_utxo(cpfp_outpoint)
646+
.map_err(|e| {
647+
log_error!(self.logger, "Failed to add CPFP UTXO {}: {}", cpfp_outpoint, e);
648+
Error::InvalidPaymentId
649+
})?
650+
.drain_to(transaction.output[our_vout as usize].script_pubkey.clone())
651+
.fee_rate(boosted_fee_rate);
652+
653+
match tx_builder.finish() {
654+
Ok(psbt) => {
655+
log_trace!(self.logger, "Created CPFP PSBT: {:?}", psbt);
656+
psbt
657+
},
658+
Err(err) => {
659+
log_error!(self.logger, "Failed to create CPFP transaction: {}", err);
660+
return Err(err.into());
661+
},
662+
}
663+
};
664+
665+
match locked_wallet.sign(&mut psbt, SignOptions::default()) {
666+
Ok(finalized) => {
667+
if !finalized {
668+
return Err(Error::OnchainTxCreationFailed);
669+
}
670+
},
671+
Err(err) => {
672+
log_error!(self.logger, "Failed to create transaction: {}", err);
673+
return Err(err.into());
674+
},
675+
}
676+
677+
let mut locked_persister = self.persister.lock().unwrap();
678+
locked_wallet.persist(&mut locked_persister).map_err(|e| {
679+
log_error!(self.logger, "Failed to persist wallet: {}", e);
680+
Error::PersistenceFailed
681+
})?;
682+
683+
let cpfp_tx = psbt.extract_tx().map_err(|e| {
684+
log_error!(self.logger, "Failed to extract CPFP transaction: {}", e);
685+
e
686+
})?;
687+
688+
let cpfp_txid = cpfp_tx.compute_txid();
689+
690+
self.broadcaster.broadcast_transactions(&[&cpfp_tx]);
691+
692+
let new_fee = locked_wallet.calculate_fee(&cpfp_tx).unwrap_or(Amount::ZERO);
693+
let new_fee_sats = new_fee.to_sat();
694+
695+
let payment_details = PaymentDetails {
696+
id: PaymentId(cpfp_txid.to_byte_array()),
697+
kind: PaymentKind::Onchain { txid: cpfp_txid, status: ConfirmationStatus::Unconfirmed },
698+
amount_msat: Some(our_value.to_sat() * 1000),
699+
fee_paid_msat: Some(new_fee_sats * 1000),
700+
direction: PaymentDirection::Outbound,
701+
status: PaymentStatus::Pending,
702+
latest_update_timestamp: SystemTime::now()
703+
.duration_since(UNIX_EPOCH)
704+
.unwrap_or(Duration::from_secs(0))
705+
.as_secs(),
706+
};
707+
self.payment_store.insert_or_update(payment_details)?;
708+
709+
log_info!(self.logger, "Created CPFP transaction {} to bump fee of {}", cpfp_txid, txid);
710+
Ok(cpfp_txid)
711+
}
571712
}
572713

573714
impl<B: Deref, E: Deref, L: Deref> Listen for Wallet<B, E, L>

tests/integration_tests_rust.rs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ use lightning::util::persist::KVStore;
3333
use lightning_invoice::{Bolt11InvoiceDescription, Description};
3434
use lightning_types::payment::{PaymentHash, PaymentPreimage};
3535

36-
use bitcoin::address::NetworkUnchecked;
3736
use bitcoin::hashes::sha256::Hash as Sha256Hash;
3837
use bitcoin::hashes::Hash;
38+
use bitcoin::{address::NetworkUnchecked, Txid};
3939
use bitcoin::{Address, Amount, ScriptBuf};
4040
use log::LevelFilter;
4141

@@ -1699,3 +1699,105 @@ async fn drop_in_async_context() {
16991699
let node = setup_node(&chain_source, config, Some(seed_bytes));
17001700
node.stop().unwrap();
17011701
}
1702+
1703+
#[test]
1704+
fn test_fee_bump_cpfp() {
1705+
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
1706+
let chain_source = TestChainSource::Esplora(&electrsd);
1707+
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);
1708+
1709+
// Fund both nodes
1710+
let addr_a = node_a.onchain_payment().new_address().unwrap();
1711+
let addr_b = node_b.onchain_payment().new_address().unwrap();
1712+
1713+
let premine_amount_sat = 500_000;
1714+
premine_and_distribute_funds(
1715+
&bitcoind.client,
1716+
&electrsd.client,
1717+
vec![addr_a.clone(), addr_b.clone()],
1718+
Amount::from_sat(premine_amount_sat),
1719+
);
1720+
1721+
node_a.sync_wallets().unwrap();
1722+
node_b.sync_wallets().unwrap();
1723+
1724+
// Send a transaction from node_b to node_a that we'll later bump
1725+
let amount_to_send_sats = 100_000;
1726+
let txid =
1727+
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
1728+
wait_for_tx(&electrsd.client, txid);
1729+
node_a.sync_wallets().unwrap();
1730+
node_b.sync_wallets().unwrap();
1731+
1732+
let payment_id = PaymentId(txid.to_byte_array());
1733+
let original_payment = node_b.payment(&payment_id).unwrap();
1734+
let original_fee = original_payment.fee_paid_msat.unwrap();
1735+
1736+
// Non-existent payment id
1737+
let fake_txid =
1738+
Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap();
1739+
let invalid_payment_id = PaymentId(fake_txid.to_byte_array());
1740+
assert_eq!(
1741+
Err(NodeError::InvalidPaymentId),
1742+
node_b.onchain_payment().bump_fee_cpfp(invalid_payment_id)
1743+
);
1744+
1745+
// Bump an outbound payment
1746+
assert_eq!(
1747+
Err(NodeError::InvalidPaymentId),
1748+
node_b.onchain_payment().bump_fee_cpfp(payment_id)
1749+
);
1750+
1751+
// Successful fee bump via CPFP
1752+
let new_txid = node_a.onchain_payment().bump_fee_cpfp(payment_id).unwrap();
1753+
wait_for_tx(&electrsd.client, new_txid);
1754+
1755+
// Sleep to allow for transaction propagation
1756+
std::thread::sleep(std::time::Duration::from_secs(5));
1757+
1758+
node_a.sync_wallets().unwrap();
1759+
node_b.sync_wallets().unwrap();
1760+
1761+
let new_payment_id = PaymentId(new_txid.to_byte_array());
1762+
let new_payment = node_a.payment(&new_payment_id).unwrap();
1763+
1764+
// Verify payment properties
1765+
assert_eq!(new_payment.amount_msat, Some(amount_to_send_sats * 1000));
1766+
assert_eq!(new_payment.direction, PaymentDirection::Outbound);
1767+
assert_eq!(new_payment.status, PaymentStatus::Pending);
1768+
1769+
// // Verify fee increased
1770+
assert!(
1771+
new_payment.fee_paid_msat > Some(original_fee),
1772+
"Fee should increase after RBF bump. Original: {}, New: {}",
1773+
original_fee,
1774+
new_payment.fee_paid_msat.unwrap()
1775+
);
1776+
1777+
// Confirm the transaction and try to bump again (should fail)
1778+
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
1779+
node_a.sync_wallets().unwrap();
1780+
node_b.sync_wallets().unwrap();
1781+
1782+
assert_eq!(
1783+
Err(NodeError::InvalidPaymentId),
1784+
node_a.onchain_payment().bump_fee_cpfp(payment_id)
1785+
);
1786+
1787+
// Verify final payment is confirmed
1788+
let final_payment = node_b.payment(&payment_id).unwrap();
1789+
assert_eq!(final_payment.status, PaymentStatus::Succeeded);
1790+
match final_payment.kind {
1791+
PaymentKind::Onchain { status, .. } => {
1792+
assert!(matches!(status, ConfirmationStatus::Confirmed { .. }));
1793+
},
1794+
_ => panic!("Unexpected payment kind"),
1795+
}
1796+
1797+
// Verify node A received the funds correctly
1798+
let node_a_received_payment =
1799+
node_a.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { txid, .. }));
1800+
assert_eq!(node_a_received_payment.len(), 1);
1801+
assert_eq!(node_a_received_payment[0].amount_msat, Some(amount_to_send_sats * 1000));
1802+
assert_eq!(node_a_received_payment[0].status, PaymentStatus::Succeeded);
1803+
}

0 commit comments

Comments
 (0)