Skip to content

Commit 2150f24

Browse files
authored
Merge pull request #4019 from TheBlueMatt/2025-08-splice-quiescent-real
Integrate Splicing with Quiescence
2 parents a9bbb24 + 9200308 commit 2150f24

File tree

7 files changed

+244
-62
lines changed

7 files changed

+244
-62
lines changed

lightning/src/events/bump_transaction/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ pub struct Utxo {
264264
pub satisfaction_weight: u64,
265265
}
266266

267+
impl_writeable_tlv_based!(Utxo, {
268+
(1, outpoint, required),
269+
(3, output, required),
270+
(5, satisfaction_weight, required),
271+
});
272+
267273
impl Utxo {
268274
/// Returns a `Utxo` with the `satisfaction_weight` estimate for a legacy P2PKH output.
269275
pub fn new_p2pkh(outpoint: OutPoint, value: Amount, pubkey_hash: &PubkeyHash) -> Self {

lightning/src/ln/channel.rs

Lines changed: 129 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2463,13 +2463,46 @@ impl PendingSplice {
24632463
}
24642464
}
24652465

2466+
pub(crate) struct SpliceInstructions {
2467+
adjusted_funding_contribution: SignedAmount,
2468+
our_funding_inputs: Vec<FundingTxInput>,
2469+
our_funding_outputs: Vec<TxOut>,
2470+
change_script: Option<ScriptBuf>,
2471+
funding_feerate_per_kw: u32,
2472+
locktime: u32,
2473+
original_funding_txo: OutPoint,
2474+
}
2475+
2476+
impl_writeable_tlv_based!(SpliceInstructions, {
2477+
(1, adjusted_funding_contribution, required),
2478+
(3, our_funding_inputs, required_vec),
2479+
(5, our_funding_outputs, required_vec),
2480+
(7, change_script, option),
2481+
(9, funding_feerate_per_kw, required),
2482+
(11, locktime, required),
2483+
(13, original_funding_txo, required),
2484+
});
2485+
24662486
pub(crate) enum QuiescentAction {
2467-
// TODO: Make this test-only once we have another variant (as some code requires *a* variant).
2487+
Splice(SpliceInstructions),
2488+
#[cfg(any(test, fuzzing))]
24682489
DoNothing,
24692490
}
24702491

2492+
pub(crate) enum StfuResponse {
2493+
Stfu(msgs::Stfu),
2494+
#[cfg_attr(not(splicing), allow(unused))]
2495+
SpliceInit(msgs::SpliceInit),
2496+
}
2497+
2498+
#[cfg(any(test, fuzzing))]
24712499
impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,
2472-
(99, DoNothing) => {},
2500+
(0, DoNothing) => {},
2501+
{1, Splice} => (),
2502+
);
2503+
#[cfg(not(any(test, fuzzing)))]
2504+
impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,,
2505+
{1, Splice} => (),
24732506
);
24742507

