channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

channeld: fix channel_ready retransmission after splice via funding_tx_index - #9256

Open
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice
Open

channeld: fix channel_ready retransmission after splice via funding_tx_index#9256
nGoline wants to merge 6 commits into
ElementsProject:masterfrom
nGoline:fix-channel-ready-retransmit-after-splice

Conversation

@nGoline

Copy link
Copy Markdown
Collaborator

Problem

On reconnect, channeld decides whether to retransmit channel_ready based on whether the peer's channel_reestablish references a splice (is_splice_active). It computed that by comparing the peer's my_current_funding_locked txid against peer->channel->funding.txid.

That comparison is wrong once a splice locks: channel_update_funding() overwrites funding.txid with the splice txid, so afterwards both sides agree on the same txid, the check falls through, and channel_ready is incorrectly retransmitted on reconnect, which can disrupt channels.

This is the problem @ddustin raised reviewing #9079 (comparing against the txid is incorrect, and we should track metadata on the inflight instead), suggesting a funding_tx_index, matching what eclair does.

Fix

Give every funding tx a stable funding_tx_index: 0 for the original funding (including RBF attempts), incrementing by 1 per splice. It is:

  • carried on the channeld inflight and on the channel,
  • threaded through the channeld_init and channeld_add_inflight wire,
  • assigned on splice creation (parent + 1) and copied onto the channel funding when a splice locks,
  • persisted in channel_funding_inflights and channels (two migrations, DEFAULT 0, so existing channels are unaffected).

The reestablish check now resolves the my_current_funding_locked txid to its funding_tx_index (current funding or a pending inflight) and treats > 0 as a splice.

Commits (bisectable)

  1. splice: add tests for funding_tx_index tracking, regression tests (xfail).
  2. splice: track funding_tx_index on inflights and channel funding, the plumbing.
  3. channeld: detect splice on reestablish via funding_tx_index, not txid, the fix.

Tests

  • test_splice_reconnect_after_lock_no_channel_ready: splice, restart, reestablish; asserts the index persists and channel_ready is not retransmitted. It fails on commit 2 (old txid compare) and passes on commit 3, so it genuinely guards the fix.
  • test_splice_funding_tx_index_increments: two sequential splices reach index 2.
  • test_splice_inflight_funding_tx_index: a pending splice inflight carries index 1 and survives a restart.
  • Existing test_reconnect_no_update confirms non-splice channels still retransmit channel_ready.

Risk and plan

The behavioural change is a single line in the reestablish check. The plan is to get the funding_tx_index semantics reviewed properly, and if we don't converge before the v26.09 freeze, revert that one line with a FIXME and keep the plumbing for the follow-up. So this carries low risk.

@ddustin, this implements the funding_tx_index approach you suggested; would appreciate your review of the assignment semantics and whether it matches what you and tbast had in mind.

@nGoline
nGoline requested a review from ddustinJune 24, 2026 21:50
@nGolinenGoline added the Status::Ready for Review The work has been completed and is now awaiting evaluation or approval. label Jun 24, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! 👏 Mostly style notes with only one major concern:

test_splice_reconnect_after_lock_no_channel_ready may not actually be testing for the lack of channel_ready because of the unneeded billboard: Channel ready for use. check.

Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadtests/test_splicing_disconnect.py Outdated
Comment threadchanneld/channeld.c Outdated
Comment threadtests/test_splicing_disconnect.py
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from a762d4d to 8de1741CompareJune 29, 2026 14:54
@nGoline
nGoline requested a review from ddustinJune 29, 2026 14:59
@nGoline

Copy link
Copy Markdown
CollaboratorAuthor

@ddustin comments addressed. Just waiting for your review.

@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch 2 times, most recently from 799b111 to 85979bbCompareJuly 13, 2026 13:08
Add regression tests that each funding transaction carries a
funding_tx_index (0 for the original funding, +1 per splice), that it is
persisted and reloaded across restart, and that a pending splice inflight
carries its index.
Changelog-None
Give every funding tx a stable index: 0 for the original funding
(including RBF attempts), incrementing by 1 per splice. Threaded through
the channeld inflight, the channel, the channeld_init and
channeld_add_inflight wire messages, set on splice creation and carried
onto the channel funding when a splice locks, and persisted in the
channel_funding_inflights and channels tables.
is_splice_active compared the peer's my_current_funding_locked txid
against peer->channel->funding.txid, which is wrong: once a splice locks,
funding.txid becomes the splice txid, both sides agree on it, the check
falls through, and channel_ready is incorrectly retransmitted on reconnect.
Resolve the txid to its funding_tx_index (the current channel funding or a
pending splice inflight) and treat > 0 as a splice.
Changelog-Fixed: channeld: correctly detect splice transactions when deciding whether to retransmit `channel_ready` on reconnect.
@nGoline
nGolineforce-pushed the fix-channel-ready-retransmit-after-splice branch from 85979bb to 7043a72CompareJuly 30, 2026 20:14
@nGolinenGoline added this to the v26.09 milestone Aug 4, 2026

