Skip to content

Include failure context in splice events#4514

Open
jkczyz wants to merge 18 commits intolightningdevkit:mainfrom
jkczyz:2026-03-splice-rbf-fail-event
Open

Include failure context in splice events#4514
jkczyz wants to merge 18 commits intolightningdevkit:mainfrom
jkczyz:2026-03-splice-rbf-fail-event

Conversation

@jkczyz
Copy link
Copy Markdown
Contributor

@jkczyz jkczyz commented Mar 26, 2026

When a splice negotiation round fails, the user needs to know why it failed and what they can do about it. Previously, SpliceFailed gave no indication of the failure cause and didn't return the contribution, making it difficult to retry.

This PR adds failure context to the splice events so users can make informed retry decisions:

  • NegotiationFailureReason indicates what went wrong — peer disconnect, counterparty abort, invalid contribution, insufficient feerate, channel closing, etc. Each variant documents how to resolve it.
  • The FundingContribution from the failed round is returned in the event. Users can feed it back to funding_contributed to retry as-is, or inspect it to understand which inputs, outputs, and feerate were used when constructing a new contribution.
  • SplicePending and SpliceFailed are renamed to SpliceNegotiated and SpliceNegotiationFailed to reflect that each negotiation round (initial or RBF) independently resolves to one of these two outcomes.
  • DiscardFunding is now emitted before SpliceNegotiationFailed so wallet inputs are unlocked before the failure handler runs. Otherwise, a retry during SpliceNegotiationFailed handling would leave inputs locked, and the subsequent DiscardFunding would incorrectly unlock inputs committed to the new attempt.

Additionally, SpliceFundingFailed internals are simplified:

  • Dead funding_txo and channel_type fields are removed.
  • An unreachable was_negotiated check is removed from the contribution pop.
  • Inputs and outputs are derived from FundingContribution via a unified splice_funding_failed_for! macro, replacing the old approach of extracting them from FundingNegotiation variants. Unused conversion methods are removed.

Also fixes a bug in into_unique_contributions where outputs were compared by full TxOut equality instead of script_pubkey. This could fail to filter change outputs that reused the same address but had different values across RBF rounds.

@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Mar 26, 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 self-assigned this Mar 26, 2026
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch 2 times, most recently from 333980d to c2cad50 Compare March 28, 2026 00:08
@jkczyz jkczyz requested review from TheBlueMatt and wpaulino March 28, 2026 00:10
Comment thread lightning/src/events/mod.rs Outdated
/// sent an invalid message). Retry by calling [`ChannelManager::splice_channel`].
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
NegotiationError,
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.

Note we could have this wrap AbortReason -- we had an internal NegotiationError struct that already wrapped this along with contributions -- but it would require making AbortReason public. Not sure if we want to expose all its variants.

fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self {
match self {
QuiescentError::FailSplice(_, ref mut r) => *r = reason,
_ => debug_assert!(false, "Expected FailSplice variant"),
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.

Little unfortunate we need to do this, but I couldn't find a better way other than passing NegotiationFailureReason to abandon_quiescent_action and quiescent_action_into_error, which doesn't seem right since they could be for future QuiescentAction variants.

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.

Does feel like quiescent_action_into_error could take the NegotiationFailureReason and we could also use NegotiationFailureReason for dual-funding (the DiscardFunding variant) but this seems fine.

Comment on lines -1646 to -1649
/// The outpoint of the channel's splice funding transaction, if one was created.
abandoned_funding_txo: Option<OutPoint>,
/// The features that this channel will operate with, if available.
channel_type: Option<ChannelTypeFeatures>,
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.

Didn't feel like there was much value to these.

  • abandoned_funding_txo would be set only if there's a failure after the transaction was negotiated but before signed.
  • In the future, when channel_type can change, maybe it is just part of FundingContribution? Could also keep it if you prefer.

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

@jkczyz jkczyz marked this pull request as ready for review April 2, 2026 17:04
Comment on lines +3185 to 3193
impl QuiescentError {
fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self {
match self {
QuiescentError::FailSplice(_, ref mut r) => *r = reason,
_ => debug_assert!(false, "Expected FailSplice variant"),
}
self
}
}
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.

Minor: The debug_assert!(false) path is only reachable in non-production builds if a caller passes a non-FailSplice variant. Currently the only caller passes through quiescent_action_into_error which always produces FailSplice for production code (only DoNothing in test builds). But debug_assert!(false) will silently return self unchanged in release builds — consider using debug_assert! with a more specific message about which variant was unexpected, or restructuring so the method only accepts FailSplice to make this impossible.

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.

Comment on lines +6943 to +6947
let is_initiator = pending_splice
.funding_negotiation
.take()
.map(|negotiation| negotiation.is_initiator())
.unwrap_or(false);
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.

The is_initiator fallback when funding_negotiation is None defaults to false, which means splice_funding_failed_for! will return None for non-initiators (since the macro has None if !$is_initiator => None). This means if the funding negotiation was already taken before reset_pending_splice_state is called and the node was actually the initiator, the splice failure event would be silently dropped.

Is funding_negotiation ever None when reset_pending_splice_state is called? If so, defaulting to false may suppress failure events for the initiator. If not, this should be a debug_assert or .expect() to catch violations of that invariant.

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.

Added debug assertions.

Comment on lines +6989 to +6993
let is_initiator = pending_splice
.funding_negotiation
.as_ref()
.map(|negotiation| negotiation.is_initiator())
.unwrap_or(false);
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.

Same concern here: funding_negotiation.as_ref() being None causes is_initiator to default to false, which will cause the splice_funding_failed_for! macro to return None for non-initiators. Since maybe_splice_funding_failed is the read-only version used during serialization, silently returning None means the splice failure events won't be persisted and the user won't learn about the failure after restart.

Consider whether funding_negotiation can actually be None in this path, and if not, using expect to enforce the invariant.

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.

Added debug assertions.

Comment thread lightning/src/ln/funding.rs
@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

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

I've completed a thorough re-review of the entire PR diff, reading all modified files and verifying the logic of key methods. Here are my findings:

Correction to Prior Review

Forward compatibility of NegotiationFailureReason: My prior cross-cutting concern was incorrect. The variant IDs are all odd (1, 3, 5, 7, 9, 11, 13, 15, 17), NOT even as I previously stated. In impl_writeable_tlv_based_enum_upgradable!, unknown odd variant IDs return Ok(None), which correctly degrades to NegotiationFailureReason::Unknown via the upgradable_option + unwrap_or(Unknown) pattern. Forward compatibility is correctly handled.

Previously Flagged Issues (still applicable)

All inline comments from the prior review pass remain valid:

  1. lightning/src/ln/channel.rs:3177with_negotiation_failure_reason silently no-ops in release for non-FailSplice variants.
  2. lightning/src/ln/channel.rs:7257is_initiator defaults to false when funding_negotiation is None, potentially suppressing initiator failure events.
  3. lightning/src/ln/channel.rs:7307 — Same is_initiator defaulting issue in maybe_splice_funding_failed.
  4. lightning/src/ln/channel.rs:14080 — Duplicate quiescent action returns NegotiationFailureReason::Unknown because .with_negotiation_failure_reason() is not chained.
  5. lightning/src/ln/channelmanager.rs:18189 — Unnecessary .clone() on contribution.
  6. pending_changelog/4514-splice-negotiation-failed.txt:11 — Changelog omits outputs(), change_output(), and value_added() public accessors.

New Issues

No new issues found beyond the prior review pass. The following were verified as correct:

  • Promotion path (splice_locked) correctly filters contributions against the promoted transaction to identify discardable inputs/outputs.
  • reset_pending_splice_state correctly pops the current contribution before filtering with contributed_inputs()/contributed_outputs().
  • maybe_splice_funding_failed correctly uses prior_contributed_inputs()/prior_contributed_outputs() without popping (read-only).
  • SpliceFundingFailed::into_parts() correctly splits into optional FundingInfo and FundingContribution.
  • All emission sites consistently emit DiscardFunding before SpliceNegotiationFailed.
  • The can_initiate_rbf check in try_stfu is correct and uses the right contribution variable.
  • FundingContribution serialization uses all-odd TLV field IDs for forward compatibility.
  • The serialization path's temporary event injection + truncation pattern is correct and avoids duplicates on reload.

@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from c2cad50 to c469423 Compare April 3, 2026 01:25
@jkczyz
Copy link
Copy Markdown
Contributor Author

jkczyz commented Apr 3, 2026

Rebased

Comment thread lightning/src/ln/channelmanager.rs Outdated
@@ -4128,6 +4118,16 @@ impl<
},
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: Unlike handle_quiescent_error (line ~6619), this site unconditionally emits DiscardFunding even when contributed_inputs and contributed_outputs are both empty (which happens for the initiator when all inputs are committed to prior rounds). The same inconsistency exists at lines ~4417, ~4920, ~11853, ~12012, ~12180, ~12329, ~15448, and ~18122.

Consider adding the same guard here:

if !splice_funding_failed.contributed_inputs.is_empty() || !splice_funding_failed.contributed_outputs.is_empty() {
    // emit DiscardFunding
}

Or, equivalently, have the macro return None for the initiator when there are no unique contributions (matching the non-initiator behavior) and emit SpliceNegotiationFailed without DiscardFunding in those cases.

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.

Fixed by making SpliceFundingFailed consumable into Option-al parts rather than exposing fields. The into_parts method now checks for empty contributions.

Comment thread lightning/src/ln/channelmanager.rs Outdated
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 83.54978% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.25%. Comparing base (42e198c) to head (65a257b).
⚠️ Report is 26 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/events/mod.rs 25.00% 18 Missing ⚠️
lightning/src/ln/channelmanager.rs 90.83% 5 Missing and 6 partials ⚠️
lightning/src/ln/channel.rs 93.05% 5 Missing ⚠️
lightning/src/ln/funding.rs 40.00% 3 Missing ⚠️
lightning/src/ln/functional_test_utils.rs 90.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4514      +/-   ##
==========================================
- Coverage   87.15%   86.25%   -0.90%     
==========================================
  Files         161      159       -2     
  Lines      109251   109233      -18     
  Branches   109251   109233      -18     
==========================================
- Hits        95215    94221     -994     
- Misses      11560    12392     +832     
- Partials     2476     2620     +144     
Flag Coverage Δ
fuzzing-fake-hashes ?
fuzzing-real-hashes ?
tests 86.25% <83.54%> (+0.03%) ⬆️

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.

@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch 2 times, most recently from 4f6b1a6 to 0609565 Compare April 3, 2026 15:33
@jkczyz
Copy link
Copy Markdown
Contributor Author

jkczyz commented Apr 3, 2026

  • Forward compatibility of NegotiationFailureReason: Uses impl_writeable_tlv_based_enum! with all-even variant IDs. A future variant added by a newer LDK version would cause UnknownRequiredFeature on deserialization, blocking ChannelManager loading on older versions. Consider impl_writeable_tlv_based_enum_upgradable! or documenting the constraint.

Changed to use impl_writeable_tlv_based_enum_upgradable and changed the reason from default_value to upgradable_option, defaulting to NegotiationFailureReason::Unknown.

@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from 0609565 to 44ecc06 Compare April 3, 2026 15:39
@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.

1 similar comment
@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

🔔 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.

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.

The changes to address feedback from the other pr lgtm, though this now needs rebase again :/

Comment thread lightning/src/events/mod.rs Outdated
Comment on lines +170 to +171
| Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
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.

Do we really want to consider these retryable?

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.

Hmm... probably not but may depend. See https://github.com/lightningdevkit/rust-lightning/pull/4514/changes/BASE..746d912772ee07165d827fd44af419ab348d673c#r3040809076. Best to make them false at least indicating the user may need to take some action first.

Each splice negotiation round can fail for different reasons, but
Event::SpliceFailed previously gave no indication of what went wrong.
Add a NegotiationFailureReason enum so users can distinguish failures
and take appropriate action (e.g., retry with a higher feerate vs.
wait for the channel to become usable).

The reason is determined at each channelmanager emission site based on
context rather than threaded through channel.rs internals, since the
channelmanager knows the triggering context (disconnect, tx_abort,
shutdown, etc.) while channel.rs functions like abandon_quiescent_action
handle both splice and non-splice quiescent actions.

The one exception is QuiescentError::FailSplice, which carries a reason
alongside the SpliceFundingFailed. This is appropriate because FailSplice
is already splice-specific, and the channel.rs code that constructs it
(e.g., contribution validation, feerate checks) knows the specific
failure cause. A with_negotiation_failure_reason method on QuiescentError
allows callers to override the default when needed.

Older serializations that lack the reason field default to Unknown via
default_value in deserialization. The persistence reload path uses
PeerDisconnected since a reload implies the peer connection was lost.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from 746d912 to 4928ea2 Compare April 28, 2026 22:05
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from 4928ea2 to 678bbc4 Compare April 28, 2026 22:17
Copy link
Copy Markdown
Contributor Author

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Rebased

Comment thread lightning/src/events/mod.rs Outdated
Comment on lines +170 to +171
| Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
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.

Hmm... probably not but may depend. See https://github.com/lightningdevkit/rust-lightning/pull/4514/changes/BASE..746d912772ee07165d827fd44af419ab348d673c#r3040809076. Best to make them false at least indicating the user may need to take some action first.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 14th 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.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 15th 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.

@ldk-reviews-bot
Copy link
Copy Markdown

🔔 16th 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.

Comment thread lightning/src/events/mod.rs Outdated
Comment thread lightning/src/ln/splicing_tests.rs Outdated
&channel_id,
funding_contribution,
NegotiationFailureReason::CounterpartyAborted {
msg: UntrustedString("No active signing session. The associated funding transaction may have already been broadcast.".to_string()),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe we should rephrase this error string? It's not very clear to the user what's happening on the counterparty's side.

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.

Yeah, this message is no good. Fixed.

Comment on lines +7021 to +7024
contributed_inputs: Vec<bitcoin::OutPoint>,

/// Outputs contributed to the splice transaction.
pub contributed_outputs: Vec<bitcoin::TxOut>,
contributed_outputs: Vec<bitcoin::TxOut>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should note that these won't include inputs/outputs that were also contributed to prior rounds, otherwise they may be interpreted as redundant since the full contribution has helpers to expose its contributed inputs/outputs.

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.

Done.

Comment thread lightning/src/ln/splicing_tests.rs Outdated
let funding_contribution =
funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap();
let funding_contribution = funding_template
.without_prior_contribution(feerate, FeeRate::MAX)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are we going with a fresh contribution?

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.

Thought it might be too big of a change given including them would necessitate dropping the value_added parameter. Turns out the change wasn't that bad. Fixed now.

promoted_tx.output.iter(),
)
})
.map(|(inputs, outputs)| FundingInfo::Contribution { inputs, outputs })
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't we check if inputs/outputs are empty? Any reason to not have all discarded inputs/outputs over a single FundingInfo::Contribution?

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.

