From 2a6501a7bd420de419a67159d03af72b55fcaf0c Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 26 Aug 2025 15:12:28 +0000 Subject: [PATCH 1/5] Correct error types for outbound splice checking methods When doing an outbound splice, we check some of the instructions first in utility methods, then convert errors to `APIError`s. These utility methods should thus either return an `APIError` or more generic (string or `()`) error type, but they currently return a `ChannelError`, which is only approprite when the calling code will do what the `ChannelError` instructs (including closing the channel). Here we fix that by returning `String`s instead. --- lightning/src/ln/channel.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e9614bcb35a..6cff41b5721 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -5982,7 +5982,7 @@ fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satos fn check_splice_contribution_sufficient( channel_balance: Amount, contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate, -) -> Result { +) -> Result { let contribution_amount = contribution.value(); if contribution_amount < SignedAmount::ZERO { let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( @@ -5996,10 +5996,10 @@ fn check_splice_contribution_sufficient( if channel_balance >= contribution_amount.unsigned_abs() + estimated_fee { Ok(estimated_fee) } else { - Err(ChannelError::Warn(format!( - "Available channel balance {} is lower than needed for splicing out {}, considering fees of {}", - channel_balance, contribution_amount.unsigned_abs(), estimated_fee, - ))) + Err(format!( + "Available channel balance {channel_balance} is lower than needed for splicing out {}, considering fees of {estimated_fee}", + contribution_amount.unsigned_abs(), + )) } } else { check_v2_funding_inputs_sufficient( @@ -6066,7 +6066,7 @@ fn estimate_v2_funding_transaction_fee( fn check_v2_funding_inputs_sufficient( contribution_amount: i64, funding_inputs: &[FundingTxInput], is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32, -) -> Result { +) -> Result { let estimated_fee = estimate_v2_funding_transaction_fee( funding_inputs, &[], is_initiator, is_splice, funding_feerate_sat_per_1000_weight, ); @@ -6089,10 +6089,9 @@ fn check_v2_funding_inputs_sufficient( let minimal_input_amount_needed = contribution_amount.saturating_add(estimated_fee as i64); if (total_input_sats as i64) < minimal_input_amount_needed { - Err(ChannelError::Warn(format!( - "Total input amount {} is lower than needed for contribution {}, considering fees of {}. Need more inputs.", - total_input_sats, contribution_amount, estimated_fee, - ))) + Err(format!( + "Total input amount {total_input_sats} is lower than needed for contribution {contribution_amount}, considering fees of {estimated_fee}. Need more inputs.", + )) } else { Ok(estimated_fee) } @@ -16205,8 +16204,8 @@ mod tests { 2000, ); assert_eq!( - format!("{:?}", res.err().unwrap()), - "Warn: Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1746. Need more inputs.", + res.err().unwrap(), + "Total input amount 100000 is lower than needed for contribution 220000, considering fees of 1746. Need more inputs.", ); } @@ -16241,8 +16240,8 @@ mod tests { 2200, ); assert_eq!( - format!("{:?}", res.err().unwrap()), - "Warn: Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2522. Need more inputs.", + res.err().unwrap(), + "Total input amount 300000 is lower than needed for contribution 298032, considering fees of 2522. Need more inputs.", ); } From 37edb4c655a6aced3bd12b0471c673f5ff20dfe2 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 26 Aug 2025 16:04:59 +0000 Subject: [PATCH 2/5] Push splice initiation through the quiescent pipeline Now that we have a `QuiescentAction` to track what we intend to do once we reach quiescence, we need to use it to initiate splices. Here we do so, adding a new `SpliceInstructions` to track the arguments that are currently passed to `splice_channel`. While these may not be exactly the right arguments to track in the end, a lot of the splice logic is still in flight, so we can worry about it later. --- lightning/src/events/bump_transaction/mod.rs | 6 + lightning/src/ln/channel.rs | 125 ++++++++++++++++--- lightning/src/ln/channelmanager.rs | 50 +++++--- lightning/src/ln/funding.rs | 6 + lightning/src/ln/splicing_tests.rs | 16 ++- lightning/src/util/ser.rs | 18 ++- 6 files changed, 174 insertions(+), 47 deletions(-) diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 6f127691f21..fb872d6fcd7 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -264,6 +264,12 @@ pub struct Utxo { pub satisfaction_weight: u64, } +impl_writeable_tlv_based!(Utxo, { + (1, outpoint, required), + (3, output, required), + (5, satisfaction_weight, required), +}); + impl Utxo { /// Returns a `Utxo` with the `satisfaction_weight` estimate for a legacy P2PKH output. pub fn new_p2pkh(outpoint: OutPoint, value: Amount, pubkey_hash: &PubkeyHash) -> Self { diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 6cff41b5721..161325df1a6 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2448,13 +2448,46 @@ impl PendingSplice { } } +pub(crate) struct SpliceInstructions { + adjusted_funding_contribution: SignedAmount, + our_funding_inputs: Vec, + our_funding_outputs: Vec, + change_script: Option, + funding_feerate_per_kw: u32, + locktime: u32, + original_funding_txo: OutPoint, +} + +impl_writeable_tlv_based!(SpliceInstructions, { + (1, adjusted_funding_contribution, required), + (3, our_funding_inputs, required_vec), + (5, our_funding_outputs, required_vec), + (7, change_script, option), + (9, funding_feerate_per_kw, required), + (11, locktime, required), + (13, original_funding_txo, required), +}); + pub(crate) enum QuiescentAction { - // TODO: Make this test-only once we have another variant (as some code requires *a* variant). + Splice(SpliceInstructions), + #[cfg(any(test, fuzzing))] DoNothing, } +pub(crate) enum StfuResponse { + Stfu(msgs::Stfu), + #[cfg_attr(not(splicing), allow(unused))] + SpliceInit(msgs::SpliceInit), +} + +#[cfg(any(test, fuzzing))] impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, - (99, DoNothing) => {}, + (0, DoNothing) => {}, + {1, Splice} => (), +); +#[cfg(not(any(test, fuzzing)))] +impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,, + {1, Splice} => (), ); /// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`]. @@ -10748,9 +10781,13 @@ where /// - `change_script`: an option change output script. If `None` and needed, one will be /// generated by `SignerProvider::get_destination_script`. #[cfg(splicing)] - pub fn splice_channel( + pub fn splice_channel( &mut self, contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: u32, - ) -> Result { + logger: &L, + ) -> Result, APIError> + where + L::Target: Logger, + { if self.holder_commitment_point.current_point().is_none() { return Err(APIError::APIMisuseError { err: format!( @@ -10780,8 +10817,6 @@ where }); } - // TODO(splicing): check for quiescence - let our_funding_contribution = contribution.value(); if our_funding_contribution == SignedAmount::ZERO { return Err(APIError::APIMisuseError { @@ -10876,8 +10911,49 @@ where } } - let prev_funding_input = self.funding.to_splice_funding_input(); + let original_funding_txo = self.funding.get_funding_txo().ok_or_else(|| { + debug_assert!(false); + APIError::APIMisuseError { + err: "Chanel isn't yet fully funded".to_owned(), + } + })?; + let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts(); + + let action = QuiescentAction::Splice(SpliceInstructions { + adjusted_funding_contribution, + our_funding_inputs, + our_funding_outputs, + change_script, + funding_feerate_per_kw, + locktime, + original_funding_txo, + }); + self.propose_quiescence(logger, action) + .map_err(|e| APIError::APIMisuseError { err: e.to_owned() }) + } + + #[cfg(splicing)] + fn send_splice_init( + &mut self, instructions: SpliceInstructions, + ) -> Result { + let SpliceInstructions { + adjusted_funding_contribution, + our_funding_inputs, + our_funding_outputs, + change_script, + funding_feerate_per_kw, + locktime, + original_funding_txo, + } = instructions; + + if self.funding.get_funding_txo() != Some(original_funding_txo) { + // This should be unreachable once we opportunistically merge splices if the + // counterparty initializes a splice. + return Err("Funding changed out from under us".to_owned()); + } + + let prev_funding_input = self.funding.to_splice_funding_input(); let funding_negotiation_context = FundingNegotiationContext { is_initiator: true, our_funding_contribution: adjusted_funding_contribution, @@ -11820,23 +11896,21 @@ where ); } - #[cfg(any(test, fuzzing))] + #[cfg(any(splicing, test, fuzzing))] #[rustfmt::skip] pub fn propose_quiescence( &mut self, logger: &L, action: QuiescentAction, - ) -> Result, ChannelError> + ) -> Result, &'static str> where L::Target: Logger, { log_debug!(logger, "Attempting to initiate quiescence"); if !self.context.is_usable() { - return Err(ChannelError::Ignore( - "Channel is not in a usable state to propose quiescence".to_owned() - )); + return Err("Channel is not in a usable state to propose quiescence"); } if self.quiescent_action.is_some() { - return Err(ChannelError::Ignore("Channel is already quiescing".to_owned())); + return Err("Channel is already quiescing"); } self.quiescent_action = Some(action); @@ -11857,7 +11931,7 @@ where // Assumes we are either awaiting quiescence or our counterparty has requested quiescence. #[rustfmt::skip] - pub fn send_stfu(&mut self, logger: &L) -> Result + pub fn send_stfu(&mut self, logger: &L) -> Result where L::Target: Logger, { @@ -11871,9 +11945,7 @@ where if self.context.is_waiting_on_peer_pending_channel_update() || self.context.is_monitor_or_signer_pending_channel_update() { - return Err(ChannelError::Ignore( - "We cannot send `stfu` while state machine is pending".to_owned() - )); + return Err("We cannot send `stfu` while state machine is pending") } let initiator = if self.context.channel_state.is_remote_stfu_sent() { @@ -11899,7 +11971,7 @@ where #[rustfmt::skip] pub fn stfu( &mut self, msg: &msgs::Stfu, logger: &L - ) -> Result, ChannelError> where L::Target: Logger { + ) -> Result, ChannelError> where L::Target: Logger { if self.context.channel_state.is_quiescent() { return Err(ChannelError::Warn("Channel is already quiescent".to_owned())); } @@ -11930,7 +12002,10 @@ where self.context.channel_state.set_remote_stfu_sent(); log_debug!(logger, "Received counterparty stfu proposing quiescence"); - return self.send_stfu(logger).map(|stfu| Some(stfu)); + return self + .send_stfu(logger) + .map(|stfu| Some(StfuResponse::Stfu(stfu))) + .map_err(|e| ChannelError::Ignore(e.to_owned())); } // We already sent `stfu` and are now processing theirs. It may be in response to ours, or @@ -11971,6 +12046,13 @@ where "Internal Error: Didn't have anything to do after reaching quiescence".to_owned() )); }, + Some(QuiescentAction::Splice(_instructions)) => { + #[cfg(splicing)] + return self.send_splice_init(_instructions) + .map(|splice_init| Some(StfuResponse::SpliceInit(splice_init))) + .map_err(|e| ChannelError::Ignore(e.to_owned())); + }, + #[cfg(any(test, fuzzing))] Some(QuiescentAction::DoNothing) => { // In quiescence test we want to just hang out here, letting the test manually // leave quiescence. @@ -12003,7 +12085,10 @@ where || (self.context.channel_state.is_remote_stfu_sent() && !self.context.channel_state.is_local_stfu_sent()) { - return self.send_stfu(logger).map(|stfu| Some(stfu)); + return self + .send_stfu(logger) + .map(|stfu| Some(stfu)) + .map_err(|e| ChannelError::Ignore(e.to_owned())); } // We're either: diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index c953e39d6d6..81c507b7d25 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -61,7 +61,7 @@ use crate::ln::channel::QuiescentAction; use crate::ln::channel::{ self, hold_time_since, Channel, ChannelError, ChannelUpdateStatus, FundedChannel, InboundV1Channel, OutboundV1Channel, PendingV2Channel, ReconnectionMsg, ShutdownResult, - UpdateFulfillCommitFetch, WithChannelContext, + StfuResponse, UpdateFulfillCommitFetch, WithChannelContext, }; use crate::ln::channel_state::ChannelDetails; #[cfg(splicing)] @@ -4494,12 +4494,15 @@ where hash_map::Entry::Occupied(mut chan_phase_entry) => { let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { - let msg = - chan.splice_channel(contribution, funding_feerate_per_kw, locktime)?; - peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceInit { - node_id: *counterparty_node_id, - msg, - }); + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let msg_opt = chan + .splice_channel(contribution, funding_feerate_per_kw, locktime, &&logger)?; + if let Some(msg) = msg_opt { + peer_state.pending_msg_events.push(MessageSendEvent::SendStfu { + node_id: *counterparty_node_id, + msg, + }); + } Ok(()) } else { Err(APIError::ChannelUnavailable { @@ -10875,7 +10878,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ )); } - let mut sent_stfu = false; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { @@ -10883,14 +10885,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &self.logger, Some(*counterparty_node_id), Some(msg.channel_id), None ); - if let Some(stfu) = try_channel_entry!( - self, peer_state, chan.stfu(&msg, &&logger), chan_entry - ) { - sent_stfu = true; - peer_state.pending_msg_events.push(MessageSendEvent::SendStfu { - node_id: *counterparty_node_id, - msg: stfu, - }); + let res = chan.stfu(&msg, &&logger); + let resp = try_channel_entry!(self, peer_state, res, chan_entry); + match resp { + None => Ok(false), + Some(StfuResponse::Stfu(msg)) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendStfu { + node_id: *counterparty_node_id, + msg, + }); + Ok(true) + }, + Some(StfuResponse::SpliceInit(msg)) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceInit { + node_id: *counterparty_node_id, + msg, + }); + Ok(true) + }, } } else { let msg = "Peer sent `stfu` for an unfunded channel"; @@ -10905,8 +10917,6 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ msg.channel_id )) } - - Ok(sent_stfu) } #[rustfmt::skip] @@ -13873,8 +13883,8 @@ where let persist = match &res { Err(e) if e.closes_channel() => NotifyOption::DoPersist, Err(_) => NotifyOption::SkipPersistHandleEvents, - Ok(sent_stfu) => { - if *sent_stfu { + Ok(responded) => { + if *responded { NotifyOption::SkipPersistHandleEvents } else { NotifyOption::SkipPersistNoEvents diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index e42c338d865..a87a3cb904e 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -104,6 +104,12 @@ pub struct FundingTxInput { pub(super) prevtx: Transaction, } +impl_writeable_tlv_based!(FundingTxInput, { + (1, utxo, required), + (3, sequence, required), + (5, prevtx, required), +}); + impl FundingTxInput { fn new bool>( prevtx: Transaction, vout: u32, witness_weight: Weight, script_filter: F, diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index 2445a2d5fc2..b60903d4c71 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -28,6 +28,8 @@ fn test_v1_splice_in() { let acceptor_node_index = 1; let initiator_node = &nodes[initiator_node_index]; let acceptor_node = &nodes[acceptor_node_index]; + let initiator_node_id = initiator_node.node.get_our_node_id(); + let acceptor_node_id = acceptor_node.node.get_our_node_id(); let channel_value_sat = 100_000; let channel_reserve_amnt_sat = 1_000; @@ -87,12 +89,16 @@ fn test_v1_splice_in() { None, // locktime ) .unwrap(); + + let init_stfu = get_event_msg!(initiator_node, MessageSendEvent::SendStfu, acceptor_node_id); + acceptor_node.node.handle_stfu(initiator_node_id, &init_stfu); + + let ack_stfu = get_event_msg!(acceptor_node, MessageSendEvent::SendStfu, initiator_node_id); + initiator_node.node.handle_stfu(acceptor_node_id, &ack_stfu); + // Extract the splice_init message - let splice_init_msg = get_event_msg!( - initiator_node, - MessageSendEvent::SendSpliceInit, - acceptor_node.node.get_our_node_id() - ); + let splice_init_msg = + get_event_msg!(initiator_node, MessageSendEvent::SendSpliceInit, acceptor_node_id); assert_eq!(splice_init_msg.funding_contribution_satoshis, splice_in_sats as i64); assert_eq!(splice_init_msg.funding_feerate_per_kw, funding_feerate_per_kw); assert_eq!(splice_init_msg.funding_pubkey.to_string(), expected_initiator_funding_key); diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index b2521d93109..15fd7fe140d 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -26,7 +26,7 @@ use core::ops::Deref; use alloc::collections::BTreeMap; use bitcoin::absolute::LockTime as AbsoluteLockTime; -use bitcoin::amount::Amount; +use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::Encodable; use bitcoin::constants::ChainHash; use bitcoin::hash_types::{BlockHash, Txid}; @@ -41,7 +41,7 @@ use bitcoin::secp256k1::ecdsa; use bitcoin::secp256k1::schnorr; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{OutPoint, Transaction, TxOut}; -use bitcoin::{consensus, TxIn, Weight, Witness}; +use bitcoin::{consensus, Sequence, TxIn, Weight, Witness}; use dnssec_prover::rr::Name; @@ -1383,6 +1383,19 @@ impl Readable for Amount { } } +impl Writeable for SignedAmount { + fn write(&self, w: &mut W) -> Result<(), io::Error> { + self.to_sat().write(w) + } +} + +impl Readable for SignedAmount { + fn read(r: &mut R) -> Result { + let amount: i64 = Readable::read(r)?; + Ok(SignedAmount::from_sat(amount)) + } +} + impl Writeable for Weight { fn write(&self, w: &mut W) -> Result<(), io::Error> { self.to_wu().write(w) @@ -1487,6 +1500,7 @@ impl_consensus_ser!(Transaction); impl_consensus_ser!(TxIn); impl_consensus_ser!(TxOut); impl_consensus_ser!(Witness); +impl_consensus_ser!(Sequence); impl Readable for Mutex { fn read(r: &mut R) -> Result { From b1077415226312e7b52cf9e8c79507e1d3524d5b Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 12 Aug 2025 22:50:34 +0000 Subject: [PATCH 3/5] Add quiescence test of disconnecting waiting on the next step While we have a test to disconnect a peer if we're waiting on an `stfu` message, we also disconnect if we've reached quiescence but we're waiting on a peer to do "something fundamental" and they take too long to do so. We test that behavior here. --- lightning/src/ln/quiescence_tests.rs | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lightning/src/ln/quiescence_tests.rs b/lightning/src/ln/quiescence_tests.rs index c13f9e72645..737a2e02569 100644 --- a/lightning/src/ln/quiescence_tests.rs +++ b/lightning/src/ln/quiescence_tests.rs @@ -549,6 +549,48 @@ fn test_quiescence_timeout_while_waiting_for_counterparty_stfu() { assert!(nodes[1].node.get_and_clear_pending_msg_events().iter().find_map(f).is_some()); } +#[test] +fn test_quiescence_timeout_while_waiting_for_counterparty_something_fundamental() { + // Test that we'll disconnect if the counterparty does not send their "something fundamental" + // within a reasonable time if we've reached quiescence. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + nodes[1].node.maybe_propose_quiescence(&node_id_0, &chan_id).unwrap(); + let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + nodes[0].node.handle_stfu(node_id_1, &stfu); + let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS { + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + } + + // nodes[1] didn't receive nodes[0]'s stfu within the timeout so it'll disconnect. + let f = |event| { + if let MessageSendEvent::HandleError { action, .. } = event { + if let msgs::ErrorAction::DisconnectPeerWithWarning { .. } = action { + Some(()) + } else { + None + } + } else { + None + } + }; + // At this point, node A is waiting on B to do something fundamental, and node B is waiting on + // A's stfu that we never delivered. Thus both should disconnect each other. + assert!(nodes[0].node.get_and_clear_pending_msg_events().into_iter().find_map(&f).is_some()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().into_iter().find_map(&f).is_some()); +} + fn do_test_quiescence_during_disconnection(with_pending_claim: bool, propose_disconnected: bool) { // Test that we'll start trying for quiescence immediately after reconnection if we're waiting // to do some quiescence-required action. From 5920de80fa706032ad0f29781f121952237c0dbf Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 12 Aug 2025 23:01:48 +0000 Subject: [PATCH 4/5] Reject `splice_init`s when we aren't quiescent --- lightning/src/ln/channel.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 161325df1a6..b9917c8fef2 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11109,6 +11109,10 @@ where ES::Target: EntropySource, L::Target: Logger, { + if !self.context.channel_state.is_quiescent() { + return Err(ChannelError::WarnAndDisconnect("Quiescence needed to splice".to_owned())); + } + let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_satoshis); let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?; From d9a278c819b83ba549b6de478cb9f9e8e13e173b Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Tue, 12 Aug 2025 23:08:35 +0000 Subject: [PATCH 5/5] Add a quick TODO about merging cp- and locally-initiated splices --- lightning/src/ln/channel.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index b9917c8fef2..811f6d42f56 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11152,6 +11152,11 @@ where })?; debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none()); + // TODO(splicing): if post_quiescence_action is set, integrate what the user wants to do + // into the counterparty-initiated splice. For always-on nodes this probably isn't a useful + // optimization, but for often-offline nodes it may be, as we may connect and immediately + // go into splicing from both sides. + let funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey; self.pending_splice = Some(PendingSplice {