@ddustinddustin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing this it appears to be deviating from the spec. Doing so inside of reestablish is particularly dangerous and such behavior changes realistically trigger a new cycle of interop testing with LDK and ACINQ.

Comment threadchanneld/channeld.c Outdated
&& !remote_next_funding
&& !(recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& funding_tx_index_for_txid(peer,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These extra conditions appear to be leaving the spec, perhaps by quite a lot. The code should match what the spec says, referenced in the comment above.

*Anode:
*-if `next_commitment_number` iszero:
*-MUSTimmediatelyfailthechannelandbroadcastanyrelevantlatestcommitment*transaction.
*-if `next_commitment_number` is1inboththe `channel_reestablish` it*sentandreceived, andnoneofthose `channel_reestablish` messages*contain `my_current_funding_locked` or `next_funding` forasplicetransaction:
*-MUSTretransmit `channel_ready`.
*-otherwise:
*-MUSTNOTretransmit `channel_ready`, butMAYsend `channel_ready` with*adifferent `short_channel_id` `alias` field.

If the spec is wrong in some way, which is always possible, we need to get the spec updated and then conform to the new spec.

Adding conditions to these things in disagreement with the spec is precisely how we get interop issues that are extraordinarily difficult to track down.

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right, and my earlier reply missed your point. The shape you NACKed on #9079 was still in the condition, and I had only reimplemented one disjunct inside it. That's fixed now.

The check is back to the original three conditions plus the single conjunct the spec adds:

if (peer->channel_ready[LOCAL]
&&peer->next_index[LOCAL] ==1&&next_commitment_number==1&& !reestablish_mentions_splice(peer, send_tlvs, recv_tlvs)) {

locked_ready[LOCAL] and the two bare next_funding checks are gone. The helper applies funding_tx_index > 0 symmetrically to both the channel_reestablish we sent and the one we received, which is a direct reading of "contain my_current_funding_locked or next_funding for a splice transaction".

On why the previous version was wrong: check_mutual_splice_locked() clears locked_ready[LOCAL] and calls channel_update_funding() to rewrite channel->funding.txid to the splice txid, in the same call. Both proxies for "is this a splice transaction" went stale at that exact moment, so both halves of the check were wrong once a splice locked, not just the received side. And per the sending rules, a node that has never spliced still includes my_current_funding_locked with the funding txid, which is why the "for a splice transaction" qualifier can't be answered by a presence check or a txid comparison.

On interop: an unrecognized txid is now treated as a splice, so behaviour is unchanged from master for non-splice channels, pending inflights, and txids we can't account for. The splice-locked case is the only difference.

To check the test actually guards this rather than passing incidentally, I swapped the helper body back to the old !bitcoin_txid_eq(txid, &funding.txid) and rebuilt: test_splice_reconnect_after_lock_no_channel_ready fails, and passes again with funding_tx_index. So next_index[LOCAL] == 1 does hold after a splice and that branch is live.

Pushed as three fixup commits rather than a rewrite so you can see just the delta. I'll autosquash before merge.

The db transaction commits after the log lines and RPC returns the tests
wait on, so poll with wait_for instead of reading the db once.
Initialise funding_tx_index in new_unsaved_channel: the channel is saved
before channel->funding is known, so it must be set at creation.
Give both migrations their rollback SQL.
…ot txid
Restore the original three-condition check and add the single conjunct the
spec requires, applied to both the channel_reestablish we sent and the one
we received.
check_mutual_splice_locked() clears locked_ready[LOCAL] and rewrites
channel->funding.txid to the splice txid in the same call, so both previous
proxies for "is this a splice transaction" went stale exactly when a splice
completed. Keying on funding_tx_index > 0 answers it for either direction;
an unrecognized txid is treated as a splice, matching previous behaviour.
@daywalker90

Copy link
Copy Markdown
Collaborator

Needs rebase

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebaseStatus::Ready for ReviewThe work has been completed and is now awaiting evaluation or approval.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@nGoline@daywalker90@ddustin