Skip to content

Commit dbaf6d9

Browse files
jkczyzclaude
andcommitted
Periodically rebroadcast unconfirmed funding and splice transactions
ChannelManager broadcast channel funding and splice transactions exactly once, at signing. If the broadcast was lost -- peer drop, wallet restart, mempool eviction, low fee race -- nothing resent the transaction, and the channel could stall waiting for a confirmation that would never come. The BroadcasterInterface doc already promised LDK would rebroadcast transactions that hadn't made it into a block; this makes that accurate for funding and splice. Each timer tick re-emits every channel's unconfirmed funding or splice transaction through the existing broadcaster, stopping once the on-chain confirmation height is set. The behavior self-heals on reorg, since the confirmation height resets to zero there. For V1 batch funding, per-tick deduping by txid merges each channel's single-element TransactionType::Funding list into one broadcast call naming every participating channel. Includes a failing test documenting a known zero-conf issue: a splice can be promoted before its tx confirms, after which the rebroadcast emits TransactionType::Funding instead of TransactionType::Splice. The fix is pending team feedback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 86dcede commit dbaf6d9

4 files changed

Lines changed: 563 additions & 9 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3159,6 +3159,15 @@ impl PendingFunding {
31593159
},
31603160
}
31613161
}
3162+
3163+
/// Returns the txid of the prior negotiated candidate that the latest one replaces, or
3164+
/// `None` for the first splice attempt (when there is no prior candidate).
3165+
fn last_replaced_txid(&self) -> Option<Txid> {
3166+
self.negotiated_candidates
3167+
.len()
3168+
.checked_sub(2)
3169+
.and_then(|idx| self.negotiated_candidates[idx].get_funding_txid())
3170+
}
31623171
}
31633172

31643173
#[derive(Debug)]
@@ -7201,6 +7210,79 @@ where
72017210
)
72027211
}
72037212

7213+
/// Returns the funding or splice transaction that should be rebroadcast because it has
7214+
/// not yet confirmed. Intended to be called periodically (e.g., from
7215+
/// [`ChannelManager::timer_tick_occurred`]).
7216+
///
7217+
/// Returns `None` when no rebroadcast is needed — the initial batch broadcast has not
7218+
/// happened yet, the relevant transaction is already confirmed, or (for the initial
7219+
/// funding only) the user is responsible for broadcasting
7220+
/// ([`ChannelContext::is_manual_broadcast`]). Splice rebroadcasts are not suppressed
7221+
/// by `is_manual_broadcast`: that flag applies to the initial V1 funding, while splice
7222+
/// transactions are always broadcast by LDK via the V2 interactive funding flow
7223+
/// regardless of how the initial channel was opened.
7224+
///
7225+
/// The initial-funding and splice branches are mutually exclusive: a splice can only
7226+
/// be initiated when the channel is [`ChannelState::ChannelReady`], which requires the
7227+
/// initial funding to have confirmed. If both somehow apply (e.g., a deep reorg
7228+
/// unconfirming a channel-ready channel with a pending splice), the initial funding
7229+
/// takes precedence, since the splice tx spends the initial-funding output and cannot
7230+
/// confirm without it.
7231+
///
7232+
/// [`ChannelManager::timer_tick_occurred`]: super::channelmanager::ChannelManager::timer_tick_occurred
7233+
pub(super) fn unconfirmed_funding_broadcast(&self) -> Option<(Transaction, TransactionType)> {
7234+
// Initial funding: rebroadcast while unconfirmed, as long as the initial batch
7235+
// broadcast has actually happened (i.e., WAITING_FOR_BATCH is clear) and the user
7236+
// isn't responsible for broadcasting.
7237+
let waiting_for_batch = matches!(
7238+
self.context.channel_state,
7239+
ChannelState::AwaitingChannelReady(flags) if flags.is_waiting_for_batch(),
7240+
);
7241+
if !self.context.is_manual_broadcast
7242+
&& !waiting_for_batch
7243+
&& self.funding.get_funding_tx_confirmation_height().is_none()
7244+
{
7245+
if let Some(tx) = &self.funding.funding_transaction {
7246+
return Some((
7247+
tx.clone(),
7248+
TransactionType::Funding {
7249+
channels: vec![(
7250+
self.context.counterparty_node_id,
7251+
self.context.channel_id,
7252+
)],
7253+
},
7254+
));
7255+
}
7256+
}
7257+
7258+
// Splice: rebroadcast the latest RBF candidate while unconfirmed. Mempool only
7259+
// admits one of the RBF siblings at a time (shared inputs), and the latest is the
7260+
// highest-feerate candidate by construction.
7261+
if let Some(pending) = &self.pending_splice {
7262+
let any_candidate_confirmed = pending
7263+
.negotiated_candidates
7264+
.iter()
7265+
.any(|f| f.get_funding_tx_confirmation_height().is_some());
7266+
if !any_candidate_confirmed {
7267+
if let Some(last_candidate) = pending.negotiated_candidates.last() {
7268+
if let Some(tx) = &last_candidate.funding_transaction {
7269+
return Some((
7270+
tx.clone(),
7271+
TransactionType::Splice {
7272+
counterparty_node_id: self.context.counterparty_node_id,
7273+
channel_id: self.context.channel_id,
7274+
contribution: pending.contributions.last().cloned(),
7275+
replaced_txid: pending.last_replaced_txid(),
7276+
},
7277+
));
7278+
}
7279+
}
7280+
}
7281+
}
7282+
7283+
None
7284+
}
7285+
72047286
fn has_pending_splice_awaiting_signatures(&self) -> bool {
72057287
self.pending_splice
72067288
.as_ref()
@@ -9358,16 +9440,11 @@ where
93589440
);
93599441
}
93609442

