Skip to content

Commit 21fed17

Browse files
jkczyzclaude
andcommitted
Skip pre-splice announcement_signatures on reestablish
When a splice transaction confirms on both sides while peers are disconnected, each peer's `channel_reestablish` carries `my_current_funding_locked` with the splice txid. In the reestablish handler, `get_announcement_sigs` was called before the inferred `splice_locked` was processed and the splice was promoted, so `self.funding` still pointed to the pre-splice scope. If `announcement_sigs_state` was `NotSent`, the generated `announcement_signatures` carried the pre-splice `short_channel_id` and bitcoin key — which the peer (having already promoted via its own inferred `splice_locked`) would verify against the post-splice `UnsignedChannelAnnouncement`, failing the signature check and force-closing. Skip the pre-promotion call when `my_current_funding_locked` matches the splice we've already confirmed — i.e. `pending_splice.sent_funding_txid` is set and equals the peer's locked txid. `maybe_promote_splice_funding` emits correct post-splice signatures after the inferred `splice_locked` is processed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2313bd5 commit 21fed17

2 files changed

Lines changed: 135 additions & 1 deletion

File tree

lightning/src/ln/channel.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10325,7 +10325,23 @@ where
1032510325
}
1032610326
}
1032710327

10328-
let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, best_block.height, logger);
10328+
// If the counterparty's `my_current_funding_locked` matches the splice we've already
10329+
// confirmed and are about to promote, any `announcement_signatures` we'd generate here
10330+
// would be for the soon-to-be-superseded pre-splice funding. Skip them;
10331+
// `maybe_promote_splice_funding` will emit correct post-splice sigs once
10332+
// `inferred_splice_locked` is processed.
10333+
let our_splice_txid =
10334+
self.pending_splice.as_ref().and_then(|ps| ps.sent_funding_txid);
10335+
let splice_promotion_pending = msg
10336+
.my_current_funding_locked
10337+
.as_ref()
10338+
.map(|funding_locked| Some(funding_locked.txid) == our_splice_txid)
10339+
.unwrap_or(false);
10340+
let announcement_sigs = if splice_promotion_pending {
10341+
None
10342+
} else {
10343+
self.get_announcement_sigs(node_signer, chain_hash, user_config, best_block.height, logger)
10344+
};
1032910345

1033010346
let mut commitment_update = None;
1033110347
let mut tx_signatures = None;

lightning/src/ln/splicing_tests.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,6 +2183,124 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
21832183
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script);
21842184
}
21852185

