Update BOLT quote for `channel_ready` re-transmission guards.
What changed, and why it matters
This commit tightens the rules for when a Core Lightning node resends a 'channel_ready' message after a peer reconnects during a splice (a way to resize a Lightning channel). Previously, the node would retransmit channel_ready whenever both sides reported they had not yet sent any commitment. The updated BOLT specification says this retransmission must not happen if either side's reconnect message references a pending splice (via next_funding or my_current_funding_locked fields). The code now checks for those splice fields and suppresses the retransmission in those cases, and a test is updated to verify that behavior.
Review whether an incorrectly retransmitted channel_ready during a splice could cause a peer to misinterpret channel state, and consider whether this warrants a security advisory or CVE if it can lead to loss of funds or channel desynchronization. Otherwise, treat as a normal protocol-compliance fix and include in release notes.
Security signals we found
Protocol conformance fix for BOLT #2 channel_ready retransmission
Splice-related state machine guard added to peer reconnect path
Test assertion added to prevent regression of retransmission during splice
Change is defensive: suppresses message that could be sent incorrectly
Evidence from the diff
In channeld.c’s peer_reconnect(), the guard around retransmitting channel_ready is expanded. The original condition retransmitted channel_ready only when peer->next_index[LOCAL] == 1 and next_commitment_number == 1. The patch adds four negative conditions: !local_next_funding, !(send_tlvs && send_tlvs->my_current_funding_locked), !remote_next_funding, and !(recv_tlvs && recv_tlvs->my_current_funding_locked). This aligns with an updated BOLT #2 quote that excludes retransmission when either channel_reestablish TLV contains my_current_funding_locked or next_funding for a splice. A test in tests/test_splicing.py is updated to capture the log position before restart and assert that no ‘Retransmitting channel_ready for channel’ log line appears post-restart during a splice-inflight scenario.
Changed components
channeld/channeld.c peer_reconnect()Lightning channel re-establishment handshakeSplicing protocol state handlingtests/test_splicing.py test_commit_crash_spliceInspect captured patch +17 / −5
diff --git a/channeld/channeld.c b/channeld/channeld.c
index ed4cc6e8..0b45fb88 100644
--- a/channeld/channeld.c
+++ b/channeld/channeld.c
@@ -5957,16 +5957,21 @@ static void peer_reconnect(struct peer *peer,
/* BOLT #2:
*
* - if `next_commitment_number` is 1 in both the
- * `channel_reestablish` it sent and received:
+ * `channel_reestablish` it sent and received, and none of those
+ * `channel_reestablish` messages contain `my_current_funding_locked` or
+ * `next_funding` for a splice transaction:
* - MUST retransmit `channel_ready`.
* - otherwise:
- * - MUST NOT retransmit `channel_ready`, but MAY send
- * `channel_ready` with a different `short_channel_id`
- * `alias` field.
+ * - MUST NOT retransmit `channel_ready`, but MAY send `channel_ready` with
+ * a different `short_channel_id` `alias` field.
*/
if (peer->channel_ready[LOCAL]
&& peer->next_index[LOCAL] == 1
- && next_commitment_number == 1) {
+ && next_commitment_number == 1
+ && !local_next_funding
+ && !(send_tlvs && send_tlvs->my_current_funding_locked)
+ && !remote_next_funding
+ && !(recv_tlvs && recv_tlvs->my_current_funding_locked)) {
struct tlv_channel_ready_tlvs *tlvs = tlv_channel_ready_tlvs_new(tmpctx);
tlvs->short_channel_id = &peer->local_alias;
diff --git a/tests/test_splicing.py b/tests/test_splicing.py
index 8e9ba0e4..8cf4e724 100644
--- a/tests/test_splicing.py
+++ b/tests/test_splicing.py
@@ -416,6 +416,8 @@ def test_commit_crash_splice(node_factory, bitcoind):
l1.daemon.wait_for_log(r"Splice initiator: we commit")
+ # Snapshot log position before restart so we only search post-restart logs below.
+ pre_restart_logpos = l1.daemon.logsearch_start
l1.restart()
# The splicing inflight should have been left pending in the DB
@@ -426,6 +428,11 @@ def test_commit_crash_splice(node_factory, bitcoind):
l1.daemon.wait_for_log(r'Splice resume check with local_next_funding: sent, remote_next_funding: received, inflights: 1')
l1.daemon.wait_for_log(r'Splice negotation, will not send commit, not recv commit, send signature, recv signature as initiator')
+ # channel_ready MUST NOT be retransmitted when either channel_reestablish
+ # message contains next_funding or my_current_funding_locked
+ assert not l1.daemon.is_in_log(r'Retransmitting channel_ready for channel',
+ start=pre_restart_logpos)
+
assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1
l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')
Why this scored 36/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.