9361-
let replaced_txid =
9362-
pending_splice.negotiated_candidates.len().checked_sub(2).and_then(|idx| {
9363-
pending_splice.negotiated_candidates[idx].get_funding_txid()
9364-
});
9365-
let contribution = pending_splice.contributions.last().cloned();
93669443
let tx_type = TransactionType::Splice {
93679444
counterparty_node_id: self.context.counterparty_node_id,
93689445
channel_id: self.context.channel_id,
9369-
contribution,
9370-
replaced_txid,
9446+
contribution: pending_splice.contributions.last().cloned(),
9447+
replaced_txid: pending_splice.last_replaced_txid(),
93719448
};
93729449
funding_tx_signed.funding_tx = Some((funding_tx, tx_type));
93739450
funding_tx_signed.splice_negotiated = Some(splice_negotiated);

lightning/src/ln/channel_open_tests.rs

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
//! Tests that test the channel open process.
1111
12-
use crate::chain::chaininterface::LowerBoundedFeeEstimator;
12+
use crate::chain::chaininterface::{LowerBoundedFeeEstimator, TransactionType};
1313
use crate::chain::channelmonitor::{self, ChannelMonitorUpdateStep};
1414
use crate::chain::transaction::OutPoint;
1515
use crate::chain::{self, ChannelMonitorUpdateStatus};
@@ -2231,6 +2231,173 @@ pub fn test_batch_funding_close_after_funding_signed() {
22312231
assert!(nodes[0].node.list_channels().is_empty());
22322232
}
22332233

