splice: Handle CHANNEL_READY during splice resume
What changed, and why it matters
This change fixes a compatibility issue between Core Lightning and the Eclair Lightning node implementation during a channel-splicing reconnect. Previously, Core Lightning would fail the connection if Eclair sent a CHANNEL_READY message while a splice was being resumed. The patch teaches Core Lightning to accept and process that out-of-order message instead of treating it as an error. It is a protocol-handling fix rather than a memory-safety or cryptographic bug, and the commit message frames it as interoperability, not security.
Treat as a normal bugfix/interoperability patch. Review that allowing CHANNEL_READY during splice resume does not advance channel state in a way that conflicts with unprocessed splice messages, and verify with Eclair interoperability tests. No emergency response is indicated by the diff alone.
Security signals we found
Protocol state machine change for splice resume
Out-of-order message handling added for CHANNEL_READY during reconnect
Peer failure path replaced with deferred processing
Interoperability fix with Eclair implementation
Evidence from the diff
The patch renames peer_expect_msg_three to peer_expect_msg_four and adds a fourth allowed message type to the splice-resume message expectation logic. During peer_reconnect, when resuming an in-flight splice, Core Lightning now permits WIRE_CHANNEL_READY to arrive before the resumed splice negotiation. If CHANNEL_READY is received prematurely, it is processed via peer_in and then the code re-waits for the originally expected splice messages (COMMITMENT_SIGNED, TX_SIGNATURES, or TX_ABORT). All other splice resume call sites pass 0 for the new allowed_premature_msg parameter, so only the reconnect paths are affected.
Changed components
channeld/channeld.csplice negotiation state machinepeer reconnect handlinginteractive_send_commitmentsresume_splice_negotiationInspect captured patch +43 / −18
diff --git a/channeld/channeld.c b/channeld/channeld.c
index b91ee9ae..aa092f58 100644
--- a/channeld/channeld.c
+++ b/channeld/channeld.c
@@ -3020,11 +3020,12 @@ static struct wally_psbt *next_splice_step(const tal_t *ctx,
return ictx->desired_psbt;
}
-static const u8 *peer_expect_msg_three(const tal_t *ctx,
+static const u8 *peer_expect_msg_four(const tal_t *ctx,
struct peer *peer,
enum peer_wire expect_type,
enum peer_wire second_allowed_type,
- enum peer_wire third_allowed_type)
+ enum peer_wire third_allowed_type,
+ enum peer_wire fourth_allowed_type)
{
u8 *msg;
enum peer_wire type;
@@ -3033,20 +3034,28 @@ static const u8 *peer_expect_msg_three(const tal_t *ctx,
type = fromwire_peektype(msg);
if (type == WIRE_ERROR)
abort();
- if (type != expect_type && type != second_allowed_type
- && type != third_allowed_type)
+ if (type != expect_type
+ && type != second_allowed_type
+ && type != third_allowed_type
+ && type != fourth_allowed_type)
peer_failed_warn(peer->pps, &peer->channel_id,
"Got incorrect message from peer: %s"
- " (should be %s or %s or %s) [%s]",
+ " (should be %s or %s or %s of %s) [%s]",
peer_wire_name(type),
peer_wire_name(expect_type),
peer_wire_name(second_allowed_type),
peer_wire_name(third_allowed_type),
+ peer_wire_name(fourth_allowed_type),
sanitize_error(tmpctx, msg, &peer->channel_id));
return msg;
}
+/* In some circumstances Eclair send CHANNEL_READY after CHANNEL_REESTABLISH but
+ * before resuming splice negotiation, so we need a way to process it in this
+ * order. */
+static void peer_in(struct peer *peer, const u8 *msg);
+
/* The question of "who signs splice commitments first" is the same order as the
* splice `tx_signature`s are. This function handles sending & receiving the
* required commitments as part of the splicing process.
@@ -3058,7 +3067,8 @@ static struct commitsig *interactive_send_commitments(struct peer *peer,
size_t inflight_index,
bool send_commitments,
bool recv_commitments,
- const u8 **msg_received)
+ const u8 **msg_received,
+ int allowed_premature_msg)
{
struct commitsig_info *result;
const u8 *msg;
@@ -3100,10 +3110,21 @@ static struct commitsig *interactive_send_commitments(struct peer *peer,
result = NULL;
if (recv_commitments) {
- msg = peer_expect_msg_three(tmpctx, peer,
- WIRE_COMMITMENT_SIGNED,
- WIRE_TX_SIGNATURES,
- WIRE_TX_ABORT);
+ msg = peer_expect_msg_four(tmpctx, peer,
+ WIRE_COMMITMENT_SIGNED,
+ WIRE_TX_SIGNATURES,
+ WIRE_TX_ABORT,
+ allowed_premature_msg);
+
+ if (allowed_premature_msg
+ && fromwire_peektype(msg) == allowed_premature_msg) {
+ peer_in(peer, msg);
+ msg = peer_expect_msg_four(tmpctx, peer,
+ WIRE_COMMITMENT_SIGNED,
+ WIRE_TX_SIGNATURES,
+ WIRE_TX_ABORT,
+ 0);
+ }
check_tx_abort(peer, msg, &inflight->outpoint.txid);
@@ -3653,7 +3674,8 @@ static void resume_splice_negotiation(struct peer *peer,
bool send_commitments,
bool recv_commitments,
bool send_signature,
- bool recv_signature)
+ bool recv_signature,
+ int allowed_premature_msg)
{
struct inflight *inflight = last_inflight(peer);
enum tx_role our_role = inflight->i_am_initiator
@@ -3712,7 +3734,8 @@ static void resume_splice_negotiation(struct peer *peer,
last_inflight_index(peer),
send_commitments,
recv_commitments,
- &msg_received);
+ &msg_received,
+ allowed_premature_msg);
check_tx_abort(peer, msg_received, &inflight->outpoint.txid);
@@ -4270,7 +4293,7 @@ static void splice_accepter(struct peer *peer, const u8 *inmsg)
peer->splice_state->count++;
- resume_splice_negotiation(peer, true, true, true, true);
+ resume_splice_negotiation(peer, true, true, true, true, 0);
}
/* splice_initiator runs when splice_ack is received by the other side. It
@@ -4576,7 +4599,7 @@ static void splice_initiator_user_finalized(struct peer *peer)
their_commit = interactive_send_commitments(peer, new_inflight->psbt,
our_role,
last_inflight_index(peer),
- true, true, NULL);
+ true, true, NULL, 0);
new_inflight->last_tx = tal_steal(new_inflight, their_commit->tx);
new_inflight->last_sig = their_commit->commit_signature;
@@ -4592,7 +4615,7 @@ static void splice_initiator_user_finalized(struct peer *peer)
peer->splicing->force_sign_first);
if (!sign_first)
- resume_splice_negotiation(peer, false, false, false, true);
+ resume_splice_negotiation(peer, false, false, false, true, 0);
outmsg = towire_channeld_splice_confirmed_update(NULL,
new_inflight->psbt,
@@ -4793,7 +4816,7 @@ static void splice_initiator_user_signed(struct peer *peer, const u8 *inmsg)
audit_psbt(inflight->psbt, inflight->psbt);
assert(tal_parent(inflight->psbt) != tmpctx);
- resume_splice_negotiation(peer, false, false, true, sign_first);
+ resume_splice_negotiation(peer, false, false, true, sign_first, 0);
audit_psbt(inflight->psbt, inflight->psbt);
assert(tal_parent(inflight->psbt) != tmpctx);
@@ -5779,7 +5802,8 @@ static void peer_reconnect(struct peer *peer,
false,
!inflight->last_tx,
false,
- true);
+ true,
+ WIRE_CHANNEL_READY);
} else if (bitcoin_txid_eq(&remote_next_funding->next_funding_txid,
&inflight->outpoint.txid)) {
/* Don't send sigs unless we have theirs */
@@ -5795,7 +5819,8 @@ static void peer_reconnect(struct peer *peer,
: false,
local_next_funding && !inflight->last_tx,
true,
- local_next_funding);
+ local_next_funding,
+ WIRE_CHANNEL_READY);
} else if (bitcoin_txid_eq(&remote_next_funding->next_funding_txid,
&peer->channel->funding.txid)) {
peer_failed_err(peer->pps,
Why this scored 34/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.