Skip to content

Commit 42e198c

Browse files
authored
Merge pull request #4577 from jkczyz/2026-04-splice-interop-fixes
Fix stale pre-splice `announcement_signatures` during reestablish
2 parents f23b8e6 + 80528b1 commit 42e198c

2 files changed

Lines changed: 231 additions & 1 deletion

File tree

lightning/src/ln/channel.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10315,7 +10315,23 @@ where
1031510315
}
1031610316
}
1031710317

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

1032010336
let mut commitment_update = None;
1032110337
let mut tx_signatures = None;
@@ -12129,6 +12145,17 @@ where
1212912145
&mut self, node_signer: &NS, chain_hash: ChainHash, best_block_height: u32,
1213012146
msg: &msgs::AnnouncementSignatures, user_config: &UserConfig
1213112147
) -> Result<msgs::ChannelAnnouncement, ChannelError> {
12148+
// Ignore sigs signed over a `short_channel_id` other than our current one (e.g. stale
12149+
// pre-splice sigs arriving after our side has promoted). Verifying them against the
12150+
// current `UnsignedChannelAnnouncement` would always fail the hash check, but per BOLT #7
12151+
// that's not a protocol violation warranting a force-close.
12152+
if Some(msg.short_channel_id) != self.funding.get_short_channel_id() {
12153+
return Err(ChannelError::Ignore(format!(
12154+
"Ignoring announcement_signatures for short_channel_id {} which does not match our current short_channel_id {:?}",
12155+
msg.short_channel_id, self.funding.get_short_channel_id(),
12156+
)));
12157+
}
12158+
1213212159
let announcement = self.get_channel_announcement(node_signer, chain_hash, user_config)?;
1213312160

1213412161
let msghash = hash_to_message!(&Sha256d::hash(&announcement.encode()[..])[..]);

lightning/src/ln/splicing_tests.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,6 +2199,209 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
21992199
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script);
22002200
}
22012201