2234+
#[xtest(feature = "_externalize_tests")]
2235+
pub fn test_funding_tx_rebroadcast() {
2236+
// Tests that `ChannelManager::timer_tick_occurred` periodically rebroadcasts an unconfirmed
2237+
// funding transaction, and that rebroadcasting stops once the transaction has been confirmed.
2238+
let chanmon_cfgs = create_chanmon_cfgs(2);
2239+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2240+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
2241+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2242+
2243+
let node_b_id = nodes[1].node.get_our_node_id();
2244+
2245+
// Open a channel without confirming. `sign_funding_transaction` verifies the initial
2246+
// broadcast happened and clears the broadcaster, so the queue is empty from here on.
2247+
let tx = create_chan_between_nodes_with_value_init(&nodes[0], &nodes[1], 100_000, 10_000);
2248+
let funding_txo = OutPoint { txid: tx.compute_txid(), index: 0 };
2249+
let channel_id = ChannelId::v1_from_funding_outpoint(funding_txo);
2250+
assert!(nodes[0].tx_broadcaster.txn_broadcast().is_empty());
2251+
2252+
// The next timer tick should rebroadcast the funding transaction because it hasn't
2253+
// confirmed yet.
2254+
nodes[0].node.timer_tick_occurred();
2255+
let rebroadcast = nodes[0].tx_broadcaster.txn_broadcast_with_types();
2256+
assert_eq!(rebroadcast.len(), 1);
2257+
assert_eq!(rebroadcast[0].0, tx);
2258+
match &rebroadcast[0].1 {
2259+
TransactionType::Funding { channels } => {
2260+
assert_eq!(channels.as_slice(), &[(node_b_id, channel_id)]);
2261+
},
2262+
other => panic!("Expected Funding, got {:?}", other),
2263+
}
2264+
2265+
// The V1 acceptor does not have the funding transaction and must not rebroadcast.
2266+
nodes[1].node.timer_tick_occurred();
2267+
assert!(nodes[1].tx_broadcaster.txn_broadcast().is_empty());
2268+
2269+
// A subsequent tick before confirmation still rebroadcasts.
2270+
nodes[0].node.timer_tick_occurred();
2271+
assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 1);
2272+
2273+
// Once the funding tx confirms, timer_tick_occurred should no longer rebroadcast.
2274+
mine_transaction(&nodes[0], &tx);
2275+
nodes[0].tx_broadcaster.clear();
2276+
nodes[0].node.timer_tick_occurred();
2277+
assert!(
2278+
nodes[0].tx_broadcaster.txn_broadcast().is_empty(),
2279+
"Should not rebroadcast after confirmation",
2280+
);
2281+
}
2282+
2283+
#[xtest(feature = "_externalize_tests")]
2284+
pub fn test_funding_tx_rebroadcast_skipped_for_manual_broadcast() {
2285+
// Tests that `ChannelManager::timer_tick_occurred` does not rebroadcast the funding
2286+
// transaction when the user is responsible for broadcasting it
2287+
// (i.e., `ChannelContext::is_manual_broadcast`).
2288+
let mut cfg = UserConfig::default();
2289+
cfg.channel_handshake_config.minimum_depth = 1;
2290+
let chanmon_cfgs = create_chanmon_cfgs(2);
2291+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2292+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(cfg.clone()), Some(cfg)]);
2293+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2294+
2295+
let node_a_id = nodes[0].node.get_our_node_id();
2296+
let node_b_id = nodes[1].node.get_our_node_id();
2297+
2298+
assert!(nodes[0].node.create_channel(node_b_id, 100_000, 0, 42, None, None).is_ok());
2299+
let open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id);
2300+
handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel);
2301+
2302+
let accept_channel = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id);
2303+
nodes[0].node.handle_accept_channel(node_b_id, &accept_channel);
2304+
let (temp_channel_id, _tx, funding_outpoint) =
2305+
create_funding_transaction(&nodes[0], &node_b_id, 100_000, 42);
2306+
nodes[0]
2307+
.node
2308+
.unsafe_manual_funding_transaction_generated(temp_channel_id, node_b_id, funding_outpoint)
2309+
.unwrap();
2310+
check_added_monitors(&nodes[0], 0);
2311+
2312+
let funding_created = get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id);
2313+
nodes[1].node.handle_funding_created(node_a_id, &funding_created);
2314+
check_added_monitors(&nodes[1], 1);
2315+
expect_channel_pending_event(&nodes[1], &node_a_id);
2316+
2317+
let funding_signed = get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id);
2318+
nodes[0].node.handle_funding_signed(node_b_id, &funding_signed);
2319+
check_added_monitors(&nodes[0], 1);
2320+
let _ = nodes[0].node.get_and_clear_pending_events();
2321+
2322+
// Only a FundingTxBroadcastSafe event was emitted; the broadcaster was never invoked.
2323+
assert!(nodes[0].tx_broadcaster.txn_broadcast().is_empty());
2324+
2325+
// A timer tick must not kick off a broadcast either — the user owns the funding tx.
2326+
nodes[0].node.timer_tick_occurred();
2327+
assert!(nodes[0].tx_broadcaster.txn_broadcast().is_empty());
2328+
}
2329+
2330+
#[xtest(feature = "_externalize_tests")]
2331+
pub fn test_batch_funding_rebroadcast_dedupe() {
2332+
// Tests that when a batch funding transaction opens multiple channels, a timer-driven
2333+
// rebroadcast emits the transaction exactly once, with all funded channels merged into the
2334+
// `TransactionType::Funding { channels }` list.
2335+
let chanmon_cfgs = create_chanmon_cfgs(3);
2336+
let node_cfgs = create_node_cfgs(3, &chanmon_cfgs);
2337+
let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]);
2338+
let nodes = create_network(3, &node_cfgs, &node_chanmgrs);
2339+
2340+
let node_a_id = nodes[0].node.get_our_node_id();
2341+
let node_b_id = nodes[1].node.get_our_node_id();
2342+
let node_c_id = nodes[2].node.get_our_node_id();
2343+
2344+
let (tx, funding_created_msgs) = create_batch_channel_funding(
2345+
&nodes[0],
2346+
&[(&nodes[1], 100_000, 0, 42, None), (&nodes[2], 200_000, 0, 43, None)],
2347+
);
2348+
2349+
nodes[1].node.handle_funding_created(node_a_id, &funding_created_msgs[0]);
2350+
check_added_monitors(&nodes[1], 1);
2351+
expect_channel_pending_event(&nodes[1], &node_a_id);
2352+
let funding_signed_msg =
2353+
get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id);
2354+
nodes[0].node.handle_funding_signed(node_b_id, &funding_signed_msg);
2355+
check_added_monitors(&nodes[0], 1);
2356+
2357+
nodes[2].node.handle_funding_created(node_a_id, &funding_created_msgs[1]);
2358+
check_added_monitors(&nodes[2], 1);
2359+
expect_channel_pending_event(&nodes[2], &node_a_id);
2360+
let funding_signed_msg =
2361+
get_event_msg!(nodes[2], MessageSendEvent::SendFundingSigned, node_a_id);
2362+
nodes[0].node.handle_funding_signed(node_c_id, &funding_signed_msg);
2363+
check_added_monitors(&nodes[0], 1);
2364+
2365+
// The batch funding transaction has been broadcast once now that both channels' monitors
2366+
// completed.
2367+
let _ = nodes[0].node.get_and_clear_pending_events();
2368+
let initial = nodes[0].tx_broadcaster.txn_broadcast();
2369+
assert_eq!(initial.len(), 1);
2370+
assert_eq!(initial[0], tx);
2371+
2372+
// The channel IDs are derived from the outpoints in the funding tx. There's one outpoint
2373+
// per channel; their indices come from the funding script order the batch path uses.
2374+
let channel_id_b =
2375+
ChannelId::v1_from_funding_outpoint(OutPoint { txid: tx.compute_txid(), index: 0 });
2376+
let channel_id_c =
2377+
ChannelId::v1_from_funding_outpoint(OutPoint { txid: tx.compute_txid(), index: 1 });
2378+
2379+
// A timer tick rebroadcasts the batch funding transaction exactly once, and the merged
2380+
// `channels` list contains both funded channels.
2381+
nodes[0].node.timer_tick_occurred();
2382+
let rebroadcast = nodes[0].tx_broadcaster.txn_broadcast_with_types();
2383+
assert_eq!(rebroadcast.len(), 1);
2384+
assert_eq!(rebroadcast[0].0, tx);
2385+
match &rebroadcast[0].1 {
2386+
TransactionType::Funding { channels } => {
2387+
assert_eq!(channels.len(), 2);
2388+
assert!(channels.contains(&(node_b_id, channel_id_b)));
2389+
assert!(channels.contains(&(node_c_id, channel_id_c)));
2390+
},
2391+
other => panic!("Expected Funding, got {:?}", other),
2392+
}
2393+
2394+
// Confirm the funding transaction and verify no more rebroadcasts occur.
2395+
mine_transaction(&nodes[0], &tx);
2396+
nodes[0].tx_broadcaster.clear();
2397+
nodes[0].node.timer_tick_occurred();
2398+
assert!(nodes[0].tx_broadcaster.txn_broadcast().is_empty());
2399+
}
2400+
22342401
#[xtest(feature = "_externalize_tests")]
22352402
pub fn test_funding_and_commitment_tx_confirm_same_block() {
22362403
// Tests that a node will forget the channel (when it only requires 1 confirmation) if the

lightning/src/ln/channelmanager.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8716,6 +8716,7 @@ impl<
87168716
let mut timed_out_mpp_htlcs = Vec::new();
87178717
let mut pending_peers_awaiting_removal = Vec::new();
87188718
let mut feerate_cache = new_hash_map();
8719+
let mut rebroadcasts: HashMap<Txid, (Transaction, TransactionType)> = new_hash_map();
87198720

87208721
{
87218722
let per_peer_state = self.per_peer_state.read().unwrap();
@@ -8800,6 +8801,27 @@ impl<
88008801
}
88018802
}
88028803

