Skip to content

Commit 5936f63

Browse files
committed
Implement RBF fee bumping for unconfirmed transactions
Add `Replace-by-Fee` functionality to allow users to increase fees on pending outbound transactions, improving confirmation likelihood during network congestion. - Uses BDK's `build_fee_bump` for transaction replacement - Validates transaction eligibility: must be outbound and unconfirmed - Implements fee rate estimation with safety limits - Maintains payment history consistency across wallet updates - Includes integration tests for various RBF scenarios
1 parent a8bda04 commit 5936f63

8 files changed

Lines changed: 471 additions & 172 deletions

File tree

bindings/ldk_node.udl

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ interface OnchainPayment {
269269
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
270270
[Throws=NodeError]
271271
void rebroadcast_transaction(PaymentId payment_id);
272+
[Throws=NodeError]
273+
Txid bump_fee_rbf(PaymentId payment_id);
272274
};
273275

274276
interface FeeRate {
@@ -454,7 +456,7 @@ interface ClosureReason {
454456

455457
[Enum]
456458
interface PaymentKind {
457-
Onchain(Txid txid, ConfirmationStatus status, Transaction? raw_tx, u64? last_broadcast_time, u32? broadcast_attempts);
459+
Onchain(Txid txid, ConfirmationStatus status);
458460
Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret);
459461
Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits);
460462
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
@@ -920,6 +922,3 @@ typedef string LSPSDateTime;
920922

921923
[Custom]
922924
typedef string ScriptBuf;
923-
924-
[Custom]
925-
typedef string Transaction;

src/ffi/types.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,21 +1248,6 @@ impl UniffiCustomTypeConverter for LSPSDateTime {
12481248
}
12491249
}
12501250

