Skip to content

Commit a52b1e5

Browse files
committed
Enhance onchain transaction management
- Implement background job for rebroadcasting unconfirmed transactions with max attempt limit - Add RBF (Replace-by-Fee) functionality allowing users to bump fees on outbound unconfirmed transactions
1 parent fdaa759 commit a52b1e5

File tree

7 files changed

+707
-40
lines changed

7 files changed

+707
-40
lines changed

bindings/ldk_node.udl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,10 @@ 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+
void rebroadcast_transaction(PaymentId payment_id);
236+
[Throws=NodeError]
237+
Txid bump_fee_rbf(PaymentId payment_id);
234238
};
235239

236240
interface FeeRate {
@@ -409,7 +413,7 @@ interface ClosureReason {
409413

410414
[Enum]
411415
interface PaymentKind {
412-
Onchain(Txid txid, ConfirmationStatus status);
416+
Onchain(Txid txid, ConfirmationStatus status, sequence<u8>? raw_tx, u64? last_broadcast_time, u32? broadcast_attempts);
413417
Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret);
414418
Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits);
415419
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);

src/config.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ pub(crate) const RGS_SYNC_TIMEOUT_SECS: u64 = 5;
9494
/// The length in bytes of our wallets' keys seed.
9595
pub const WALLET_KEYS_SEED_LEN: usize = 64;
9696

97+
// The time in-between unconfirmed transaction broadcasts.
98+
pub(crate) const UNCONFIRMED_TX_BROADCAST_INTERVAL: Duration = Duration::from_secs(300);
99+
97100
#[derive(Debug, Clone)]
98101
/// Represents the configuration of an [`Node`] instance.
99102
///
@@ -534,6 +537,45 @@ impl From<MaxDustHTLCExposure> for LdkMaxDustHTLCExposure {
534537
}
535538
}
536539

540+
/// Policy for controlling transaction rebroadcasting behavior.
541+
///
542+
/// Determines the strategy for resending unconfirmed transactions to the network
543+
/// to ensure they remain in mempools and eventually get confirmed.
544+
#[derive(Clone, Debug)]
545+
pub struct RebroadcastPolicy {
546+
/// Minimum time between rebroadcast attempts in seconds.
547+
///
548+
/// This prevents excessive network traffic by ensuring a minimum delay
549+
/// between consecutive rebroadcast attempts.
550+
///
551+
/// **Recommended values**: 60-600 seconds (1-10 minutes)
552+
pub min_rebroadcast_interval: u64,
553+
/// Maximum number of broadcast attempts before giving up.
554+
///
555+
/// After reaching this limit, the transaction will no longer be rebroadcast
556+
/// automatically. Manual intervention may be required.
557+
///
558+
/// **Recommended values**: 12-48 attempts
559+
pub max_broadcast_attempts: u32,
560+
/// Exponential backoff factor for increasing intervals between attempts.
561+
///
562+
/// Each subsequent rebroadcast wait time is multiplied by this factor,
563+
/// creating an exponential backoff pattern.
564+
///
565+
/// - `1.0`: No backoff (constant interval)
566+
/// - `1.5`: 50% increase each attempt
567+
/// - `2.0`: 100% increase (doubling) each attempt
568+
///
569+
/// **Recommended values**: 1.2-2.0
570+
pub backoff_factor: f32,
571+
}
572+
573+
impl Default for RebroadcastPolicy {
574+
fn default() -> Self {
575+
Self { min_rebroadcast_interval: 300, max_broadcast_attempts: 24, backoff_factor: 1.5 }
576+
}
577+
}
578+
537579
#[cfg(test)]
538580
mod tests {
539581
use std::str::FromStr;

src/lib.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ pub use builder::NodeBuilder as Builder;
129129
use chain::ChainSource;
130130
use config::{
131131
default_user_config, may_announce_channel, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL,
132-
PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL,
132+
PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, UNCONFIRMED_TX_BROADCAST_INTERVAL,
133133
};
134134
use connection::ConnectionManager;
135135
use event::{EventHandler, EventQueue};
@@ -402,6 +402,31 @@ impl Node {
402402
}
403403
});
404404

405+
// Regularly rebroadcast unconfirmed transactions.
406+
let rebroadcast_wallet = Arc::clone(&self.wallet);
407+
let rebroadcast_logger = Arc::clone(&self.logger);
408+
let mut stop_rebroadcast = self.stop_sender.subscribe();
409+
self.runtime.spawn_cancellable_background_task(async move {
410+
let mut interval = tokio::time::interval(UNCONFIRMED_TX_BROADCAST_INTERVAL);
411+
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
412+
loop {
413+
tokio::select! {
414+
_ = stop_rebroadcast.changed() => {
415+
log_debug!(
416+
rebroadcast_logger,
417+
"Stopping rebroadcasting unconfirmed transactions."
418+
);
419+
return;
420+
}
421+
_ = interval.tick() => {
422+
if let Err(e) = rebroadcast_wallet.rebroadcast_unconfirmed_transactions() {
423+
log_error!(rebroadcast_logger, "Background rebroadcast failed: {}", e);
424+
}
425+
}
426+
}
427+
}
428+
});
429+
405430
// Regularly broadcast node announcements.
406431
let bcast_cm = Arc::clone(&self.channel_manager);
407432
let bcast_pm = Arc::clone(&self.peer_manager);