8804+
if let Some((tx, tx_type)) = funded_chan.unconfirmed_funding_broadcast() {
8805+
let txid = tx.compute_txid();
8806+
match rebroadcasts.entry(txid) {
8807+
hash_map::Entry::Vacant(e) => {
8808+
e.insert((tx, tx_type));
8809+
},
8810+
hash_map::Entry::Occupied(mut e) => {
8811+
// Merge batch-funded channel lists so all
8812+
// participants appear once.
8813+
if let (
8814+
TransactionType::Funding { channels: existing },
8815+
TransactionType::Funding { channels: new_channels },
8816+
) = (&mut e.get_mut().1, tx_type)
8817+
{
8818+
debug_assert_eq!(new_channels.len(), 1);
8819+
existing.extend(new_channels);
8820+
}
8821+
},
8822+
}
8823+
}
8824+
88038825
true
88048826
},
88058827
None => {
@@ -8863,6 +8885,23 @@ impl<
88638885
}
88648886
}
88658887

8888+
// Rebroadcast any unconfirmed funding/splice transactions outside the `per_peer_state`
8889+
// lock. The mempool/broadcaster is expected to be idempotent, so re-sending an already
8890+
// known transaction is harmless.
8891+
for (_txid, (tx, tx_type)) in rebroadcasts {
8892+
log_debug!(
8893+
self.logger,
8894+
"Rebroadcasting {} transaction {}",
8895+
match &tx_type {
8896+
TransactionType::Funding { .. } => "funding",
8897+
TransactionType::Splice { .. } => "splice",
8898+
_ => "unknown",
8899+
},
8900+
tx.compute_txid(),
8901+
);
8902+
self.tx_broadcaster.broadcast_transactions(&[(&tx, tx_type)]);
8903+
}
8904+
88668905
// When a peer disconnects but still has channels, the peer's `peer_state` entry in the
88678906
// `per_peer_state` is not removed by the `peer_disconnected` function. If the channels
88688907
// of to that peer is later closed while still being disconnected (i.e. force closed),

0 commit comments

Comments
 (0)