Skip to content

Include interactive funding candidates on broadcast#4570

Open
jkczyz wants to merge 2 commits intolightningdevkit:mainfrom
jkczyz:2026-04-splice-transaction-type
Open

Include interactive funding candidates on broadcast#4570
jkczyz wants to merge 2 commits intolightningdevkit:mainfrom
jkczyz:2026-04-splice-transaction-type

Conversation

@jkczyz
Copy link
Copy Markdown
Contributor

@jkczyz jkczyz commented Apr 17, 2026

Include every negotiated funding candidate (original + RBF replacements) at broadcast time so downstream consumers can update their own state tracking from the broadcaster callback. The local contribution data isn't recoverable from the on-chain transaction, and the replaced txids aren't either, so the broadcaster callback is the only place where this information can be observed.

  • TransactionType::Splice is replaced with TransactionType::InteractiveFunding { candidates: Vec<FundingCandidate> }. Each FundingCandidate carries its txid and the channels participating in it; each ChannelFunding carries the channel id, counterparty node id, purpose (Establishment / Splice), and the local node's contribution for this candidate. The list covers every negotiated candidate in order — original first, then each RBF replacement — letting downstream reconcile any historical txid, not just the immediate predecessor. The alternative (reacting to SplicePending) was worse: that event races with BDK wallet sync and doesn't carry the replaced txid.
  • The new variant is structured to be forward-compatible with batches and V2 (dual-funded) channel establishment, neither of which is implemented today. For the current single-channel splice path, each candidate's channels list has length 1.
  • FundingCandidate, ChannelFunding, and FundingPurpose implement Writeable / Readable so downstream can persist them directly without mirroring.
  • FundingContribution gains public getters for estimated_fee, inputs, and max_feerate; feerate is elevated from pub(super) to pub. Combined with existing value_added, outputs, change_output, this exposes everything a downstream consumer needs without reaching into the raw transaction.

The Hash derive on TransactionType is dropped (unused in the workspace; FundingContribution transitively contains ConfirmedUtxo which lacks Hash).

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 17, 2026

👋 Thanks for assigning @TheBlueMatt 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.

@jkczyz jkczyz requested review from TheBlueMatt and wpaulino April 17, 2026 21:14
@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

ldk-claude-review-bot commented Apr 17, 2026

Good — no remaining references to the old TransactionType::Splice variant.

No new issues found beyond what was already flagged in prior review comments. Here's my summary:

Prior review status:

  • channel.rs:9395filter_map silently dropping candidates: Resolved. Code now uses .map() + .expect().
  • chaininterface.rs:123 — Grammar nit ("An" → "A"): Resolved.
  • chaininterface.rs:175 — Dead code TLV serialization impls: Still applicable (minor nit, already flagged).
  • channel.rs:7256 — 0-conf rebroadcast type mismatch: This was about pre-existing behavior outside the scope of this PR's changes, and the referenced test doesn't exist in this PR. Not applicable to this diff.

New issues: None found.

The contribution offset math (contrib_offset = negotiated_candidates.len() - contributions.len()) correctly handles the invariant that missing contributions are always leading entries. The TransactionType variant rename from Splice to InteractiveFunding is applied consistently across all match sites. The new public getters on FundingContribution are straightforward field accessors. Test assertions properly validate the candidate chain, replaced-txid contract, and contribution presence for both initiator and acceptor.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

1 similar comment
@ldk-reviews-bot
Copy link
Copy Markdown