24752508
/// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`].
@@ -6158,7 +6191,7 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos
61586191
fn check_splice_contribution_sufficient(
61596192
channel_balance: Amount, contribution: &SpliceContribution, is_initiator: bool,
61606193
funding_feerate: FeeRate,
6161-
) -> Result<Amount, ChannelError> {
6194+
) -> Result<Amount, String> {
61626195
let contribution_amount = contribution.value();
61636196
if contribution_amount < SignedAmount::ZERO {
61646197
let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee(
@@ -6172,10 +6205,10 @@ fn check_splice_contribution_sufficient(
61726205
if channel_balance >= contribution_amount.unsigned_abs() + estimated_fee {
61736206
Ok(estimated_fee)
61746207
} else {
6175-
Err(ChannelError::Warn(format!(
6176-
"Available channel balance {} is lower than needed for splicing out {}, considering fees of {}",
6177-
channel_balance, contribution_amount.unsigned_abs(), estimated_fee,
6178-
)))
6208+
Err(format!(
6209+
"Available channel balance {channel_balance} is lower than needed for splicing out {}, considering fees of {estimated_fee}",
6210+
contribution_amount.unsigned_abs(),
6211+
))
61796212
}
61806213
} else {
61816214
check_v2_funding_inputs_sufficient(
@@ -6242,7 +6275,7 @@ fn estimate_v2_funding_transaction_fee(
62426275
fn check_v2_funding_inputs_sufficient(
62436276
contribution_amount: i64, funding_inputs: &[FundingTxInput], is_initiator: bool,
62446277
is_splice: bool, funding_feerate_sat_per_1000_weight: u32,
6245-
) -> Result<u64, ChannelError> {
6278+
) -> Result<u64, String> {
62466279
let estimated_fee = estimate_v2_funding_transaction_fee(
62476280
funding_inputs, &[], is_initiator, is_splice, funding_feerate_sat_per_1000_weight,
62486281
);
@@ -6265,10 +6298,9 @@ fn check_v2_funding_inputs_sufficient(
62656298

62666299
let minimal_input_amount_needed = contribution_amount.saturating_add(estimated_fee as i64);
62676300
if (total_input_sats as i64) < minimal_input_amount_needed {
6268-
Err(ChannelError::Warn(format!(
6269-
"Total input amount {} is lower than needed for contribution {}, considering fees of {}. Need more inputs.",
6270-
total_input_sats, contribution_amount, estimated_fee,
6271-
)))
6301+
Err(format!(
6302+
"Total input amount {total_input_sats} is lower than needed for contribution {contribution_amount}, considering fees of {estimated_fee}. Need more inputs.",
6303+
))
62726304
} else {
62736305
Ok(estimated_fee)
62746306
}
@@ -11022,9 +11054,13 @@ where
1102211054
/// - `change_script`: an option change output script. If `None` and needed, one will be
1102311055
/// generated by `SignerProvider::get_destination_script`.
1102411056
#[cfg(splicing)]
11025-
pub fn splice_channel(
11057+
pub fn splice_channel<L: Deref>(
1102611058
&mut self, contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: u32,
11027-
) -> Result<msgs::SpliceInit, APIError> {
11059+
logger: &L,
11060+
) -> Result<Option<msgs::Stfu>, APIError>
11061+
where
11062+
L::Target: Logger,
11063+
{
1102811064
if self.holder_commitment_point.current_point().is_none() {
1102911065
return Err(APIError::APIMisuseError {
1103011066
err: format!(
@@ -11036,7 +11072,7 @@ where
1103611072

1103711073
// Check if a splice has been initiated already.
1103811074
// Note: only a single outstanding splice is supported (per spec)
11039-
if self.pending_splice.is_some() {
11075+
if self.pending_splice.is_some() || self.quiescent_action.is_some() {
1104011076
return Err(APIError::APIMisuseError {
1104111077
err: format!(
1104211078
"Channel {} cannot be spliced, as it has already a splice pending",
@@ -11054,8 +11090,6 @@ where
1105411090
});
1105511091
}
1105611092

11057-
// TODO(splicing): check for quiescence
11058-
1105911093
let our_funding_contribution = contribution.value();
1106011094
if our_funding_contribution == SignedAmount::ZERO {
1106111095
return Err(APIError::APIMisuseError {
@@ -11150,8 +11184,50 @@ where
1115011184
}
1115111185
}
1115211186

11153-
let prev_funding_input = self.funding.to_splice_funding_input();
11187+
let original_funding_txo = self.funding.get_funding_txo().ok_or_else(|| {
11188+
debug_assert!(false);
11189+
APIError::APIMisuseError { err: "Channel isn't yet fully funded".to_owned() }
11190+
})?;
11191+
1115411192
let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts();
11193+
11194+
let action = QuiescentAction::Splice(SpliceInstructions {
11195+
adjusted_funding_contribution,
11196+
our_funding_inputs,
11197+
our_funding_outputs,
11198+
change_script,
11199+
funding_feerate_per_kw,
11200+
locktime,
11201+
original_funding_txo,
11202+
});
11203+
self.propose_quiescence(logger, action)
11204+
.map_err(|e| APIError::APIMisuseError { err: e.to_owned() })
11205+
}
11206+
11207+
#[cfg(splicing)]
11208+
fn send_splice_init(
11209+
&mut self, instructions: SpliceInstructions,
11210+
) -> Result<msgs::SpliceInit, String> {
11211+
let SpliceInstructions {
11212+
adjusted_funding_contribution,
11213+
our_funding_inputs,
11214+
our_funding_outputs,
11215+
change_script,
11216+
funding_feerate_per_kw,
11217+
locktime,
11218+
original_funding_txo,
11219+
} = instructions;
11220+
11221+
// Check if a splice has been initiated already.
11222+
// Note: only a single outstanding splice is supported (per spec)
11223+
if self.pending_splice.is_some() {
11224+
return Err(format!(
11225+
"Channel {} cannot be spliced, as it has already a splice pending",
11226+
self.context.channel_id(),
11227+
));
11228+
}
11229+
11230+
let prev_funding_input = self.funding.to_splice_funding_input();
1115511231
let funding_negotiation_context = FundingNegotiationContext {
1115611232
is_initiator: true,
1115711233
our_funding_contribution: adjusted_funding_contribution,
@@ -11307,6 +11383,10 @@ where
1130711383
ES::Target: EntropySource,
1130811384
L::Target: Logger,
1130911385
{
11386+
if !self.context.channel_state.is_quiescent() {
11387+
return Err(ChannelError::WarnAndDisconnect("Quiescence needed to splice".to_owned()));
11388+
}
11389+
1131011390
let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_satoshis);
1131111391
let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?;
1131211392

@@ -11346,6 +11426,11 @@ where
1134611426
})?;
1134711427
debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none());
1134811428

11429+
// TODO(splicing): if quiescent_action is set, integrate what the user wants to do into the
11430+
// counterparty-initiated splice. For always-on nodes this probably isn't a useful
11431+
// optimization, but for often-offline nodes it may be, as we may connect and immediately
11432+
// go into splicing from both sides.
11433+
1134911434
let funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey;
1135011435

1135111436
self.pending_splice = Some(PendingSplice {
@@ -12094,23 +12179,21 @@ where
1209412179
);
1209512180
}
1209612181

12097-
#[cfg(any(test, fuzzing))]
12182+
#[cfg(any(splicing, test, fuzzing))]
1209812183
#[rustfmt::skip]
1209912184
pub fn propose_quiescence<L: Deref>(
1210012185
&mut self, logger: &L, action: QuiescentAction,
12101-
) -> Result<Option<msgs::Stfu>, ChannelError>
12186+
) -> Result<Option<msgs::Stfu>, &'static str>
1210212187
where
1210312188
L::Target: Logger,
1210412189
{
1210512190
log_debug!(logger, "Attempting to initiate quiescence");
1210612191

1210712192
if !self.context.is_usable() {
12108-
return Err(ChannelError::Ignore(
12109-
"Channel is not in a usable state to propose quiescence".to_owned()
12110-
));
12193+
return Err("Channel is not in a usable state to propose quiescence");
1211112194
}
1211212195
if self.quiescent_action.is_some() {
12113-
return Err(ChannelError::Ignore("Channel is already quiescing".to_owned()));
12196+
return Err("Channel already has a pending quiescent action and cannot start another");
1211412197
}
1211512198

1211612199
self.quiescent_action = Some(action);
@@ -12131,7 +12214,7 @@ where
1213112214

1213212215
// Assumes we are either awaiting quiescence or our counterparty has requested quiescence.
1213312216
#[rustfmt::skip]
12134-
pub fn send_stfu<L: Deref>(&mut self, logger: &L) -> Result<msgs::Stfu, ChannelError>
12217+
pub fn send_stfu<L: Deref>(&mut self, logger: &L) -> Result<msgs::Stfu, &'static str>
1213512218
where
1213612219
L::Target: Logger,
1213712220
{
@@ -12145,9 +12228,7 @@ where
1214512228
if self.context.is_waiting_on_peer_pending_channel_update()
1214612229
|| self.context.is_monitor_or_signer_pending_channel_update()
1214712230
{
12148-
return Err(ChannelError::Ignore(
12149-
"We cannot send `stfu` while state machine is pending".to_owned()
12150-
));
12231+
return Err("We cannot send `stfu` while state machine is pending")
1215112232
}
1215212233

1215312234
let initiator = if self.context.channel_state.is_remote_stfu_sent() {
@@ -12173,7 +12254,7 @@ where
1217312254
#[rustfmt::skip]
1217412255
pub fn stfu<L: Deref>(
1217512256
&mut self, msg: &msgs::Stfu, logger: &L
12176-
) -> Result<Option<msgs::Stfu>, ChannelError> where L::Target: Logger {
12257+
) -> Result<Option<StfuResponse>, ChannelError> where L::Target: Logger {
1217712258
if self.context.channel_state.is_quiescent() {
1217812259
return Err(ChannelError::Warn("Channel is already quiescent".to_owned()));
1217912260
}
@@ -12204,7 +12285,10 @@ where
1220412285
self.context.channel_state.set_remote_stfu_sent();
1220512286

1220612287
log_debug!(logger, "Received counterparty stfu proposing quiescence");
12207-
return self.send_stfu(logger).map(|stfu| Some(stfu));
12288+
return self
12289+
.send_stfu(logger)
12290+
.map(|stfu| Some(StfuResponse::Stfu(stfu)))
12291+
.map_err(|e| ChannelError::Ignore(e.to_owned()));
1220812292
}
1220912293

1221012294
// We already sent `stfu` and are now processing theirs. It may be in response to ours, or
@@ -12245,6 +12329,13 @@ where
1224512329
"Internal Error: Didn't have anything to do after reaching quiescence".to_owned()
1224612330
));
1224712331
},
12332+
Some(QuiescentAction::Splice(_instructions)) => {
12333+
#[cfg(splicing)]
12334+
return self.send_splice_init(_instructions)
12335+
.map(|splice_init| Some(StfuResponse::SpliceInit(splice_init)))
12336+
.map_err(|e| ChannelError::WarnAndDisconnect(e.to_owned()));
12337+
},
12338+
#[cfg(any(test, fuzzing))]
1224812339
Some(QuiescentAction::DoNothing) => {
1224912340
// In quiescence test we want to just hang out here, letting the test manually
1225012341
// leave quiescence.
@@ -12277,7 +12368,10 @@ where
1227712368
|| (self.context.channel_state.is_remote_stfu_sent()
1227812369
&& !self.context.channel_state.is_local_stfu_sent())
1227912370
{
12280-
return self.send_stfu(logger).map(|stfu| Some(stfu));
12371+
return self
12372+
.send_stfu(logger)
12373+
.map(|stfu| Some(stfu))
12374+
.map_err(|e| ChannelError::Ignore(e.to_owned()));
1228112375
}
1228212376

1228312377
// We're either:
@@ -16478,8 +16572,8 @@ mod tests {
1647816572
2000,
1647916573
);
1648016574
assert_eq!(
16481-
format!("{:?}", res.err().unwrap()),
16482-
"Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1746. Need more inputs.",
16575+
res.err().unwrap(),
16576+
"Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1746. Need more inputs.",
1648316577
);
1648416578
}
1648516579

@@ -16514,8 +16608,8 @@ mod tests {
1651416608
2200,
1651516609
);
1651616610
assert_eq!(
16517-
format!("{:?}", res.err().unwrap()),
16518-
"Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2522. Need more inputs.",
16611+
res.err().unwrap(),
16612+
"Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2522. Need more inputs.",
1651916613
);
1652016614
}
1652116615

0 commit comments

Comments
 (0)