2202+
#[test]
2203+
fn test_splice_confirms_on_both_sides_while_disconnected() {
2204+
// Regression test: when a splice transaction confirms on both sides while peers are
2205+
// disconnected, each peer's `channel_reestablish` carries `my_current_funding_locked` with the
2206+
// splice txid. The receiving side must not emit `announcement_signatures` for the pre-splice
2207+
// funding in that handler — those would be verified against the post-splice channel
2208+
// announcement on the peer and force-close the channel. Instead, sigs are generated after the
2209+
// inferred `splice_locked` promotes the splice funding.
2210+
let chanmon_cfgs = create_chanmon_cfgs(2);
2211+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2212+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]);
2213+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2214+
2215+
let node_id_0 = nodes[0].node.get_our_node_id();
2216+
let node_id_1 = nodes[1].node.get_our_node_id();
2217+
2218+
let initial_channel_value_sat = 100_000;
2219+
let (_, _, channel_id, _) =
2220+
create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0);
2221+
2222+
let prev_funding_outpoint = get_monitor!(nodes[0], channel_id).get_funding_txo();
2223+
let prev_funding_script = get_monitor!(nodes[0], channel_id).get_funding_script();
2224+
2225+
// Capture the pre-splice scid so we can later assert the announcement_sigs each side emits
2226+
// on reconnect carry the post-splice scid, not the pre-splice one the bug would emit.
2227+
let pre_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap();
2228+
2229+
let outputs = vec![
2230+
TxOut {
2231+
value: Amount::from_sat(initial_channel_value_sat / 4),
2232+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
2233+
},
2234+
TxOut {
2235+
value: Amount::from_sat(initial_channel_value_sat / 4),
2236+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
2237+
},
2238+
];
2239+
let funding_contribution =
2240+
initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap();
2241+
let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
2242+
2243+
// Disconnect before either side confirms the splice.
2244+
nodes[0].node.peer_disconnected(node_id_1);
2245+
nodes[1].node.peer_disconnected(node_id_0);
2246+
2247+
// Confirm the splice on both sides while disconnected. Each side's `transactions_confirmed`
2248+
// runs `check_get_splice_locked`, which sets `pending_splice.sent_funding_txid` so that
2249+
// `my_current_funding_locked` will carry the splice txid on reconnect. No `splice_locked`
2250+
// messages are queued while disconnected.
2251+
confirm_transaction(&nodes[0], &splice_tx);
2252+
confirm_transaction(&nodes[1], &splice_tx);
2253+
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
2254+
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
2255+
2256+
// Reconnect manually so we can inspect each side's emitted `SendAnnouncementSignatures`.
2257+
// Each side's `channel_reestablish` carries `my_current_funding_locked` with the splice
2258+
// txid, triggering inferred `splice_locked` on the peer. With the fix in place,
2259+
// `announcement_signatures` are generated from the post-splice funding (via the promotion
2260+
// path) rather than the pre-splice funding (via the reestablish handler).
2261+
connect_nodes(&nodes[0], &nodes[1]);
2262+
let reestablish_0 = get_chan_reestablish_msgs!(nodes[0], nodes[1]);
2263+
let reestablish_1 = get_chan_reestablish_msgs!(nodes[1], nodes[0]);
2264+
for msg in &reestablish_0 {
2265+
nodes[1].node.handle_channel_reestablish(node_id_0, msg);
2266+
}
2267+
for msg in &reestablish_1 {
2268+
nodes[0].node.handle_channel_reestablish(node_id_1, msg);
2269+
}
2270+
check_added_monitors(&nodes[0], 1);
2271+
check_added_monitors(&nodes[1], 1);
2272+
expect_channel_ready_event(&nodes[0], &node_id_1);
2273+
expect_channel_ready_event(&nodes[1], &node_id_0);
2274+
2275+
// Each side should emit exactly one `SendAnnouncementSignatures` (post-promotion). The
2276+
// pre-fix behavior would emit a second, stale pre-splice one — our assertion is that the
2277+
// only sigs we send carry the post-splice scid.
2278+
let take_announcement_sigs = |events: Vec<MessageSendEvent>| -> msgs::AnnouncementSignatures {
2279+
let mut sigs = events.into_iter().filter_map(|e| match e {
2280+
MessageSendEvent::SendAnnouncementSignatures { msg, .. } => Some(msg),
2281+
_ => None,
2282+
});
2283+
let only = sigs.next().expect("expected one SendAnnouncementSignatures");
2284+
assert!(sigs.next().is_none(), "expected only one SendAnnouncementSignatures");
2285+
only
2286+
};
2287+
let node_0_events = nodes[0].node.get_and_clear_pending_msg_events();
2288+
let node_1_events = nodes[1].node.get_and_clear_pending_msg_events();
2289+
let node_0_sigs = take_announcement_sigs(node_0_events);
2290+
let node_1_sigs = take_announcement_sigs(node_1_events);
2291+
assert_ne!(node_0_sigs.short_channel_id, pre_splice_scid);
2292+
assert_ne!(node_1_sigs.short_channel_id, pre_splice_scid);
2293+
2294+
// Cross-deliver to complete the post-splice announcement exchange, then drain the
2295+
// resulting `BroadcastChannelAnnouncement` events on each side.
2296+
nodes[1].node.handle_announcement_signatures(node_id_0, &node_0_sigs);
2297+
nodes[0].node.handle_announcement_signatures(node_id_1, &node_1_sigs);
2298+
let _ = nodes[0].node.get_and_clear_pending_msg_events();
2299+
let _ = nodes[1].node.get_and_clear_pending_msg_events();
2300+
2301+
// Channel must still be operational after reconnect — no force-close from mismatched
2302+
// announcement signatures.
2303+
send_payment(&nodes[0], &[&nodes[1]], 1_000_000);
2304+
2305+
// No stray events or messages left over.
2306+
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
2307+
assert!(nodes[1].node.get_and_clear_pending_events().is_empty());
2308+
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
2309+
assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty());
2310+
2311+
// Clean up chain-source state for the retired pre-splice funding so end-of-test checks pass.
2312+
nodes[0]
2313+
.chain_source
2314+
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone());
2315+
nodes[1]
2316+
.chain_source
2317+
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script);
2318+
}
2319+
2320+
#[test]
2321+
fn test_stale_announcement_signatures_ignored_after_splice_lock() {
2322+
// Regression test: a peer may transmit `announcement_signatures` signed over a pre-splice
2323+
// `short_channel_id` (for example, a stale retransmission or a peer implementation that
2324+
// hasn't yet caught up to our post-splice promotion). Verifying those sigs against the
2325+
// post-splice `UnsignedChannelAnnouncement` will always fail the hash check, but that is not
2326+
// a protocol violation — the spec permits ignoring and the channel should stay open.
2327+
let chanmon_cfgs = create_chanmon_cfgs(2);
2328+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2329+
let mut config = test_default_channel_config();
2330+
config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage =
2331+
100;
2332+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]);
2333+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2334+
2335+
let node_id_0 = nodes[0].node.get_our_node_id();
2336+
let node_id_1 = nodes[1].node.get_our_node_id();
2337+
2338+
let initial_channel_value_sat = 100_000;
2339+
// Use the lower-level helper so we get the signed `ChannelAnnouncement` back — the test
2340+
// needs node 1's pre-splice announcement signatures to replay later.
2341+
let chan_announcement =
2342+
create_chan_between_nodes_with_value(&nodes[0], &nodes[1], initial_channel_value_sat, 0);
2343+
let channel_id = chan_announcement.3;
2344+
update_nodes_with_chan_announce(
2345+
&nodes,
2346+
0,
2347+
1,
2348+
&chan_announcement.0,
2349+
&chan_announcement.1,
2350+
&chan_announcement.2,
2351+
);
2352+
2353+
// Extract node 1's pre-splice signatures from the ChannelAnnouncement. `UnsignedChannelAnnouncement`
2354+
// orders `node_id_1`/`node_id_2` by serialized pubkey; node 1's sigs are in slot 1 iff node 1's
2355+
// pubkey is lexicographically smaller.
2356+
let node_1_is_node_one = node_id_1.serialize() < node_id_0.serialize();
2357+
let (stale_node_sig, stale_bitcoin_sig) = if node_1_is_node_one {
2358+
(chan_announcement.0.node_signature_1, chan_announcement.0.bitcoin_signature_1)
2359+
} else {
2360+
(chan_announcement.0.node_signature_2, chan_announcement.0.bitcoin_signature_2)
2361+
};
2362+
2363+
// Capture the pre-splice `short_channel_id` — this is the scid the stale sigs sign over.
2364+
let pre_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap();
2365+
2366+
let outputs = vec![
2367+
TxOut {
2368+
value: Amount::from_sat(initial_channel_value_sat / 4),
2369+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
2370+
},
2371+
TxOut {
2372+
value: Amount::from_sat(initial_channel_value_sat / 4),
2373+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
2374+
},
2375+
];
2376+
let funding_contribution =
2377+
initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap();
2378+
let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
2379+
mine_transaction(&nodes[0], &splice_tx);
2380+
mine_transaction(&nodes[1], &splice_tx);
2381+
lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
2382+
2383+
// The post-splice scid is now different; confirm that.
2384+
let post_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap();
2385+
assert_ne!(pre_splice_scid, post_splice_scid);
2386+
2387+
// Replay node 1's pre-splice announcement signatures, now stale (the current scid is the
2388+
// post-splice one). This is the exact shape of message a peer would send if it retransmitted
2389+
// an old `announcement_signatures` across a splice handoff.
2390+
let stale_sigs = msgs::AnnouncementSignatures {
2391+
channel_id,
2392+
short_channel_id: pre_splice_scid,
2393+
node_signature: stale_node_sig,
2394+
bitcoin_signature: stale_bitcoin_sig,
2395+
};
2396+
nodes[0].node.handle_announcement_signatures(node_id_1, &stale_sigs);
2397+
2398+
// No force-close, no outbound error, no events. The channel must still be listed and usable.
2399+
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
2400+
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
2401+
assert_eq!(nodes[0].node.list_channels().len(), 1);
2402+
send_payment(&nodes[0], &[&nodes[1]], 1_000_000);
2403+
}
2404+
22022405
#[test]
22032406
fn test_propose_splice_while_disconnected() {
22042407
do_test_propose_splice_while_disconnected(false);

0 commit comments

Comments
 (0)