🔔 2nd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment thread lightning/src/ln/channel.rs Outdated
Comment on lines +7241 to +7256
if !self.context.is_manual_broadcast
&& !waiting_for_batch
&& self.funding.get_funding_tx_confirmation_height().is_none()
{
if let Some(tx) = &self.funding.funding_transaction {
return Some((
tx.clone(),
TransactionType::Funding {
channels: vec![(
self.context.counterparty_node_id,
self.context.channel_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.

Bug: After 0-conf splice promotion, self.pending_splice is None (cleared at maybe_promote_splice_funding line 11680) and self.funding holds the splice's FundingScope with funding_tx_confirmation_height == 0 and funding_transaction = Some(splice_tx). This initial-funding branch will match:

  • is_manual_broadcast = false (normal channel)
  • waiting_for_batch = false (channel is ChannelReady)
  • get_funding_tx_confirmation_height().is_none() = true (height is 0)
  • funding_transaction = Some(splice_tx)

...and return TransactionType::Funding wrapping the splice transaction. The splice branch below is unreachable because self.pending_splice is None.

The test test_splice_rebroadcast_uses_splice_type_after_0conf_promotion (added in this PR) expects TransactionType::Splice with the original contribution preserved, but this code would produce TransactionType::Funding. That test will fail.

A possible fix: check self.funding.channel_transaction_parameters.splice_parent_funding_txid — it is Some(...) for promoted splices and None for initial funding. You'd also need to persist the contribution and replaced_txid metadata somewhere that survives promotion (e.g., on FundingScope or ChannelContext), since pending_splice is cleared.

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.

Discussed offline. Dropped the commit to retry for now. We may do this later in ChannelMonitor where we'll store the FundingContribution.

@jkczyz jkczyz force-pushed the 2026-04-splice-transaction-type branch from dbaf6d9 to 86dcede Compare April 22, 2026 18:36
Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Needs rebase, also ci is very sad.

@jkczyz jkczyz self-assigned this Apr 23, 2026
@jkczyz jkczyz force-pushed the 2026-04-splice-transaction-type branch from 86dcede to dacf092 Compare April 23, 2026 21:42
@jkczyz
Copy link
Copy Markdown
Contributor Author

jkczyz commented Apr 23, 2026

Rebased. Compilation failures were from new tests in main.

@jkczyz jkczyz requested a review from TheBlueMatt April 23, 2026 22:17
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 78.04878% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.16%. Comparing base (42e198c) to head (d94e8fb).

Files with missing lines Patch % Lines
lightning/src/ln/funding.rs 25.00% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4570      +/-   ##
==========================================
+ Coverage   87.15%   87.16%   +0.01%     
==========================================
  Files         161      161              
  Lines      109251   109284      +33     
  Branches   109251   109284      +33     
==========================================
+ Hits        95215    95260      +45     
+ Misses      11560    11548      -12     
  Partials     2476     2476              
Flag Coverage Δ
fuzzing-fake-hashes 31.17% <75.60%> (+0.01%) ⬆️
fuzzing-real-hashes 22.91% <0.00%> (-0.02%) ⬇️
tests 86.23% <78.04%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 3rd Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 1st Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 4th Reminder

Hey @TheBlueMatt @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment thread lightning/src/chain/chaininterface.rs Outdated
contribution: Option<FundingContribution>,
/// For an RBF replacement, the txid of the prior negotiated splice candidate being
/// replaced. `None` for the first splice attempt.
replaced_txid: Option<Txid>,
Copy link
Copy Markdown
Contributor

@tnull tnull Apr 27, 2026

Choose a reason for hiding this comment

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

Hmm, what happens when we make multiple subsequent RBFs? Could/should we make this a Vec and track all prior conflicting Txids to make sure we're not losing any intermediary step when tracking these events?

Also, when integrating BDK's RBF events in LDK Node we found that it would be nice if the API clearly defined an ordering for the replaced_txids, i.e., so we can always identify the 'original' txid (under which we started tracking a certain payment). That is, if we make this a Vec it would be great if the API could guarantee that replaced_txids[0] is always the 'original'.

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.

That seems reasonable, though things get hairy if we have multiple splice-RBFs that conflict across channels. ISTM LDK Node might need to track that somehow, but maybe we can just avoid ever building such things?

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.

Ok, discussed a bit offline. Ideally, we'll want to use a format in LDK Node that is compatible with batching. I've pushed a change that refactors TransactionType::Splice to TransactionType::InteractiveFunding, which supports batches of both v2 channel establishment and splices in the future. It also contains all previous attempts (including contributions) along with the corresponding txid instead of replaced_txid. Including simplifies the downstream handling. Then in 0.4 we can add re-broadcasts.

See how it's used in LDK Node here: lightningdevkit/ldk-node#888. LMK what you think.

TheBlueMatt
TheBlueMatt previously approved these changes Apr 28, 2026
Copy link
Copy Markdown
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Changes LGTM, no strong opinion on the API, if it works for LDK Node alright, though I agree exposing all the previous RBFs might be nice.

Comment thread lightning/src/chain/chaininterface.rs Outdated
contribution: Option<FundingContribution>,
/// For an RBF replacement, the txid of the prior negotiated splice candidate being
/// replaced. `None` for the first splice attempt.
replaced_txid: Option<Txid>,
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.

That seems reasonable, though things get hairy if we have multiple splice-RBFs that conflict across channels. ISTM LDK Node might need to track that somehow, but maybe we can just avoid ever building such things?

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 5th Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz force-pushed the 2026-04-splice-transaction-type branch 2 times, most recently from 925b58d to 4b82770 Compare April 29, 2026 21:13
@jkczyz jkczyz requested review from TheBlueMatt and tnull April 29, 2026 22:35
TheBlueMatt
TheBlueMatt previously approved these changes Apr 30, 2026
@TheBlueMatt
Copy link
Copy Markdown
Collaborator

CI is very sad but feel free to squash.

@jkczyz jkczyz force-pushed the 2026-04-splice-transaction-type branch from 4b82770 to 741430b Compare April 30, 2026 18:49
@jkczyz
Copy link
Copy Markdown
Contributor Author

jkczyz commented Apr 30, 2026

Squashed and rebased to fix CI.

Comment on lines +160 to +175
impl_writeable_tlv_based!(FundingCandidate, {
(1, txid, required),
(3, channels, required_vec),
});

impl_writeable_tlv_based!(ChannelFunding, {
(1, counterparty_node_id, required),
(3, channel_id, required),
(5, purpose, required),
(7, contribution, option),
});

impl_writeable_tlv_based_enum!(FundingPurpose,
(0, Establishment) => {},
(2, Splice) => {},
);
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: these three serialization impls (FundingCandidate, ChannelFunding, FundingPurpose) appear to be dead code — TransactionType is never serialized (no Writeable/Readable impl), and these types are only used inside TransactionType::InteractiveFunding, which is purely in-memory and passed through BroadcasterInterface::broadcast_transactions as a reference.

If these are forward-looking (e.g., for future persistence or cross-process messaging), a brief comment explaining the intent would help. Otherwise they could be removed to avoid confusion.

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.

We need these for downstream

Comment thread lightning/src/ln/channel.rs Outdated
@jkczyz jkczyz changed the title Add splice details to TransactionType::Splice Include interactive funding candidates on broadcast Apr 30, 2026
@jkczyz jkczyz force-pushed the 2026-04-splice-transaction-type branch from 741430b to 8c3d58f Compare April 30, 2026 19:01
Comment thread lightning/src/chain/chaininterface.rs Outdated
@jkczyz jkczyz requested a review from TheBlueMatt April 30, 2026 19:16
@jkczyz jkczyz force-pushed the 2026-04-splice-transaction-type branch from 8c3d58f to f245f31 Compare April 30, 2026 19:17
jkczyz and others added 2 commits April 30, 2026 14:19
Replace TransactionType::Splice with TransactionType::InteractiveFunding
so downstream consumers can update their own state tracking from the
broadcast callback. The local contribution data isn't recoverable from
the on-chain transaction, so the broadcast must surface it directly.
Each candidate carries the participating channels and their local
contributions; the broadcast lists every negotiated candidate — original
first, then each RBF replacement — letting downstream reconcile any
historical txid, not just the immediate predecessor.

The new variant is structured to be forward-compatible with batches and
V2 (dual-funded) channel establishment, neither of which is implemented
today. The new types are Writeable/Readable so downstream can persist
them directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add public getters for `estimated_fee`, `inputs`, and `max_feerate`, and
elevate `feerate` from `pub(super)` to `pub`. Together with the existing
`value_added`, `outputs`, and `change_output`, this gives downstream
consumers of `TransactionType::Splice` (notably LDK Node, which updates
`PaymentDetails` from the broadcast callback) the data they need
without reaching into the raw transaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jkczyz jkczyz force-pushed the 2026-04-splice-transaction-type branch from f245f31 to d94e8fb Compare April 30, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants