Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/payment/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,27 @@ impl OnchainPayment {
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
self.wallet.bump_fee_rbf(payment_id, fee_rate_opt)
}

/// Bumps the fee of a given UTXO using Child-Pays-For-Parent (CPFP) by creating a new transaction.
///
/// This method creates a new transaction that spends the specified UTXO with a higher fee rate,
/// effectively increasing the priority of both the new transaction and the parent transaction
/// it depends on. This is useful when a transaction is stuck in the mempool due to insufficient
/// fees and you want to accelerate its confirmation.
///
/// CPFP works by creating a child transaction that spends one or more outputs from the parent
/// transaction. Miners will consider the combined fees of both transactions when deciding
/// which transactions to include in a block.
///
/// # Parameters
/// * `payment_id` - The identifier of the payment whose UTXO should be fee-bumped
/// * `fee_rate` - The fee rate to use for the CPFP transaction, if not provided, a reasonable fee rate is used
///
/// Returns the [`Txid`] of the newly created CPFP transaction if successful.
pub fn bump_fee_cpfp(
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
) -> Result<Txid, Error> {
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
self.wallet.bump_fee_cpfp(payment_id, fee_rate_opt)
}
}
113 changes: 113 additions & 0 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1403,6 +1403,119 @@ impl Wallet {

Ok(new_txid)
}

#[allow(deprecated)]
pub(crate) fn bump_fee_cpfp(
&self, payment_id: PaymentId, fee_rate: Option<FeeRate>,
) -> Result<Txid, Error> {
let txid = Txid::from_slice(&payment_id.0).expect("32 bytes");

let payment = self.pending_payment_store.get(&payment_id).ok_or(Error::InvalidPaymentId)?;

if let PaymentKind::Onchain { status, .. } = &payment.details.kind {
match status {
ConfirmationStatus::Confirmed { .. } => {
log_error!(self.logger, "Transaction {} is already confirmed", txid);
return Err(Error::InvalidPaymentId);
},
ConfirmationStatus::Unconfirmed => {},
}
}

let mut locked_wallet = self.inner.lock().unwrap();

let wallet_tx = locked_wallet.get_tx(txid).ok_or(Error::InvalidPaymentId)?;
let transaction = &wallet_tx.tx_node.tx;

// Create the CPFP transaction using a high fee rate to get it confirmed quickly.
let mut our_vout: Option<u32> = None;

for (vout_index, output) in transaction.output.iter().enumerate() {
let script = output.script_pubkey.clone();

if locked_wallet.is_mine(script) {
our_vout = Some(vout_index as u32);
break;
}
}

let our_vout = our_vout.ok_or_else(|| {
log_error!(
self.logger,
"Could not find an output owned by this wallet in transaction {}",
txid
);
Error::InvalidPaymentId
})?;

let cpfp_outpoint = OutPoint::new(txid, our_vout);

let confirmation_target = ConfirmationTarget::OnchainPayment;
let estimated_fee_rate = self.fee_estimator.estimate_fee_rate(confirmation_target);

const CPFP_MULTIPLIER: f64 = 1.5;
let boosted_fee_rate = fee_rate.unwrap_or_else(|| {
FeeRate::from_sat_per_kwu(
((estimated_fee_rate.to_sat_per_kwu() as f64) * CPFP_MULTIPLIER) as u64,
)
});

let mut psbt = {
let mut tx_builder = locked_wallet.build_tx();
tx_builder
.add_utxo(cpfp_outpoint)
.map_err(|e| {
log_error!(self.logger, "Failed to add CPFP UTXO {}: {}", cpfp_outpoint, e);
Error::InvalidPaymentId
})?
.drain_to(transaction.output[our_vout as usize].script_pubkey.clone())
.fee_rate(boosted_fee_rate);

match tx_builder.finish() {
Ok(psbt) => {
log_trace!(self.logger, "Created CPFP PSBT: {:?}", psbt);
psbt
},
Err(err) => {
log_error!(self.logger, "Failed to create CPFP transaction: {}", err);
return Err(err.into());
},
}
};

match locked_wallet.sign(&mut psbt, SignOptions::default()) {
Ok(finalized) => {
if !finalized {
return Err(Error::OnchainTxCreationFailed);
}
},
Err(err) => {
log_error!(self.logger, "Failed to create transaction: {}", err);
return Err(err.into());
},
}

let mut locked_persister = self.persister.lock().unwrap();
locked_wallet.persist(&mut locked_persister).map_err(|e| {
log_error!(self.logger, "Failed to persist wallet: {}", e);
Error::PersistenceFailed
})?;

let cpfp_tx = psbt.extract_tx().map_err(|e| {
log_error!(self.logger, "Failed to extract CPFP transaction: {}", e);
e
})?;

let cpfp_txid = cpfp_tx.compute_txid();

self.broadcaster.broadcast_transactions(&[(
&cpfp_tx,
lightning::chain::chaininterface::TransactionType::Sweep { channels: vec![] },
)]);

log_info!(self.logger, "Created CPFP transaction {} to bump fee of {}", cpfp_txid, txid);
Ok(cpfp_txid)
}
}

