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
11 changes: 10 additions & 1 deletion bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dictionary Config {
u64 probing_liquidity_limit_multiplier;
AnchorChannelsConfig? anchor_channels_config;
SendingParameters? sending_parameters;
boolean auto_rebroadcast_unconfirmed_tx;
};

dictionary AnchorChannelsConfig {
Expand Down Expand Up @@ -231,6 +232,10 @@ interface OnchainPayment {
Txid send_to_address([ByRef]Address address, u64 amount_sats, FeeRate? fee_rate);
[Throws=NodeError]
Txid send_all_to_address([ByRef]Address address, boolean retain_reserve, FeeRate? fee_rate);
[Throws=NodeError]
void rebroadcast_transaction(PaymentId payment_id);
[Throws=NodeError]
Txid bump_fee_rbf(PaymentId payment_id);
};

interface FeeRate {
Expand Down Expand Up @@ -311,6 +316,7 @@ enum NodeError {
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
"InvalidTransaction",
};

dictionary NodeStatus {
Expand Down Expand Up @@ -409,7 +415,7 @@ interface ClosureReason {

[Enum]
interface PaymentKind {
Onchain(Txid txid, ConfirmationStatus status);
Onchain(Txid txid, ConfirmationStatus status, Transaction? raw_tx, u64? last_broadcast_time, u32? broadcast_attempts);
Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret);
Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits);
Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity);
Expand Down Expand Up @@ -865,3 +871,6 @@ typedef string OrderId;

[Custom]
typedef string DateTime;

[Custom]
typedef string Transaction;
61 changes: 61 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const DEFAULT_LDK_WALLET_SYNC_INTERVAL_SECS: u64 = 30;
const DEFAULT_FEE_RATE_CACHE_UPDATE_INTERVAL_SECS: u64 = 60 * 10;
const DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER: u64 = 3;
const DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS: u64 = 25_000;
const DEFAULT_MIN_REBROADCAST_INTERVAL_SECS: u64 = 300;
const DEFAULT_MAX_BROADCAST_ATTEMPTS: u32 = 24;
const DEFAULT_BACKOFF_FACTOR: f32 = 1.5;

/// The default log level.
pub const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Debug;
Expand Down Expand Up @@ -94,6 +97,9 @@ pub(crate) const RGS_SYNC_TIMEOUT_SECS: u64 = 5;
/// The length in bytes of our wallets' keys seed.
pub const WALLET_KEYS_SEED_LEN: usize = 64;

// The time in-between unconfirmed transaction broadcasts.
pub(crate) const UNCONFIRMED_TX_BROADCAST_INTERVAL: Duration = Duration::from_secs(300);

#[derive(Debug, Clone)]
/// Represents the configuration of an [`Node`] instance.
///
Expand All @@ -115,6 +121,7 @@ pub const WALLET_KEYS_SEED_LEN: usize = 64;
/// | `log_level` | Debug |
/// | `anchor_channels_config` | Some(..) |
/// | `sending_parameters` | None |
/// | `auto_rebroadcast_unconfirmed_tx` | true |
///
/// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their
/// respective default values.
Expand Down Expand Up @@ -179,6 +186,16 @@ pub struct Config {
/// **Note:** If unset, default parameters will be used, and you will be able to override the
/// parameters on a per-payment basis in the corresponding method calls.
pub sending_parameters: Option<SendingParameters>,
/// This will determine whether to automatically rebroadcast unconfirmed transactions
/// (e.g., channel funding or sweep transactions).
///
/// If enabled, the node will periodically attempt to rebroadcast any unconfirmed transactions to
/// increase propagation and confirmation likelihood. This is helpful in cases where transactions
/// were dropped by the mempool or not widely propagated.
///
/// Defaults to `true`. Disabling this may be desired for privacy-sensitive use cases or low-bandwidth
/// environments, but may result in slower or failed confirmations if transactions are not re-announced.
pub auto_rebroadcast_unconfirmed_tx: bool,
}

impl Default for Config {
Expand All @@ -193,6 +210,7 @@ impl Default for Config {
anchor_channels_config: Some(AnchorChannelsConfig::default()),
sending_parameters: None,
node_alias: None,
auto_rebroadcast_unconfirmed_tx: true,
}
}
}
Expand Down Expand Up @@ -534,6 +552,49 @@ impl From<MaxDustHTLCExposure> for LdkMaxDustHTLCExposure {
}
}

