Skip to content

Commit 4ee3ef1

Browse files
jkczyzclaude
andcommitted
Split DiscardFunding from SpliceFailed event
When a splice fails, users need to reclaim UTXOs they contributed to the funding transaction. Previously, the contributed inputs and outputs were included in the SpliceFailed event. This commit splits them into a separate DiscardFunding event with a new FundingInfo::Contribution variant, providing a consistent interface for UTXO cleanup across all funding failure scenarios. Changes: - Add FundingInfo::Contribution variant to hold inputs/outputs for DiscardFunding events - Remove contributed_inputs/outputs fields from SpliceFailed event - Add QuiescentError enum for better error handling in funding_contributed - Emit DiscardFunding on all funding_contributed error paths - Filter duplicate inputs/outputs when contribution overlaps existing pending contribution - Return Err(APIError) from funding_contributed on all error cases - Add comprehensive test coverage for funding_contributed error paths Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0c5cc5e commit 4ee3ef1

6 files changed

Lines changed: 667 additions & 78 deletions

File tree

lightning/src/events/mod.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ pub enum FundingInfo {
7878
/// The outpoint of the funding
7979
outpoint: transaction::OutPoint,
8080
},
81+
/// The contributions used to for a dual funding or splice funding transaction.
82+
Contribution {
83+
/// UTXOs spent as inputs contributed to the funding transaction.
84+
inputs: Vec<OutPoint>,
85+
/// Outputs contributed to the funding transaction.
86+
outputs: Vec<TxOut>,
87+
},
8188
}
8289

8390
impl_writeable_tlv_based_enum!(FundingInfo,
@@ -86,6 +93,10 @@ impl_writeable_tlv_based_enum!(FundingInfo,
8693
},
8794
(1, OutPoint) => {
8895
(1, outpoint, required)
96+
},
97+
(2, Contribution) => {
98+
(0, inputs, optional_vec),
99+
(1, outputs, optional_vec),
89100
}
90101
);
91102