Shouldn't we check if inputs/outputs are empty?

That's already checked in into_unique_contributions.

Any reason to not have all discarded inputs/outputs over a single FundingInfo::Contribution?

Not sure if there is a strong reason one way or the other. Though separate ones make it simpler to compute using into_unique_contributions. So would prefer less code if possible.

jkczyz and others added 16 commits May 5, 2026 13:09
Since `reason` is read as `upgradable_option`, odd variant IDs let
older readers skip unknown future variants (falling back to `Unknown`)
instead of failing with `UnknownRequiredFeature`.
Per BOLT 2's channel_reestablish rationale, next_funding's tx_abort
branch is reached when the receiver did not sign and has discarded
the funding transaction. The prior "may have been broadcast" wording
was misleading — broadcast requires both signing steps to complete,
which is incompatible with this path.
Replace the abandoned_funding_txo and channel_type fields on
Event::SpliceFailed with an Option<FundingContribution> from the failed
round. Users can feed this back to funding_contributed to retry or use
it to inform a fresh attempt via splice_channel.

Also makes FundingContribution::feerate() public so users can inspect
the feerate when deciding whether to retry or bump.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These fields hold only the unique-to-this-round contributions; the full
set is reachable via the `contribution` field's helpers, so duplicating
prior-round entries here would be redundant.
Make do_initiate_rbf_splice_in (and do_initiate_rbf_splice_in_and_out)
amend the prior contribution with a pure feerate bump. Every caller was
passing the same `added_value` as the initial splice; with amend semantics
the parameter is unused, so drop it.

