Skip to content

Guard payment records against splice funding rebroadcasts - #1049

Merged
tnull merged 2 commits into
lightningdevkit:mainfrom
jkczyz:2026-08-funding-rebroadcast-workarounds
Aug 13, 2026
Merged

Guard payment records against splice funding rebroadcasts#1049
tnull merged 2 commits into
lightningdevkit:mainfrom
jkczyz:2026-08-funding-rebroadcast-workarounds

Conversation

@jkczyz

@jkczyz jkczyz commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

After a 0conf splice is promoted while its funding transaction is still unconfirmed, LDK re-broadcasts that transaction on every monitor-update completion — re-typed as a plain Funding transaction, without the InteractiveFunding contribution metadata it was originally broadcast with. This continues until the splice confirms, including across restarts.

Two guards in the funding classification path handle these re-offers:

  • Skip funding broadcasts without wallet activity — a re-offer of a splice the wallet has no stake in (e.g. the channel opener splicing out to an external address) minted a spurious zero-amount payment record that nothing ever confirms. Funding broadcasts that move no wallet funds are now skipped — the same condition the interactive-funding classification declines on.
  • Keep interactive-funding classification over funding rebroadcasts — a re-offer of a recorded splice overwrote the contribution-derived amount/fee with wallet-view figures and downgraded the recorded transaction type, repeatedly until confirmation. The merge is now skipped when the record is already classified as interactive funding.

The re-typing is upstream behavior rust-lightning should fix, tracked at https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/4878. Both commits carry TODOs and canary tests that fail once upstream stops re-typing the re-offers, so the workarounds can be removed when that happens.

Split out of #930, which builds on these commits.

Generated with assistance from Claude Code (Claude Fable 5).

jkczyz and others added 2 commits August 12, 2026 17:02
After a 0conf splice is promoted, LDK re-broadcasts the still-unconfirmed
funding transaction through its generic funding path on every monitor-update
completion, re-typed as a plain funding transaction without contribution
metadata. The channel opener (or either side of a V2-established channel)
re-broadcasts this way even for splices the interactive-funding
classification deliberately declined to record — a splice-out paying an
external address, or a counterparty-initiated splice with no local
contribution. Recording the re-broadcast minted a spurious zero-amount
payment that nothing ever confirms, leaving it stuck pending forever.

Skip recording funding broadcasts that move no wallet funds, matching the
condition the interactive-funding classification declines on; anything
declined there must be skipped here, or its re-broadcast resurrects the
record. The fee is deliberately not part of the condition: the wallet
derives a splice's fee from the previous funding output whenever it funded
the original channel open, so a computable fee is no sign of participation.
Channel opens are unaffected: the funder's wallet always contributes the
funding input.

The re-typed re-broadcasts are upstream behavior rust-lightning should fix
by keeping the interactive-funding classification on re-offers (or not
re-offering at all), tracked at
https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/4878.
The skip leaves no trace in the stores, so a canary test asserts its log
line on the initiator of a 0conf splice-out to an external address; when
that fails against a newer LDK, re-evaluate whether the skip still sees
traffic.

Generated with assistance from Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
LDK re-broadcasts a promoted-but-unconfirmed 0conf splice on every
monitor-update completion, typed as a plain funding transaction. Its
reclassification carries wallet-view figures rather than the
contribution-derived ones the interactive-funding classification
recorded, so each rebroadcast overwrote the record's amount and fee and
downgraded its transaction type — repeatedly, until the splice
confirmed and even across restarts.

A funding-typed classification of a record already classified as
interactive funding carries nothing the record doesn't have, so skip
the merge and leave the record untouched.

The re-typed re-broadcasts are upstream behavior rust-lightning should
fix by keeping the interactive-funding classification on re-offers (or
not re-offering at all), tracked at
https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/4878.
Since the guard leaves no trace in the stores, classification logs when
a funding-typed broadcast arrives for an interactive-funding record, and
a canary test asserts that line on the contributing side of a 0conf
splice; when it fails against a newer LDK, re-evaluate whether the guard
still sees traffic.

Generated with assistance from Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ldk-reviews-bot

ldk-reviews-bot commented Aug 12, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tnull tnull 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.

One non-blocking nit (not the biggest fan of asserting on logs). Will just go ahead and land this.

// appears the classification pipeline has demonstrably processed one.
let skipped = format!("Not recording channel-funding broadcast {}", txo.txid);
assert!(
logger_a.wait_for(&skipped).await,

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.

nit: Hmm, should we really assert on the log output here? Usually I'm not the biggest fan as that is pretty brittle.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I wasn't a fan either, but the alternative was to allow parameterizing the broadcaster or adding some _test_utils feature to expose a test-only API. Once it is fixed upstream, we can drop the test as it's only here to fail when the workaround is no longer needed.

Comment thread src/wallet/mod.rs
// downgrade, leaving no trace that a re-broadcast arrived. Log the arrival so tests can
// observe the traffic. The read cannot go stale: only the broadcast loop writes
// interactive-funding classifications, and it runs this classification too.
if let Some(current) = self.payment_store.get(&payment_id) {

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.

Man, there are now so many edge cases to handle in this code. I increasingly think we need to reconsider the chosen approach (classifying through the broadcaster interface rather than keeping state in LDK and providing an API for it). Reopened https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/3566#issuecomment-440916 and added it to the v0.4 milestone.

@tnull
tnull merged commit 13799a4 into lightningdevkit:main Aug 13, 2026
20 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants