Add pending SpliceDetails to ChannelDetails#4687
Conversation
PendingFunding tracked our splice contributions in a compact list implicitly aligned to the tail of the negotiated candidates, with the in-flight negotiation round's contribution as the implicit last entry. Every consumer had to re-derive this positional relationship, which is easy to get wrong -- e.g., attributing an in-flight round's contribution to a completed counterparty-only candidate. Instead, store each candidate's contribution with the candidate itself and give the in-flight round's contribution its own field, making such misattribution unrepresentable. The contributions still form a suffix of the candidates -- once a round includes our contribution, every subsequent round carries it forward (possibly feerate-adjusted) so the splice intention is never lost -- which is now asserted when a round completes. The on-disk format is unchanged: the suffix invariant makes the compact encoding lossless, so candidate fundings and contributions are still written as parallel fields readable by LDK 0.2 and recomposed when read, with the in-flight contribution stored under a new TLV. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A channel may have splice attempts in progress: one under negotiation with the counterparty and any negotiated transactions (the original splice and any RBF replacements) waiting on confirmations. This state was only observable through events and the broadcaster's TransactionType::InteractiveFunding, neither of which can be queried on demand. Add an optional splice_details field to ChannelDetails exposing the negotiation status and our contribution to it, the negotiated candidates (txid, post-splice channel value, our contribution, and confirmation progress), and the txids of any splice_locked messages exchanged with the counterparty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
👋 Thanks for assigning @wpaulino as a reviewer! |
| // Data written before the in-flight round's contribution was stored separately kept it | ||
| // as the last entry while a negotiation was pending. | ||
| if negotiation_contribution.is_none() && contributions.len() > fundings.len() { | ||
| negotiation_contribution = contributions.pop(); | ||
| } | ||
| // An in-flight contribution is only meaningful while its negotiation round is alive; | ||
| // rounds not surviving serialization round trips have their events handled separately. | ||
| if funding_negotiation.is_none() { | ||
| negotiation_contribution = None; | ||
| } |
There was a problem hiding this comment.
This heuristic for recovering the in-flight contribution from LDK 0.2 data is insufficient when there are leading counterparty-only (None) candidates.
In the old format the in-flight (AwaitingSignatures) round's contribution was appended as the last entry of contributions while a negotiation was pending, but it is not in negotiated_candidates yet. Since contributions form a suffix, with K contributed prior candidates and M total prior candidates (M >= K), the old contributions length is K + 1 (the +1 being the in-flight). This heuristic only pops when K + 1 > M, i.e. only when K == M (no leading None candidates).
When M > K (at least one leading counterparty-only candidate), K + 1 <= M, so the pop is skipped and:
negotiation_contributionstaysNone(the in-flight contribution is lost), andcontrib_offset = M - (K+1)mis-assigns: one prior candidate atcontrib_offsetwrongly receives a contribution and the in-flight contribution is consumed into the candidate list.
Concrete minimal case from old 0.2 data: candidates=[None] (counterparty-only splice) followed by a we-contribute RBF round at AwaitingSignatures → old bytes are fundings=[f0], contributions=[c]. Here 1 > 1 is false, so this reads back as candidate[0].contribution = Some(c) and negotiation_contribution = None, both incorrect (should be candidate[0].contribution = None, negotiation_contribution = Some(c)). This is exactly the first-contribution-on-RBF flow exercised by the new test, and would corrupt contribution tracking / splice_funding_failed and later trip the suffix debug_assert! in splice_funding_negotiated.
The presence of an in-flight contribution in old data can't be detected by length alone here; it needs to be tied to funding_negotiation.is_some() together with the suffix structure.
SummaryFound one significant backward-compatibility bug in the refactored serialization.
Cross-cutting notes
|
Wallets like LDK Node need to show a channel's pending splice state (e.g., when displaying channel details), but it is currently only observable by tracking events or the broadcaster's
TransactionType::InteractiveFunding, neither of which can be queried on demand.This adds an optional
splice_detailsfield toChannelDetailsexposing that state: any splice/RBF round under negotiation (status, feerate, our contribution, txid and post-splice value once known) and any negotiated candidates awaiting confirmations, along with thesplice_lockedtxids exchanged.The first commit refactors
PendingFundingto store each round's contribution with its negotiated candidate instead of in a tail-aligned parallel list, which every consumer had to realign (and which this API initially got wrong). The on-disk format is unchanged and remains readable by LDK 0.2.