impl Listen for Wallet {
Expand Down
101 changes: 101 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ use lightning::routing::gossip::{NodeAlias, NodeId};
use lightning::routing::router::RouteParametersConfig;
use lightning_invoice::{Bolt11InvoiceDescription, Description};
use lightning_types::payment::{PaymentHash, PaymentPreimage};

use log::LevelFilter;

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
Expand Down Expand Up @@ -2809,3 +2810,103 @@ async fn splice_in_with_all_balance() {
node_a.stop().unwrap();
node_b.stop().unwrap();
}

#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
async fn test_fee_bump_cpfp() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
let chain_source = random_chain_source(&bitcoind, &electrsd);
let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false);

// Fund both nodes
let addr_a = node_a.onchain_payment().new_address().unwrap();
let addr_b = node_b.onchain_payment().new_address().unwrap();

let premine_amount_sat = 500_000;
premine_and_distribute_funds(
&bitcoind.client,
&electrsd.client,
vec![addr_a.clone(), addr_b.clone()],
Amount::from_sat(premine_amount_sat),
)
.await;

node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

// Send a transaction from node_b to node_a that we'll later bump
let amount_to_send_sats = 100_000;
let txid =
node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap();
wait_for_tx(&electrsd.client, txid).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

let payment_id = PaymentId(txid.to_byte_array());
let original_payment = node_b.payment(&payment_id).unwrap();
let original_fee = original_payment.fee_paid_msat.unwrap();

// Non-existent payment id
let fake_txid =
Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap();
let invalid_payment_id = PaymentId(fake_txid.to_byte_array());
assert_eq!(
Err(NodeError::InvalidPaymentId),
node_b.onchain_payment().bump_fee_cpfp(invalid_payment_id, None)
);

// Successful fee bump via CPFP
let new_txid = node_a.onchain_payment().bump_fee_cpfp(payment_id, None).unwrap();
wait_for_tx(&electrsd.client, new_txid).await;

// Sleep to allow for transaction propagation
std::thread::sleep(std::time::Duration::from_secs(5));

node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

let new_payment_id = PaymentId(new_txid.to_byte_array());
let new_payment = node_a.payment(&new_payment_id).unwrap();

// Verify payment properties
assert_eq!(new_payment.direction, PaymentDirection::Outbound);
assert_eq!(new_payment.status, PaymentStatus::Pending);

// Verify fee increased
assert!(
new_payment.fee_paid_msat > Some(original_fee),
"Fee should increase after CPFP bump. Original: {}, New: {}",
original_fee,
new_payment.fee_paid_msat.unwrap()
);

// Confirm the transaction and try to bump again (should fail)
generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await;
node_a.sync_wallets().unwrap();
node_b.sync_wallets().unwrap();

assert_eq!(
Err(NodeError::InvalidPaymentId),
node_a.onchain_payment().bump_fee_cpfp(payment_id, None)
);

// Verify final payment is confirmed
let final_payment = node_b.payment(&payment_id).unwrap();
assert_eq!(final_payment.status, PaymentStatus::Succeeded);
match final_payment.kind {
PaymentKind::Onchain { status, .. } => {
assert!(matches!(status, ConfirmationStatus::Confirmed { .. }));
},
_ => panic!("Unexpected payment kind"),
}

// Verify the inbound payment (parent tx) is confirmed with the original amount.
let inbound_payment = node_a.payment(&payment_id).unwrap();
assert_eq!(inbound_payment.amount_msat, Some(amount_to_send_sats * 1000));
assert_eq!(inbound_payment.direction, PaymentDirection::Inbound);
assert_eq!(inbound_payment.status, PaymentStatus::Succeeded);

// Verify the CPFP child tx (self-spend) is also confirmed.
let cpfp_payment = node_a.payment(&new_payment_id).unwrap();
assert_eq!(cpfp_payment.direction, PaymentDirection::Outbound);
assert_eq!(cpfp_payment.status, PaymentStatus::Succeeded);
}
Loading