src/payment/onchain.rs

Lines changed: 27 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,30 @@ 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+
/// Manually trigger a rebroadcast of a specific transaction according to the default policy.
126+
///
127+
/// This is useful if you suspect a transaction may not have propagated properly through the
128+
/// network and you want to attempt to rebroadcast it immediately rather than waiting for the
129+
/// automatic background job to handle it.
130+
///
131+
/// updating the attempt count and last broadcast time for the transaction in the payment store.
132+
pub fn rebroadcast_transaction(&self, payment_id: PaymentId) -> Result<(), Error> {
133+
self.wallet.rebroadcast_transaction(payment_id)?;
134+
Ok(())
135+
}
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+
}
123150
}

src/payment/store.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,24 @@ impl StorableObject for PaymentDetails {
293293
}
294294
}
295295

296+
if let Some(attempts) = update.broadcast_attempts {
297+
match self.kind {
298+
PaymentKind::Onchain { ref mut broadcast_attempts, .. } => {
299+
update_if_necessary!(*broadcast_attempts, attempts);
300+
},
301+
_ => {},
302+
}
303+
}
304+
305+
if let Some(broadcast_time) = update.last_broadcast_time {
306+
match self.kind {
307+
PaymentKind::Onchain { ref mut last_broadcast_time, .. } => {
308+
update_if_necessary!(*last_broadcast_time, broadcast_time);
309+
},
310+
_ => {},
311+
}
312+
}
313+
296314
if updated {
297315
self.latest_update_timestamp = SystemTime::now()
298316
.duration_since(UNIX_EPOCH)
@@ -353,6 +371,12 @@ pub enum PaymentKind {
353371
txid: Txid,
354372
/// The confirmation status of this payment.
355373
status: ConfirmationStatus,
374+
/// The raw transaction for rebroadcasting
375+
raw_tx: Option<Vec<u8>>,
376+
/// Last broadcast attempt timestamp (UNIX seconds)
377+
last_broadcast_time: Option<u64>,
378+
/// Number of broadcast attempts
379+
broadcast_attempts: Option<u32>,
356380
},
357381
/// A [BOLT 11] payment.
358382
///
@@ -451,6 +475,9 @@ impl_writeable_tlv_based_enum!(PaymentKind,
451475
(0, Onchain) => {
452476
(0, txid, required),
453477
(2, status, required),
478+
(4, raw_tx, option),
479+
(10, last_broadcast_time, option),
480+
(12, broadcast_attempts, option),
454481
},
455482
(2, Bolt11) => {
456483
(0, hash, required),
@@ -542,6 +569,8 @@ pub(crate) struct PaymentDetailsUpdate {
542569
pub direction: Option<PaymentDirection>,
543570
pub status: Option<PaymentStatus>,
544571
pub confirmation_status: Option<ConfirmationStatus>,
572+
pub last_broadcast_time: Option<Option<u64>>,
573+
pub broadcast_attempts: Option<Option<u32>>,
545574
}
546575

547576
impl PaymentDetailsUpdate {
@@ -557,6 +586,8 @@ impl PaymentDetailsUpdate {
557586
direction: None,
558587
status: None,
559588
confirmation_status: None,
589+
last_broadcast_time: None,
590+
broadcast_attempts: None,
560591
}
561592
}
562593
}
@@ -572,9 +603,11 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
572603
_ => (None, None, None),
573604
};
574605

575-
let confirmation_status = match value.kind {
576-
PaymentKind::Onchain { status, .. } => Some(status),
577-
_ => None,
606+
let (confirmation_status, last_broadcast_time, broadcast_attempts) = match value.kind {
607+
PaymentKind::Onchain { status, last_broadcast_time, broadcast_attempts, .. } => {
608+
(Some(status), last_broadcast_time, broadcast_attempts)
609+
},
610+
_ => (None, None, None),
578611
};
579612

580613
let counterparty_skimmed_fee_msat = match value.kind {
@@ -595,6 +628,8 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
595628
direction: Some(value.direction),
596629
status: Some(value.status),
597630
confirmation_status,
631+
last_broadcast_time: Some(last_broadcast_time),
632+
broadcast_attempts: Some(broadcast_attempts),
598633
}
599634
}
600635
}

0 commit comments

Comments
 (0)