Add do_initiate_splice_in_at_feerate for first-contribution cases that
need an explicit feerate (used by the tiebreak tests' node[1]).
Reverse the event ordering at all emission sites so that
Event::DiscardFunding is emitted before Event::SpliceFailed. If the
user retries the splice when handling SpliceFailed, the contributed
inputs would still be locked. A subsequent DiscardFunding would then
incorrectly unlock inputs that are now committed to the new attempt.
Emitting DiscardFunding first avoids this by ensuring inputs are
unlocked before any retry occurs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename Event::SplicePending to Event::SpliceNegotiated and
Event::SpliceFailed to Event::SpliceNegotiationFailed. These names
better reflect the per-round semantics: each negotiation attempt
resolves to one of these two outcomes, independent of the overall
splice lifecycle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The was_negotiated check is unnecessary because reset_pending_splice_state
only runs when funding_negotiation is present, meaning
on_tx_signatures_exchange hasn't been called yet. Since the feerate is
only recorded in last_funding_feerate_sat_per_1000_weight during
on_tx_signatures_exchange, the current round's feerate can never match
it. So the contribution can always be unconditionally popped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Filter outputs by script_pubkey rather than full TxOut equality. Outputs
reusing the same address as a prior round are still considered committed
even if the value differs (e.g., different change amounts across RBF
rounds with different feerates).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the maybe_create_splice_funding_failed! macro and
splice_funding_failed_for method with a unified splice_funding_failed_for!
macro that derives contributed inputs and outputs from the
FundingContribution rather than extracting them from the negotiation
state.

Callers pass ident parameters for which PendingSplice filtering methods
to use: contributed_inputs/contributed_outputs when the current round's
contribution has been popped or was never pushed, and
prior_contributed_inputs/prior_contributed_outputs for the read-only
persistence path where the contribution is cloned instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hods

Now that splice_funding_failed_for! derives inputs and outputs from
FundingContribution directly, remove the unused NegotiationError struct
and into_negotiation_error methods from the interactive tx types, along
with the into/to_contributed_inputs_and_outputs methods on
ConstructedTransaction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
If splice_locked is sent between our outgoing STFU and the
counterparty's STFU response, the stfu() handler would proceed to
send tx_init_rbf for an already-confirmed splice. Guard against this
by re-checking can_initiate_rbf when entering quiescence. Disconnect
because there is no way to cancel quiescence after both sides have
exchanged STFU.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The earlier fixup dropped do_initiate_rbf_splice_in's value_added
parameter and switched it to amend semantics. This test specifically
verifies that DiscardFunding fires on the can_initiate_rbf failure
path with a unique input surviving filtering, which requires fresh
input selection across rounds. Inline the splice_channel +
funding_contributed flow with without_prior_contribution rather than
using the helper to preserve the original assertions.
When a splice funding is promoted, produce FundingInfo::Contribution
instead of FundingInfo::Tx for the discarded funding events. Each
contribution is filtered against the promoted funding transaction's
inputs and outputs, so only inputs and outputs unique to the discarded
round are reported.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This test specifically requires fresh inputs across RBF rounds (it
clears the wallet between rounds and asserts the inputs differ).
Inline the splice_channel + funding_contributed flow with
without_prior_contribution rather than using a helper, since this
is the only call site that needs that behavior.
@jkczyz jkczyz force-pushed the 2026-03-splice-rbf-fail-event branch from 678bbc4 to 65a257b Compare May 6, 2026 00:21
@jkczyz jkczyz requested review from TheBlueMatt and wpaulino May 6, 2026 04:30
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