2186+
#[test]
2187+
fn test_splice_confirms_on_both_sides_while_disconnected() {
2188+
// Regression test: when a splice transaction confirms on both sides while peers are
2189+
// disconnected, each peer's `channel_reestablish` carries `my_current_funding_locked` with the
2190+
// splice txid. The receiving side must not emit `announcement_signatures` for the pre-splice
2191+
// funding in that handler — those would be verified against the post-splice channel
2192+
// announcement on the peer and force-close the channel. Instead, sigs are generated after the
2193+
// inferred `splice_locked` promotes the splice funding.
2194+
let chanmon_cfgs = create_chanmon_cfgs(2);
2195+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2196+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
2197+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2198+
2199+
let node_id_0 = nodes[0].node.get_our_node_id();
2200+
let node_id_1 = nodes[1].node.get_our_node_id();
2201+
2202+
let initial_channel_value_sat = 100_000;
2203+
let (_, _, channel_id, _) =
2204+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
2205+
2206+
let prev_funding_outpoint = get_monitor!(nodes[0], channel_id).get_funding_txo();
2207+
let prev_funding_script = get_monitor!(nodes[0], channel_id).get_funding_script();
2208+
2209+
// Capture the pre-splice scid so we can later assert the announcement_sigs each side emits
2210+
// on reconnect carry the post-splice scid, not the pre-splice one the bug would emit.
2211+
let pre_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap();
2212+
2213+
let outputs = vec![
2214+
TxOut {
2215+
value: Amount::from_sat(initial_channel_value_sat / 4),
2216+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
2217+
},
2218+
TxOut {
2219+
value: Amount::from_sat(initial_channel_value_sat / 4),
2220+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
2221+
},
2222+
];
2223+
let funding_contribution =
2224+
initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap();
2225+
let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
2226+
2227+
// Disconnect before either side confirms the splice.
2228+
nodes[0].node.peer_disconnected(node_id_1);
2229+
nodes[1].node.peer_disconnected(node_id_0);
2230+
2231+
// Confirm the splice on both sides while disconnected. Each side's `transactions_confirmed`
2232+
// runs `check_get_splice_locked`, which sets `pending_splice.sent_funding_txid` so that
2233+
// `my_current_funding_locked` will carry the splice txid on reconnect. No `splice_locked`
2234+
// messages are queued while disconnected.
2235+
confirm_transaction(&nodes[0], &splice_tx);
2236+
confirm_transaction(&nodes[1], &splice_tx);
2237+
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
2238+
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
2239+
2240+
// Reconnect manually so we can inspect each side's emitted `SendAnnouncementSignatures`.
2241+
// Each side's `channel_reestablish` carries `my_current_funding_locked` with the splice
2242+
// txid, triggering inferred `splice_locked` on the peer. With the fix in place,
2243+
// `announcement_signatures` are generated from the post-splice funding (via the promotion
2244+
// path) rather than the pre-splice funding (via the reestablish handler).
2245+
connect_nodes(&nodes[0], &nodes[1]);
2246+
let reestablish_0 = get_chan_reestablish_msgs!(nodes[0], nodes[1]);
2247+
let reestablish_1 = get_chan_reestablish_msgs!(nodes[1], nodes[0]);
2248+
for msg in &reestablish_0 {
2249+
nodes[1].node.handle_channel_reestablish(node_id_0, msg);
2250+
}
2251+
for msg in &reestablish_1 {
2252+
nodes[0].node.handle_channel_reestablish(node_id_1, msg);
2253+
}
2254+
check_added_monitors(&nodes[0], 1);
2255+
check_added_monitors(&nodes[1], 1);
2256+
expect_channel_ready_event(&nodes[0], &node_id_1);
2257+
expect_channel_ready_event(&nodes[1], &node_id_0);
2258+
2259+
// Each side should emit exactly one `SendAnnouncementSignatures` (post-promotion). The
2260+
// pre-fix behavior would emit a second, stale pre-splice one — our assertion is that the
2261+
// only sigs we send carry the post-splice scid.
2262+
let take_announcement_sigs = |events: Vec<MessageSendEvent>| -> msgs::AnnouncementSignatures {
2263+
let mut sigs = events.into_iter().filter_map(|e| match e {
2264+
MessageSendEvent::SendAnnouncementSignatures { msg, .. } => Some(msg),
2265+
_ => None,
2266+
});
2267+
let only = sigs.next().expect("expected one SendAnnouncementSignatures");
2268+
assert!(sigs.next().is_none(), "expected only one SendAnnouncementSignatures");
2269+
only
2270+
};
2271+
let node_0_events = nodes[0].node.get_and_clear_pending_msg_events();
2272+
let node_1_events = nodes[1].node.get_and_clear_pending_msg_events();
2273+
let node_0_sigs = take_announcement_sigs(node_0_events);
2274+
let node_1_sigs = take_announcement_sigs(node_1_events);
2275+
assert_ne!(node_0_sigs.short_channel_id, pre_splice_scid);
2276+
assert_ne!(node_1_sigs.short_channel_id, pre_splice_scid);
2277+
2278+
// Cross-deliver to complete the post-splice announcement exchange, then drain the
2279+
// resulting `BroadcastChannelAnnouncement` events on each side.
2280+
nodes[1].node.handle_announcement_signatures(node_id_0, &node_0_sigs);
2281+
nodes[0].node.handle_announcement_signatures(node_id_1, &node_1_sigs);
2282+
let _ = nodes[0].node.get_and_clear_pending_msg_events();
2283+
let _ = nodes[1].node.get_and_clear_pending_msg_events();
2284+
2285+
// Channel must still be operational after reconnect — no force-close from mismatched
2286+
// announcement signatures.
2287+
send_payment(&nodes[0], &[&nodes[1]], 1_000_000);
2288+
2289+
// No stray events or messages left over.
2290+
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
2291+
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
2292+
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
2293+
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
2294+
2295+
// Clean up chain-source state for the retired pre-splice funding so end-of-test checks pass.
2296+
nodes[0]
2297+
.chain_source
2298+
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone());
2299+
nodes[1]
2300+
.chain_source
2301+
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script);
2302+
}
2303+
21862304
#[test]
21872305
fn test_propose_splice_while_disconnected() {
21882306
do_test_propose_splice_while_disconnected(false);

0 commit comments

Comments
 (0)