1251-
impl UniffiCustomTypeConverter for Transaction {
1252-
type Builtin = String;
1253-
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
1254-
if let Some(bytes) = hex_utils::to_vec(&val) {
1255-
if let Ok(tx) = bitcoin::consensus::deserialize::<Transaction>(&bytes) {
1256-
return Ok(tx);
1257-
}
1258-
}
1259-
Err(Error::InvalidTransaction.into())
1260-
}
1261-
fn from_custom(obj: Self) -> Self::Builtin {
1262-
hex_utils::to_string(&bitcoin::consensus::serialize(&obj))
1263-
}
1264-
}
1265-
12661251
#[cfg(test)]
12671252
mod tests {
12681253
use std::num::NonZeroU64;

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ pub use builder::NodeBuilder as Builder;
124124
use chain::ChainSource;
125125
use config::{
126126
default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config,
127-
NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, UNCONFIRMED_TX_BROADCAST_INTERVAL,
127+
NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL,
128+
UNCONFIRMED_TX_BROADCAST_INTERVAL,
128129
};
129130
use connection::ConnectionManager;
130131
pub use error::Error as NodeError;

src/payment/onchain.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ use crate::logger::{log_info, LdkLogger, Logger};
1717
use crate::types::{ChannelManager, Wallet};
1818
use crate::wallet::OnchainSendAmount;
1919

20-
use bitcoin::{Address, Txid};
2120
use lightning::ln::channelmanager::PaymentId;
2221

23-
use std::sync::{Arc, RwLock};
24-
2522
#[cfg(not(feature = "uniffi"))]
2623
type FeeRate = bitcoin::FeeRate;
2724
#[cfg(feature = "uniffi")]
@@ -136,4 +133,18 @@ impl OnchainPayment {
136133
pub fn rebroadcast_transaction(&self, payment_id: PaymentId) -> Result<(), Error> {
137134
self.wallet.rebroadcast_transaction(payment_id)
138135
}
136+
137+
/// Attempt to bump the fee of an unconfirmed transaction using Replace-by-Fee (RBF).
138+
///
139+
/// This creates a new transaction that replaces the original one, increasing the fee by the
140+
/// specified increment to improve its chances of confirmation. The original transaction must
141+
/// be signaling RBF replaceability for this to succeed.
142+
///
143+
/// The new transaction will have the same outputs as the original but with a
144+
/// higher fee, resulting in faster confirmation potential.
145+
///
146+
/// Returns the Txid of the new replacement transaction if successful.
147+
pub fn bump_fee_rbf(&self, payment_id: PaymentId) -> Result<Txid, Error> {
148+
self.wallet.bump_fee_rbf(payment_id)
149+
}
139150
}

src/payment/pending_payment_store.rs

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
66
// accordance with one or both of these licenses.
77

8-
use bitcoin::Txid;
8+
use bitcoin::{Transaction, Txid};
99
use lightning::{impl_writeable_tlv_based, ln::channelmanager::PaymentId};
1010

1111
use crate::{
@@ -20,11 +20,20 @@ pub struct PendingPaymentDetails {
2020
pub details: PaymentDetails,
2121
/// Transaction IDs that have replaced or conflict with this payment.
2222
pub conflicting_txids: Vec<Txid>,
23+
/// The raw transaction for rebroadcasting
24+
pub raw_tx: Option<Transaction>,
25+
/// Last broadcast attempt timestamp (UNIX seconds)
26+
pub last_broadcast_time: Option<u64>,
27+
/// Number of broadcast attempts
28+
pub broadcast_attempts: Option<u32>,
2329
}
2430

2531
impl PendingPaymentDetails {
26-
pub(crate) fn new(details: PaymentDetails, conflicting_txids: Vec<Txid>) -> Self {
27-
Self { details, conflicting_txids }
32+
pub(crate) fn new(
33+
details: PaymentDetails, conflicting_txids: Vec<Txid>, raw_tx: Option<Transaction>,
34+
last_broadcast_time: Option<u64>, broadcast_attempts: Option<u32>,
35+
) -> Self {
36+
Self { details, conflicting_txids, raw_tx, last_broadcast_time, broadcast_attempts }
2837
}
2938

3039
/// Convert to finalized payment for the main payment store
@@ -36,13 +45,19 @@ impl PendingPaymentDetails {
3645
impl_writeable_tlv_based!(PendingPaymentDetails, {
3746
(0, details, required),
3847
(2, conflicting_txids, optional_vec),
48+
(3, raw_tx, option),
49+
(5, last_broadcast_time, option),
50+
(7, broadcast_attempts, option),
3951
});
4052

4153
#[derive(Clone, Debug, PartialEq, Eq)]
4254
pub(crate) struct PendingPaymentDetailsUpdate {
4355
pub id: PaymentId,
4456
pub payment_update: Option<PaymentDetailsUpdate>,
4557
pub conflicting_txids: Option<Vec<Txid>>,
58+
pub raw_tx: Option<Option<Transaction>>,
59+
pub last_broadcast_time: Option<Option<u64>>,
60+
pub broadcast_attempts: Option<Option<u32>>,
4661
}
4762

4863
impl StorableObject for PendingPaymentDetails {
@@ -56,18 +71,39 @@ impl StorableObject for PendingPaymentDetails {
5671
fn update(&mut self, update: &Self::Update) -> bool {
5772
let mut updated = false;
5873

74+
macro_rules! update_if_necessary {
75+
($val:expr, $update:expr) => {
76+
if $val != $update {
77+
$val = $update;
78+
updated = true;
79+
}
80+
};
81+
}
82+
5983
// Update the underlying payment details if present
6084
if let Some(payment_update) = &update.payment_update {
6185
updated |= self.details.update(payment_update);
6286
}
6387

6488
if let Some(new_conflicting_txids) = &update.conflicting_txids {
65-
if &self.conflicting_txids != new_conflicting_txids {
66-
self.conflicting_txids = new_conflicting_txids.clone();
67-
updated = true;
89+
// Don't overwrite existing conflicts with an empty list
90+
if !new_conflicting_txids.is_empty() {
91+
update_if_necessary!(self.conflicting_txids, new_conflicting_txids.clone());
6892
}
6993
}
7094

95+
if let Some(new_raw_tx) = &update.raw_tx {
96+
update_if_necessary!(self.raw_tx, new_raw_tx.clone());
97+
}
98+
99+
if let Some(new_last_broadcast_time) = update.last_broadcast_time {
100+
update_if_necessary!(self.last_broadcast_time, new_last_broadcast_time);
101+
}
102+
103+
if let Some(new_broadcast_attempts) = update.broadcast_attempts {
104+
update_if_necessary!(self.broadcast_attempts, new_broadcast_attempts);
105+
}
106+
71107
updated
72108
}
73109

@@ -88,6 +124,9 @@ impl From<&PendingPaymentDetails> for PendingPaymentDetailsUpdate {
88124
id: value.id(),
89125
payment_update: Some(value.details.to_update()),
90126
conflicting_txids: Some(value.conflicting_txids.clone()),
127+
raw_tx: Some(value.raw_tx.clone()),
128+
last_broadcast_time: Some(value.last_broadcast_time),
129+
broadcast_attempts: Some(value.broadcast_attempts),
91130
}
92131
}
93132
}

src/payment/store.rs

Lines changed: 10 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ use lightning::{
1919
use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};
2020
use lightning_types::string::UntrustedString;
2121

22-
use bitcoin::{BlockHash, Transaction, Txid};
23-
24-
use std::time::{Duration, SystemTime, UNIX_EPOCH};
25-
2622
use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate};
2723
use crate::hex_utils;
2824

@@ -295,28 +291,10 @@ impl StorableObject for PaymentDetails {
295291
}
296292
}
297293

298-
if let Some(tx) = &update.raw_tx {
299-
match self.kind {
300-
PaymentKind::Onchain { ref mut raw_tx, .. } => {
301-
update_if_necessary!(*raw_tx, tx.clone());
302-
},
303-
_ => {},
304-
}
305-
}
306-
307-
if let Some(attempts) = update.broadcast_attempts {
294+
if let Some(tx_id) = update.txid {
308295
match self.kind {
309-
PaymentKind::Onchain { ref mut broadcast_attempts, .. } => {
310-
update_if_necessary!(*broadcast_attempts, attempts);
311-
},
312-
_ => {},
313-
}
314-
}
315-
316-
if let Some(broadcast_time) = update.last_broadcast_time {
317-
match self.kind {
318-
PaymentKind::Onchain { ref mut last_broadcast_time, .. } => {
319-
update_if_necessary!(*last_broadcast_time, broadcast_time);
296+
PaymentKind::Onchain { ref mut txid, .. } => {
297+
update_if_necessary!(*txid, tx_id);
320298
},
321299
_ => {},
322300
}
@@ -382,12 +360,6 @@ pub enum PaymentKind {
382360
txid: Txid,
383361
/// The confirmation status of this payment.
384362
status: ConfirmationStatus,
385-
/// The raw transaction for rebroadcasting
386-
raw_tx: Option<Transaction>,
387-
/// Last broadcast attempt timestamp (UNIX seconds)
388-
last_broadcast_time: Option<u64>,
389-
/// Number of broadcast attempts
390-
broadcast_attempts: Option<u32>,
391363
},
392364
/// A [BOLT 11] payment.
393365
///
@@ -485,10 +457,7 @@ pub enum PaymentKind {
485457
impl_writeable_tlv_based_enum!(PaymentKind,
486458
(0, Onchain) => {
487459
(0, txid, required),
488-
(1, raw_tx, option),
489460
(2, status, required),
490-
(3, last_broadcast_time, option),
491-
(5, broadcast_attempts, option),
492461
},
493462
(2, Bolt11) => {
494463
(0, hash, required),
@@ -580,9 +549,7 @@ pub(crate) struct PaymentDetailsUpdate {
580549
pub direction: Option<PaymentDirection>,
581550
pub status: Option<PaymentStatus>,
582551
pub confirmation_status: Option<ConfirmationStatus>,
583-
pub raw_tx: Option<Option<Transaction>>,
584-
pub last_broadcast_time: Option<Option<u64>>,
585-
pub broadcast_attempts: Option<Option<u32>>,
552+
pub txid: Option<Txid>,
586553
}
587554

588555
impl PaymentDetailsUpdate {
@@ -598,9 +565,7 @@ impl PaymentDetailsUpdate {
598565
direction: None,
599566
status: None,
600567
confirmation_status: None,
601-
raw_tx: None,
602-
last_broadcast_time: None,
603-
broadcast_attempts: None,
568+
txid: None,
604569
}
605570
}
606571
}
@@ -616,17 +581,10 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
616581
_ => (None, None, None),
617582
};
618583

619-
let (confirmation_status, raw_tx, last_broadcast_time, broadcast_attempts) =
620-
match &value.kind {
621-
PaymentKind::Onchain {
622-
status,
623-
raw_tx,
624-
last_broadcast_time,
625-
broadcast_attempts,
626-
..
627-
} => (Some(*status), raw_tx.clone(), *last_broadcast_time, *broadcast_attempts),
628-
_ => (None, None, None, None),
629-
};
584+
let (confirmation_status, txid) = match &value.kind {
585+
PaymentKind::Onchain { status, txid, .. } => (Some(*status), Some(*txid)),
586+
_ => (None, None),
587+
};
630588

631589
let counterparty_skimmed_fee_msat = match value.kind {
632590
PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => {
@@ -646,9 +604,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
646604
direction: Some(value.direction),
647605
status: Some(value.status),
648606
confirmation_status,
649-
raw_tx: Some(raw_tx),
650-
last_broadcast_time: Some(last_broadcast_time),
651-
broadcast_attempts: Some(broadcast_attempts),
607+
txid,
652608
}
653609
}
654610
}

0 commit comments

Comments
 (0)