From c2b15c212ad164cfa4aab148efa52146fa63df7c Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 10 Nov 2025 09:58:07 -0800 Subject: [PATCH 1/4] Avoid returning user error when stfu is not ready to be sent yet When we propose quiescence due to a user initiated action, such as a splice, it's possible that the `stfu` message is not ready to be sent out yet due to the state machine pending a change. This would result in an error communicated back to the caller of `ChannelManager::splice_channel`, but this is unnecessary and confusing, the splice is still pending and will be carried out once quiescence is eventually negotiated. --- lightning/src/ln/channel.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index c5942f38da6..d7acb1ab98d 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -13099,7 +13099,13 @@ where self.context.channel_state.set_awaiting_quiescence(); if self.context.is_live() { - Ok(Some(self.send_stfu(logger)?)) + match self.send_stfu(logger) { + Ok(stfu) => Ok(Some(stfu)), + Err(e) => { + log_debug!(logger, "{e}"); + Ok(None) + }, + } } else { log_debug!(logger, "Waiting for peer reconnection to send stfu"); Ok(None) From a6ed667c6936ff7f188515e1633a90a5609b14b7 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 10 Nov 2025 10:01:00 -0800 Subject: [PATCH 2/4] Remove invalid splice debug assertion in funding_tx_constructed We incorrectly assumed that both commitments must be at the same height, but this is certainly possible whenever updates are proposed in both directions at the same time. --- lightning/src/ln/channel.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index d7acb1ab98d..45a1d984dcb 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -6199,12 +6199,7 @@ where { funding.channel_transaction_parameters.funding_outpoint = Some(funding_outpoint); - if is_splice { - debug_assert_eq!( - holder_commitment_transaction_number, - self.counterparty_next_commitment_transaction_number, - ); - } else { + if !is_splice { self.assert_no_commitment_advancement(holder_commitment_transaction_number, "initial commitment_signed"); self.channel_state = ChannelState::FundingNegotiated(FundingNegotiatedFlags::new()); } From 25264d758d11b3f3b6af881fa1eff95c96131cca Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 10 Nov 2025 10:01:32 -0800 Subject: [PATCH 3/4] Break up ChannelContext::funding_tx_constructed This method was originally intended to DRY the logic between `FundedChannel` and `PendingChannelV2` when handling `funding_tx_constructed`, though it's not very useful anymore. --- lightning/src/ln/channel.rs | 75 +++++++++++++++---------------------- 1 file changed, 31 insertions(+), 44 deletions(-) diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 45a1d984dcb..e7426a0876a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -2028,18 +2028,32 @@ where | NegotiatingFundingFlags::THEIR_INIT_SENT ), ); + chan.context.assert_no_commitment_advancement( + chan.unfunded_context.transaction_number(), + "initial commitment_signed", + ); + + chan.context.channel_state = + ChannelState::FundingNegotiated(FundingNegotiatedFlags::new()); + chan.funding.channel_transaction_parameters.funding_outpoint = + Some(funding_outpoint); let interactive_tx_constructor = chan .interactive_tx_constructor .take() .expect("PendingV2Channel::interactive_tx_constructor should be set"); - let commitment_signed = chan.context.funding_tx_constructed( - &mut chan.funding, - funding_outpoint, - false, - chan.unfunded_context.transaction_number(), - &&logger, - )?; + + let commitment_signed = + chan.context.get_initial_commitment_signed_v2(&chan.funding, &&logger); + let commitment_signed = match commitment_signed { + Some(commitment_signed) => commitment_signed, + // TODO(dual_funding): Support async signing + None => { + return Err(AbortReason::InternalError( + "Failed to compute commitment_signed signatures", + )); + }, + }; (interactive_tx_constructor, commitment_signed) }, @@ -2068,14 +2082,11 @@ where ) }) .and_then(|(is_initiator, mut funding, interactive_tx_constructor)| { - match chan.context.funding_tx_constructed( - &mut funding, - funding_outpoint, - true, - chan.holder_commitment_point.next_transaction_number(), - &&logger, - ) { - Ok(commitment_signed) => { + funding.channel_transaction_parameters.funding_outpoint = + Some(funding_outpoint); + match chan.context.get_initial_commitment_signed_v2(&funding, &&logger) + { + Some(commitment_signed) => { // Advance the state pending_splice.funding_negotiation = Some(FundingNegotiation::AwaitingSignatures { @@ -2084,14 +2095,17 @@ where }); Ok((interactive_tx_constructor, commitment_signed)) }, - Err(e) => { + // TODO(splicing): Support async signing + None => { // Restore the taken state for later error handling pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction { funding, interactive_tx_constructor, }); - Err(e) + Err(AbortReason::InternalError( + "Failed to compute commitment_signed signatures", + )) }, } })? @@ -6189,33 +6203,6 @@ where Ok(()) } - #[rustfmt::skip] - fn funding_tx_constructed( - &mut self, funding: &mut FundingScope, funding_outpoint: OutPoint, is_splice: bool, - holder_commitment_transaction_number: u64, logger: &L, - ) -> Result - where - L::Target: Logger - { - funding.channel_transaction_parameters.funding_outpoint = Some(funding_outpoint); - - if !is_splice { - self.assert_no_commitment_advancement(holder_commitment_transaction_number, "initial commitment_signed"); - self.channel_state = ChannelState::FundingNegotiated(FundingNegotiatedFlags::new()); - } - - let commitment_signed = self.get_initial_commitment_signed_v2(&funding, logger); - let commitment_signed = match commitment_signed { - Some(commitment_signed) => commitment_signed, - // TODO(splicing): Support async signing - None => { - return Err(AbortReason::InternalError("Failed to compute commitment_signed signatures")); - }, - }; - - Ok(commitment_signed) - } - /// Asserts that the commitment tx numbers have not advanced from their initial number. fn assert_no_commitment_advancement( &self, holder_commitment_transaction_number: u64, msg_name: &str, From 1e7862829bd9349f7a442b5a6f836ba898f7e87f Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Mon, 10 Nov 2025 09:57:08 -0800 Subject: [PATCH 4/4] Replace quiescence fuzz coverage with splicing Splicing should also have fuzz coverage, and it depends on the quiescence protocol. --- fuzz/src/chanmon_consistency.rs | 432 ++++++++++++++++++++++++++++++-- 1 file changed, 416 insertions(+), 16 deletions(-) diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 9f03de47d23..943e1f2d63a 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -43,13 +43,16 @@ use lightning::chain::{ chainmonitor, channelmonitor, BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch, }; use lightning::events; -use lightning::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; +use lightning::ln::channel::{ + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS, +}; use lightning::ln::channel_state::ChannelDetails; use lightning::ln::channelmanager::{ ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RecentPaymentDetails, RecipientOnionFields, }; use lightning::ln::functional_test_utils::*; +use lightning::ln::funding::{FundingTxInput, SpliceContribution}; use lightning::ln::inbound_payment::ExpandedKey; use lightning::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, Init, MessageSendEvent, @@ -68,6 +71,7 @@ use lightning::sign::{ }; use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::util::config::UserConfig; +use lightning::util::errors::APIError; use lightning::util::hash_tables::*; use lightning::util::logger::Logger; use lightning::util::ser::{LengthReadable, ReadableArgs, Writeable, Writer}; @@ -163,6 +167,63 @@ impl Writer for VecWriter { } } +pub struct TestWallet { + secret_key: SecretKey, + utxos: Mutex>, + secp: Secp256k1, +} + +impl TestWallet { + pub fn new(secret_key: SecretKey) -> Self { + Self { secret_key, utxos: Mutex::new(Vec::new()), secp: Secp256k1::new() } + } + + fn get_change_script(&self) -> Result { + let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); + Ok(ScriptBuf::new_p2wpkh(&public_key.wpubkey_hash().unwrap())) + } + + pub fn add_utxo(&self, outpoint: bitcoin::OutPoint, value: Amount) -> TxOut { + let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); + let utxo = lightning::events::bump_transaction::Utxo::new_v0_p2wpkh( + outpoint, + value, + &public_key.wpubkey_hash().unwrap(), + ); + self.utxos.lock().unwrap().push(utxo.clone()); + utxo.output + } + + pub fn sign_tx( + &self, mut tx: Transaction, + ) -> Result { + let utxos = self.utxos.lock().unwrap(); + for i in 0..tx.input.len() { + if let Some(utxo) = + utxos.iter().find(|utxo| utxo.outpoint == tx.input[i].previous_output) + { + let sighash = bitcoin::sighash::SighashCache::new(&tx).p2wpkh_signature_hash( + i, + &utxo.output.script_pubkey, + utxo.output.value, + bitcoin::EcdsaSighashType::All, + )?; + let signature = self.secp.sign_ecdsa( + &secp256k1::Message::from_digest(sighash.to_byte_array()), + &self.secret_key, + ); + let bitcoin_sig = bitcoin::ecdsa::Signature { + signature, + sighash_type: bitcoin::EcdsaSighashType::All, + }; + tx.input[i].witness = + bitcoin::Witness::p2wpkh(&bitcoin_sig, &self.secret_key.public_key(&self.secp)); + } + } + Ok(tx) + } +} + /// The LDK API requires that any time we tell it we're done persisting a `ChannelMonitor[Update]` /// we never pass it in as the "latest" `ChannelMonitor` on startup. However, we can pass /// out-of-date monitors as long as we never told LDK we finished persisting them, which we do by @@ -671,6 +732,7 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { let mut config = UserConfig::default(); config.channel_config.forwarding_fee_proportional_millionths = 0; config.channel_handshake_config.announce_for_forwarding = true; + config.reject_inbound_splices = false; if anchors { config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; config.manually_accept_inbound_channels = true; @@ -724,6 +786,7 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { let mut config = UserConfig::default(); config.channel_config.forwarding_fee_proportional_millionths = 0; config.channel_handshake_config.announce_for_forwarding = true; + config.reject_inbound_splices = false; if anchors { config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; config.manually_accept_inbound_channels = true; @@ -984,6 +1047,30 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }}; } + let wallet_a = TestWallet::new(SecretKey::from_slice(&[1; 32]).unwrap()); + let wallet_b = TestWallet::new(SecretKey::from_slice(&[2; 32]).unwrap()); + let wallet_c = TestWallet::new(SecretKey::from_slice(&[3; 32]).unwrap()); + let wallets = vec![wallet_a, wallet_b, wallet_c]; + let coinbase_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn { ..Default::default() }], + output: wallets + .iter() + .map(|w| TxOut { + value: Amount::from_sat(100_000), + script_pubkey: w.get_change_script().unwrap(), + }) + .collect(), + }; + let coinbase_txid = coinbase_tx.compute_txid(); + wallets.iter().enumerate().for_each(|(i, w)| { + w.add_utxo( + bitcoin::OutPoint { txid: coinbase_txid, vout: i as u32 }, + Amount::from_sat(100_000), + ); + }); + let fee_est_a = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); let mut last_htlc_clear_fee_a = 253; let fee_est_b = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); @@ -1073,6 +1160,34 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } *node_id == a_id }, + MessageSendEvent::SendSpliceInit { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendSpliceAck { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendSpliceLocked { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendTxAddInput { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendTxAddOutput { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendTxComplete { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, + MessageSendEvent::SendTxAbort { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } + *node_id == a_id + }, MessageSendEvent::SendChannelReady { .. } => continue, MessageSendEvent::SendAnnouncementSignatures { .. } => continue, MessageSendEvent::SendChannelUpdate { ref node_id, ref msg } => { @@ -1208,7 +1323,79 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { dest.handle_stfu(nodes[$node].get_our_node_id(), msg); } } - } + }, + MessageSendEvent::SendTxAddInput { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_add_input from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_add_input(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendTxAddOutput { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_add_output from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_add_output(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendTxRemoveInput { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_remove_input from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_remove_input(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendTxRemoveOutput { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_remove_output from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_remove_output(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendTxComplete { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_complete from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_complete(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendTxAbort { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering tx_abort from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_tx_abort(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendSpliceInit { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering splice_init from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_splice_init(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendSpliceAck { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering splice_ack from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_splice_ack(nodes[$node].get_our_node_id(), msg); + } + } + }, + MessageSendEvent::SendSpliceLocked { ref node_id, ref msg } => { + for (idx, dest) in nodes.iter().enumerate() { + if dest.get_our_node_id() == *node_id { + out.locked_write(format!("Delivering splice_locked from node {} to node {}.\n", $node, idx).as_bytes()); + dest.handle_splice_locked(nodes[$node].get_our_node_id(), msg); + } + } + }, MessageSendEvent::SendChannelReady { .. } => { // Can be generated as a reestablish response }, @@ -1347,6 +1534,25 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { events::Event::PaymentForwarded { .. } if $node == 1 => {}, events::Event::ChannelReady { .. } => {}, events::Event::HTLCHandlingFailed { .. } => {}, + + events::Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } => { + let signed_tx = wallets[$node].sign_tx(unsigned_transaction).unwrap(); + nodes[$node] + .funding_transaction_signed( + &channel_id, + &counterparty_node_id, + signed_tx, + ) + .unwrap(); + }, + events::Event::SplicePending { .. } => {}, + events::Event::SpliceFailed { .. } => {}, + _ => { if out.may_fail.load(atomic::Ordering::Acquire) { return; @@ -1652,16 +1858,220 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { }, 0xa0 => { - nodes[0].maybe_propose_quiescence(&nodes[1].get_our_node_id(), &chan_a_id).unwrap() + let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); + let contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(10_000), + inputs: vec![input], + change_script: None, + }; + let funding_feerate_sat_per_kw = fee_est_a.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[0].splice_channel( + &chan_a_id, + &nodes[1].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } }, 0xa1 => { - nodes[1].maybe_propose_quiescence(&nodes[0].get_our_node_id(), &chan_a_id).unwrap() + let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); + let contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(10_000), + inputs: vec![input], + change_script: None, + }; + let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[1].splice_channel( + &chan_a_id, + &nodes[0].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } }, 0xa2 => { - nodes[1].maybe_propose_quiescence(&nodes[2].get_our_node_id(), &chan_b_id).unwrap() + let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); + let contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(10_000), + inputs: vec![input], + change_script: None, + }; + let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[1].splice_channel( + &chan_b_id, + &nodes[2].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } }, 0xa3 => { - nodes[2].maybe_propose_quiescence(&nodes[1].get_our_node_id(), &chan_b_id).unwrap() + let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); + let contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(10_000), + inputs: vec![input], + change_script: None, + }; + let funding_feerate_sat_per_kw = fee_est_c.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[2].splice_channel( + &chan_b_id, + &nodes[1].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } + }, + + // We conditionally splice out `MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS` only when the node + // has double the balance required to send a payment upon a `0xff` byte. We do this to + // ensure there's always liquidity available for a payment to succeed then. + 0xa4 => { + let outbound_capacity_msat = nodes[0] + .list_channels() + .iter() + .find(|chan| chan.channel_id == chan_a_id) + .map(|chan| chan.outbound_capacity_msat) + .unwrap(); + if outbound_capacity_msat >= 20_000_000 { + let contribution = SpliceContribution::SpliceOut { + outputs: vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[0].script_pubkey.clone(), + }], + }; + let funding_feerate_sat_per_kw = + fee_est_a.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[0].splice_channel( + &chan_a_id, + &nodes[1].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } + } + }, + 0xa5 => { + let outbound_capacity_msat = nodes[1] + .list_channels() + .iter() + .find(|chan| chan.channel_id == chan_a_id) + .map(|chan| chan.outbound_capacity_msat) + .unwrap(); + if outbound_capacity_msat >= 20_000_000 { + let contribution = SpliceContribution::SpliceOut { + outputs: vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), + }], + }; + let funding_feerate_sat_per_kw = + fee_est_b.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[1].splice_channel( + &chan_a_id, + &nodes[0].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } + } + }, + 0xa6 => { + let outbound_capacity_msat = nodes[1] + .list_channels() + .iter() + .find(|chan| chan.channel_id == chan_b_id) + .map(|chan| chan.outbound_capacity_msat) + .unwrap(); + if outbound_capacity_msat >= 20_000_000 { + let contribution = SpliceContribution::SpliceOut { + outputs: vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), + }], + }; + let funding_feerate_sat_per_kw = + fee_est_b.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[1].splice_channel( + &chan_b_id, + &nodes[2].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } + } + }, + 0xa7 => { + let outbound_capacity_msat = nodes[2] + .list_channels() + .iter() + .find(|chan| chan.channel_id == chan_b_id) + .map(|chan| chan.outbound_capacity_msat) + .unwrap(); + if outbound_capacity_msat >= 20_000_000 { + let contribution = SpliceContribution::SpliceOut { + outputs: vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: coinbase_tx.output[2].script_pubkey.clone(), + }], + }; + let funding_feerate_sat_per_kw = + fee_est_c.ret_val.load(atomic::Ordering::Acquire); + if let Err(e) = nodes[2].splice_channel( + &chan_b_id, + &nodes[1].get_our_node_id(), + contribution, + funding_feerate_sat_per_kw, + None, + ) { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), + "{:?}", + e + ); + } + } }, 0xb0 | 0xb1 | 0xb2 => { @@ -1828,16 +2238,6 @@ pub fn do_test(data: &[u8], underlying_out: Out, anchors: bool) { } }; } - // We may be pending quiescence, so first process all messages to ensure we can - // complete the quiescence handshake. - process_all_events!(); - - // Then exit quiescence and process all messages again, to resolve any pending - // HTLCs (only irrevocably committed ones) before attempting to send more payments. - nodes[0].exit_quiescence(&nodes[1].get_our_node_id(), &chan_a_id).unwrap(); - nodes[1].exit_quiescence(&nodes[0].get_our_node_id(), &chan_a_id).unwrap(); - nodes[1].exit_quiescence(&nodes[2].get_our_node_id(), &chan_b_id).unwrap(); - nodes[2].exit_quiescence(&nodes[1].get_our_node_id(), &chan_b_id).unwrap(); process_all_events!(); // Finally, make sure that at least one end of each channel can make a substantial payment