/// Policy for controlling transaction rebroadcasting behavior.
///
/// Determines the strategy for resending unconfirmed transactions to the network
/// to ensure they remain in mempools and eventually get confirmed.
#[derive(Clone, Debug)]
pub struct RebroadcastPolicy {
/// Minimum time between rebroadcast attempts in seconds.
///
/// This prevents excessive network traffic by ensuring a minimum delay
/// between consecutive rebroadcast attempts.
///
/// **Recommended values**: 60-600 seconds (1-10 minutes)
pub min_rebroadcast_interval_secs: u64,
/// Maximum number of broadcast attempts before giving up.
///
/// After reaching this limit, the transaction will no longer be rebroadcast
/// automatically. Manual intervention may be required.
///
/// **Recommended values**: 12-48 attempts
pub max_broadcast_attempts: u32,
/// Exponential backoff factor for increasing intervals between attempts.
///
/// Each subsequent rebroadcast wait time is multiplied by this factor,
/// creating an exponential backoff pattern.
///
/// - `1.0`: No backoff (constant interval)
/// - `1.5`: 50% increase each attempt
/// - `2.0`: 100% increase (doubling) each attempt
///
/// **Recommended values**: 1.2-2.0
pub backoff_factor: f32,
}

impl Default for RebroadcastPolicy {
fn default() -> Self {
Self {
min_rebroadcast_interval_secs: DEFAULT_MIN_REBROADCAST_INTERVAL_SECS,
max_broadcast_attempts: DEFAULT_MAX_BROADCAST_ATTEMPTS,
backoff_factor: DEFAULT_BACKOFF_FACTOR,
}
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;
Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ pub enum Error {
LiquiditySourceUnavailable,
/// The given operation failed due to the LSP's required opening fee being too high.
LiquidityFeeTooHigh,
/// The given transaction is invalid.
InvalidTransaction,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -193,6 +195,7 @@ impl fmt::Display for Error {
Self::LiquidityFeeTooHigh => {
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
},
Self::InvalidTransaction => write!(f, "The given transaction is invalid."),
}
}
}
Expand Down
17 changes: 16 additions & 1 deletion src/ffi/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub use lightning_invoice::{Description, SignedRawBolt11Invoice};
pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo;
pub use lightning_liquidity::lsps1::msgs::{OrderId, OrderParameters, PaymentState};

pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid};
pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Transaction, Txid};

pub use bip39::Mnemonic;

Expand Down Expand Up @@ -1117,6 +1117,21 @@ impl UniffiCustomTypeConverter for DateTime {
}
}

impl UniffiCustomTypeConverter for Transaction {
type Builtin = String;
fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
if let Some(bytes) = hex_utils::to_vec(&val) {
if let Ok(tx) = bitcoin::consensus::deserialize::<Transaction>(&bytes) {
return Ok(tx);
}
}
Err(Error::InvalidTransaction.into())
}
fn from_custom(obj: Self) -> Self::Builtin {
hex_utils::to_string(&bitcoin::consensus::serialize(&obj))
}
}

#[cfg(test)]
mod tests {
use std::{
Expand Down
29 changes: 28 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ pub use builder::NodeBuilder as Builder;
use chain::ChainSource;
use config::{
default_user_config, may_announce_channel, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL,
PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL,
PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, UNCONFIRMED_TX_BROADCAST_INTERVAL,
};
use connection::ConnectionManager;
use event::{EventHandler, EventQueue};
Expand Down Expand Up @@ -402,6 +402,33 @@ impl Node {
}
});

// Regularly rebroadcast unconfirmed transactions.
let rebroadcast_wallet = Arc::clone(&self.wallet);
let rebroadcast_logger = Arc::clone(&self.logger);
let mut stop_rebroadcast = self.stop_sender.subscribe();
if self.config.auto_rebroadcast_unconfirmed_tx {
self.runtime.spawn_cancellable_background_task(async move {
let mut interval = tokio::time::interval(UNCONFIRMED_TX_BROADCAST_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = stop_rebroadcast.changed() => {
log_debug!(
rebroadcast_logger,
"Stopping rebroadcasting unconfirmed transactions."
);
return;
}
_ = interval.tick() => {
if let Err(e) = rebroadcast_wallet.rebroadcast_unconfirmed_transactions() {
log_error!(rebroadcast_logger, "Background rebroadcast failed: {}", e);
}
}
}
}
});
}

// Regularly broadcast node announcements.
let bcast_cm = Arc::clone(&self.channel_manager);
let bcast_pm = Arc::clone(&self.peer_manager);
Expand Down
26 changes: 26 additions & 0 deletions src/payment/onchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::types::{ChannelManager, Wallet};
use crate::wallet::OnchainSendAmount;

use bitcoin::{Address, Txid};
use lightning::ln::channelmanager::PaymentId;

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

Expand Down Expand Up @@ -120,4 +121,29 @@ impl OnchainPayment {
let fee_rate_opt = maybe_map_fee_rate_opt!(fee_rate);
self.wallet.send_to_address(address, send_amount, fee_rate_opt)
}

