Skip to content

Commit 80528b1

Browse files
jkczyzclaude
andcommitted
Ignore stale announcement_signatures instead of force-closing
A peer may transmit `announcement_signatures` signed over a stale `short_channel_id` — most plausibly a retransmission or a peer implementation whose view hasn't caught up to our post-splice promotion. Verifying such sigs against the current `UnsignedChannelAnnouncement` (built from `self.funding`) always fails the hash check, which previously produced a force-close. BOLT #7 does not require closing in this situation; the mismatch is expected across splice handoffs. Short-circuit with `ChannelError::Ignore` when `msg.short_channel_id` doesn't match the current funding's scid, leaving the genuine invalid-signature paths in place for sigs that actually target our current scid. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 21fed17 commit 80528b1

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

lightning/src/ln/channel.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12155,6 +12155,17 @@ where
1215512155
&mut self, node_signer: &NS, chain_hash: ChainHash, best_block_height: u32,
1215612156
msg: &msgs::AnnouncementSignatures, user_config: &UserConfig
1215712157
) -> Result<msgs::ChannelAnnouncement, ChannelError> {
12158+
// Ignore sigs signed over a `short_channel_id` other than our current one (e.g. stale
12159+
// pre-splice sigs arriving after our side has promoted). Verifying them against the
12160+
// current `UnsignedChannelAnnouncement` would always fail the hash check, but per BOLT #7
12161+
// that's not a protocol violation warranting a force-close.
12162+
if Some(msg.short_channel_id) != self.funding.get_short_channel_id() {
12163+
return Err(ChannelError::Ignore(format!(
12164+
"Ignoring announcement_signatures for short_channel_id {} which does not match our current short_channel_id {:?}",
12165+
msg.short_channel_id, self.funding.get_short_channel_id(),
12166+
)));
12167+
}
12168+
1215812169
let announcement = self.get_channel_announcement(node_signer, chain_hash, user_config)?;
1215912170

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

lightning/src/ln/splicing_tests.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2301,6 +2301,91 @@ fn test_splice_confirms_on_both_sides_while_disconnected() {
23012301
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script);
23022302
}
23032303

2304+
#[test]
2305+
fn test_stale_announcement_signatures_ignored_after_splice_lock() {
2306+
// Regression test: a peer may transmit `announcement_signatures` signed over a pre-splice
2307+
// `short_channel_id` (for example, a stale retransmission or a peer implementation that
2308+
// hasn't yet caught up to our post-splice promotion). Verifying those sigs against the
2309+
// post-splice `UnsignedChannelAnnouncement` will always fail the hash check, but that is not
2310+
// a protocol violation — the spec permits ignoring and the channel should stay open.
2311+
let chanmon_cfgs = create_chanmon_cfgs(2);
2312+
let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
2313+
let mut config = test_default_channel_config();
2314+
config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage =
2315+
100;
2316+
let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]);
2317+
let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
2318+
2319+
let node_id_0 = nodes[0].node.get_our_node_id();
2320+
let node_id_1 = nodes[1].node.get_our_node_id();
2321+
2322+
let initial_channel_value_sat = 100_000;
2323+
// Use the lower-level helper so we get the signed `ChannelAnnouncement` back — the test
2324+
// needs node 1's pre-splice announcement signatures to replay later.
2325+
let chan_announcement =
2326+
create_chan_between_nodes_with_value(&nodes[0], &nodes[1], initial_channel_value_sat, 0);
2327+
let channel_id = chan_announcement.3;
2328+
update_nodes_with_chan_announce(
2329+
&nodes,
2330+
0,
2331+
1,
2332+
&chan_announcement.0,
2333+
&chan_announcement.1,
2334+
&chan_announcement.2,
2335+
);
2336+
2337+
// Extract node 1's pre-splice signatures from the ChannelAnnouncement. `UnsignedChannelAnnouncement`
2338+
// orders `node_id_1`/`node_id_2` by serialized pubkey; node 1's sigs are in slot 1 iff node 1's
2339+
// pubkey is lexicographically smaller.
2340+
let node_1_is_node_one = node_id_1.serialize() < node_id_0.serialize();
2341+
let (stale_node_sig, stale_bitcoin_sig) = if node_1_is_node_one {
2342+
(chan_announcement.0.node_signature_1, chan_announcement.0.bitcoin_signature_1)
2343+
} else {
2344+
(chan_announcement.0.node_signature_2, chan_announcement.0.bitcoin_signature_2)
2345+
};
2346+
2347+
// Capture the pre-splice `short_channel_id` — this is the scid the stale sigs sign over.
2348+
let pre_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap();
2349+
2350+
let outputs = vec![
2351+
TxOut {
2352+
value: Amount::from_sat(initial_channel_value_sat / 4),
2353+
script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(),
2354+
},
2355+
TxOut {
2356+
value: Amount::from_sat(initial_channel_value_sat / 4),
2357+
script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(),
2358+
},
2359+
];
2360+
let funding_contribution =
2361+
initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap();
2362+
let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution);
2363+
mine_transaction(&nodes[0], &splice_tx);
2364+
mine_transaction(&nodes[1], &splice_tx);
2365+
lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1);
2366+
2367+
// The post-splice scid is now different; confirm that.
2368+
let post_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap();
2369+
assert_ne!(pre_splice_scid, post_splice_scid);
2370+
2371+
// Replay node 1's pre-splice announcement signatures, now stale (the current scid is the
2372+
// post-splice one). This is the exact shape of message a peer would send if it retransmitted
2373+
// an old `announcement_signatures` across a splice handoff.
2374+
let stale_sigs = msgs::AnnouncementSignatures {
2375+
channel_id,
2376+
short_channel_id: pre_splice_scid,
2377+
node_signature: stale_node_sig,
2378+
bitcoin_signature: stale_bitcoin_sig,
2379+
};
2380+
nodes[0].node.handle_announcement_signatures(node_id_1, &stale_sigs);
2381+
2382+
// No force-close, no outbound error, no events. The channel must still be listed and usable.
2383+
assert!(nodes[0].node.get_and_clear_pending_events().is_empty());
2384+
assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty());
2385+
assert_eq!(nodes[0].node.list_channels().len(), 1);
2386+
send_payment(&nodes[0], &[&nodes[1]], 1_000_000);
2387+
}
2388+
23042389
#[test]
23052390
fn test_propose_splice_while_disconnected() {
23062391
do_test_propose_splice_while_disconnected(false);

0 commit comments

Comments
 (0)