@@ -1580,10 +1591,6 @@ pub enum Event {
15801591
abandoned_funding_txo: Option<OutPoint>,
15811592
/// The features that this channel will operate with, if available.
15821593
channel_type: Option<ChannelTypeFeatures>,
1583-
/// UTXOs spent as inputs contributed to the splice transaction.
1584-
contributed_inputs: Vec<OutPoint>,
1585-
/// Outputs contributed to the splice transaction.
1586-
contributed_outputs: Vec<TxOut>,
15871594
},
15881595
/// Used to indicate to the user that they can abandon the funding transaction and recycle the
15891596
/// inputs for another purpose.
@@ -2378,8 +2385,6 @@ impl Writeable for Event {
23782385
ref counterparty_node_id,
23792386
ref abandoned_funding_txo,
23802387
ref channel_type,
2381-
ref contributed_inputs,
2382-
ref contributed_outputs,
23832388
} => {
23842389
52u8.write(writer)?;
23852390
write_tlv_fields!(writer, {
@@ -2388,8 +2393,6 @@ impl Writeable for Event {
23882393
(5, user_channel_id, required),
23892394
(7, counterparty_node_id, required),
23902395
(9, abandoned_funding_txo, option),
2391-
(11, *contributed_inputs, optional_vec),
2392-
(13, *contributed_outputs, optional_vec),
23932396
});
23942397
},
23952398
&Event::FundingNeeded { .. } => {
@@ -3015,8 +3018,6 @@ impl MaybeReadable for Event {
30153018
(5, user_channel_id, required),
30163019
(7, counterparty_node_id, required),
30173020
(9, abandoned_funding_txo, option),
3018-
(11, contributed_inputs, optional_vec),
3019-
(13, contributed_outputs, optional_vec),
30203021
});
30213022

30223023
Ok(Some(Event::SpliceFailed {
@@ -3025,8 +3026,6 @@ impl MaybeReadable for Event {
30253026
counterparty_node_id: counterparty_node_id.0.unwrap(),
30263027
abandoned_funding_txo,
30273028
channel_type,
3028-
contributed_inputs: contributed_inputs.unwrap_or_default(),
3029-
contributed_outputs: contributed_outputs.unwrap_or_default(),
30303029
}))
30313030
};
30323031
f()

lightning/src/ln/channel.rs

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2940,6 +2940,35 @@ pub(crate) enum QuiescentAction {
29402940
DoNothing,
29412941
}
29422942

2943+
pub(super) enum QuiescentError {
2944+
DoNothing,
2945+
DiscardFunding { inputs: Vec<bitcoin::OutPoint>, outputs: Vec<bitcoin::TxOut> },
2946+
FailSplice(SpliceFundingFailed),
2947+
}
2948+
2949+
impl From<QuiescentAction> for QuiescentError {
2950+
fn from(action: QuiescentAction) -> Self {
2951+
match action {
2952+
QuiescentAction::LegacySplice(_) => {
2953+
debug_assert!(false);
2954+
QuiescentError::DoNothing
2955+
},
2956+
QuiescentAction::Splice { contribution, .. } => {
2957+
let (contributed_inputs, contributed_outputs) =
2958+
contribution.into_contributed_inputs_and_outputs();
2959+
return QuiescentError::FailSplice(SpliceFundingFailed {
2960+
funding_txo: None,
2961+
channel_type: None,
2962+
contributed_inputs,
2963+
contributed_outputs,
2964+
});
2965+
},
2966+
#[cfg(any(test, fuzzing))]
2967+
QuiescentAction::DoNothing => QuiescentError::DoNothing,
2968+
}
2969+
}
2970+
}
2971+
29432972
pub(crate) enum StfuResponse {
29442973
Stfu(msgs::Stfu),
29452974
SpliceInit(msgs::SpliceInit),
@@ -11907,9 +11936,30 @@ where
1190711936

1190811937
pub fn funding_contributed<L: Logger>(
1190911938
&mut self, contribution: FundingContribution, locktime: LockTime, logger: &L,
11910-
) -> Result<Option<msgs::Stfu>, SpliceFundingFailed> {
11939+
) -> Result<Option<msgs::Stfu>, QuiescentError> {
1191111940
debug_assert!(contribution.is_splice());
1191211941

11942+
if let Some(QuiescentAction::Splice { contribution: existing, .. }) = &self.quiescent_action
11943+
{
11944+
let (new_inputs, new_outputs) = contribution.into_contributed_inputs_and_outputs();
11945+
11946+
// Filter out inputs/outputs already in the existing contribution
11947+
let inputs: Vec<_> = new_inputs
11948+
.into_iter()
11949+
.filter(|input| !existing.contributed_inputs().any(|e| e == *input))
11950+
.collect();
11951+
let outputs: Vec<_> = new_outputs
11952+
.into_iter()
11953+
.filter(|output| !existing.contributed_outputs().any(|e| *e == *output))
11954+
.collect();
11955+
11956+
if inputs.is_empty() && outputs.is_empty() {
11957+
return Err(QuiescentError::DoNothing);
11958+
}
11959+
11960+
return Err(QuiescentError::DiscardFunding { inputs, outputs });
11961+
}
11962+
1191311963
if let Err(e) = contribution.net_value().and_then(|our_funding_contribution| {
1191411964
// For splice-out, our_funding_contribution is adjusted to cover fees if there
1191511965
// aren't any inputs.
@@ -11920,37 +11970,15 @@ where
1192011970
let (contributed_inputs, contributed_outputs) =
1192111971
contribution.into_contributed_inputs_and_outputs();
1192211972

11923-
return Err(SpliceFundingFailed {
11973+
return Err(QuiescentError::FailSplice(SpliceFundingFailed {
1192411974
funding_txo: None,
1192511975
channel_type: None,
1192611976
contributed_inputs,
1192711977
contributed_outputs,
11928-
});
11978+
}));
1192911979
}
1193011980

11931-
self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime }).map_err(
11932-
|action| {
11933-
// FIXME: Any better way to do this?
11934-
if let QuiescentAction::Splice { contribution, .. } = action {
11935-
let (contributed_inputs, contributed_outputs) =
11936-
contribution.into_contributed_inputs_and_outputs();
11937-
SpliceFundingFailed {
11938-
funding_txo: None,
11939-
channel_type: None,
11940-
contributed_inputs,
11941-
contributed_outputs,
11942-
}
11943-
} else {
11944-
debug_assert!(false);
11945-
SpliceFundingFailed {
11946-
funding_txo: None,
11947-
channel_type: None,
11948-
contributed_inputs: vec![],
11949-
contributed_outputs: vec![],
11950-
}
11951-
}
11952-
},
11953-
)
11981+
self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime })
1195411982
}
1195511983

1195611984
fn send_splice_init(&mut self, instructions: SpliceInstructions) -> msgs::SpliceInit {
@@ -13069,19 +13097,19 @@ where
1306913097
#[rustfmt::skip]
1307013098
pub fn propose_quiescence<L: Logger>(
1307113099
&mut self, logger: &L, action: QuiescentAction,
13072-
) -> Result<Option<msgs::Stfu>, QuiescentAction> {
13100+
) -> Result<Option<msgs::Stfu>, QuiescentError> {
1307313101
log_debug!(logger, "Attempting to initiate quiescence");
1307413102

1307513103
if !self.context.is_usable() {
1307613104
log_debug!(logger, "Channel is not in a usable state to propose quiescence");
13077-
return Err(action);
13105+
return Err(action.into());
1307813106
}
1307913107
if self.quiescent_action.is_some() {
1308013108
log_debug!(
1308113109
logger,
1308213110
"Channel already has a pending quiescent action and cannot start another",
1308313111
);
13084-
return Err(action);
13112+
return Err(action.into());
1308513113
}
1308613114

1308713115
self.quiescent_action = Some(action);

0 commit comments

Comments
 (0)