/// Manually trigger a rebroadcast of a specific transaction according to the default policy.
///
/// This is useful if you suspect a transaction may not have propagated properly through the
/// network and you want to attempt to rebroadcast it immediately rather than waiting for the
/// automatic background job to handle it.
///
/// updating the attempt count and last broadcast time for the transaction in the payment store.
pub fn rebroadcast_transaction(&self, payment_id: PaymentId) -> Result<(), Error> {
self.wallet.rebroadcast_transaction(payment_id)
}

/// Attempt to bump the fee of an unconfirmed transaction using Replace-by-Fee (RBF).
///
/// This creates a new transaction that replaces the original one, increasing the fee by the
/// specified increment to improve its chances of confirmation. The original transaction must
/// be signaling RBF replaceability for this to succeed.
///
/// The new transaction will have the same outputs as the original but with a
/// higher fee, resulting in faster confirmation potential.
///
/// Returns the Txid of the new replacement transaction if successful.
pub fn bump_fee_rbf(&self, payment_id: PaymentId) -> Result<Txid, Error> {
self.wallet.bump_fee_rbf(payment_id)
}
}
62 changes: 57 additions & 5 deletions src/payment/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use lightning::{

use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret};

use bitcoin::{BlockHash, Txid};
use bitcoin::{BlockHash, Transaction, Txid};

use std::time::{Duration, SystemTime, UNIX_EPOCH};

Expand Down Expand Up @@ -293,6 +293,33 @@ impl StorableObject for PaymentDetails {
}
}

if let Some(tx) = &update.raw_tx {
match self.kind {
PaymentKind::Onchain { ref mut raw_tx, .. } => {
update_if_necessary!(*raw_tx, tx.clone());
},
_ => {},
}
}

if let Some(attempts) = update.broadcast_attempts {
match self.kind {
PaymentKind::Onchain { ref mut broadcast_attempts, .. } => {
update_if_necessary!(*broadcast_attempts, attempts);
},
_ => {},
}
}

if let Some(broadcast_time) = update.last_broadcast_time {
match self.kind {
PaymentKind::Onchain { ref mut last_broadcast_time, .. } => {
update_if_necessary!(*last_broadcast_time, broadcast_time);
},
_ => {},
}
}

if updated {
self.latest_update_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
Expand Down Expand Up @@ -353,6 +380,12 @@ pub enum PaymentKind {
txid: Txid,
/// The confirmation status of this payment.
status: ConfirmationStatus,
/// The raw transaction for rebroadcasting
raw_tx: Option<Transaction>,
/// Last broadcast attempt timestamp (UNIX seconds)
last_broadcast_time: Option<u64>,
/// Number of broadcast attempts
broadcast_attempts: Option<u32>,
},
/// A [BOLT 11] payment.
///
Expand Down Expand Up @@ -450,7 +483,10 @@ pub enum PaymentKind {
impl_writeable_tlv_based_enum!(PaymentKind,
(0, Onchain) => {
(0, txid, required),
(1, raw_tx, option),
(2, status, required),
(3, last_broadcast_time, option),
(5, broadcast_attempts, option),
},
(2, Bolt11) => {
(0, hash, required),
Expand Down Expand Up @@ -542,6 +578,9 @@ pub(crate) struct PaymentDetailsUpdate {
pub direction: Option<PaymentDirection>,
pub status: Option<PaymentStatus>,
pub confirmation_status: Option<ConfirmationStatus>,
pub raw_tx: Option<Option<Transaction>>,
pub last_broadcast_time: Option<Option<u64>>,
pub broadcast_attempts: Option<Option<u32>>,
}

impl PaymentDetailsUpdate {
Expand All @@ -557,6 +596,9 @@ impl PaymentDetailsUpdate {
direction: None,
status: None,
confirmation_status: None,
raw_tx: None,
last_broadcast_time: None,
broadcast_attempts: None,
}
}
}
Expand All @@ -572,10 +614,17 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
_ => (None, None, None),
};

let confirmation_status = match value.kind {
PaymentKind::Onchain { status, .. } => Some(status),
_ => None,
};
let (confirmation_status, raw_tx, last_broadcast_time, broadcast_attempts) =
match &value.kind {
PaymentKind::Onchain {
status,
raw_tx,
last_broadcast_time,
broadcast_attempts,
..
} => (Some(*status), raw_tx.clone(), *last_broadcast_time, *broadcast_attempts),
_ => (None, None, None, None),
};

let counterparty_skimmed_fee_msat = match value.kind {
PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => {
Expand All @@ -595,6 +644,9 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate {
direction: Some(value.direction),
status: Some(value.status),
confirmation_status,
raw_tx: Some(raw_tx),
last_broadcast_time: Some(last_broadcast_time),
broadcast_attempts: Some(broadcast_attempts),
}
}
}
Expand Down
Loading
Loading