diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index cc16690e2..e27d4d012 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -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, + ) -> Result { + let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate); + self.wallet.bump_fee_cpfp(payment_id, fee_rate_opt) + } } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 20b96c747..6c30ee31d 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -1403,6 +1403,119 @@ impl Wallet { Ok(new_txid) } + + #[allow(deprecated)] + pub(crate) fn bump_fee_cpfp( + &self, payment_id: PaymentId, fee_rate: Option, + ) -> Result { + 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 = 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 { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 5eb5a08af..76fad16d0 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -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)] @@ -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); +}