From e88ef680cf5d79cb1e29f4b9b49c1d1b72e79103 Mon Sep 17 00:00:00 2001 From: Jeffrey Czyz Date: Mon, 19 Oct 2026 15:20:51 -0500 Subject: [PATCH 1/2] Bound splice-outs by the next splice-out maximum splice_out refused any amount above the channel's outbound capacity. That capacity is computed against the current channel reserve, but a splice-out shrinks the channel and with it the reserve, so LDK can accept a larger splice-out than the guard allowed. Compare against LDK's next_splice_out_maximum_sat instead, which accounts for the reduced reserve, so the guard only rejects amounts LDK would refuse. Suggested by tankyleo in #1103. Co-Authored-By: Claude Fable 5.1 --- src/lib.rs | 4 +--- tests/integration_tests_rust.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fb295d3ff9..c2be7adaab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2034,9 +2034,7 @@ impl Node { if let Some(channel_details) = open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0) { - let splice_amount_msat = - splice_amount_sats.checked_mul(1_000).ok_or(Error::ChannelSplicingFailed)?; - if splice_amount_msat > channel_details.outbound_capacity_msat { + if splice_amount_sats > channel_details.next_splice_out_maximum_sat { return Err(Error::ChannelSplicingFailed); } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index c1fc7df161..9c2895f38d 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -2994,7 +2994,7 @@ async fn splice_channel() { assert_eq!(node_b.list_balances().total_lightning_balance_sats, 0); let address = node_a.onchain_payment().new_address().unwrap(); - let excessive_splice_out_sats = node_a.list_channels()[0].outbound_capacity_msat / 1000 + 1; + let excessive_splice_out_sats = node_a.list_channels()[0].next_splice_out_maximum_sat + 1; assert_eq!( node_a.splice_out( &user_channel_id_a, From af0f5e12a71b0b157c699534e8841db24e818951 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 21 Oct 2026 21:39:13 +0000 Subject: [PATCH 2/2] Add `Node::splice_out_max` In case a user wants to withdraw on-chain the maximum amount possible from a channel, the sum of the value of the spliced output and the fees they contribute to the splice has to be equal to `ChannelDetails::next_splice_out_maximum_sat`. It is currently not ergonomic to express this via the current `Node::splice_out` API, so we add `Node::splice_out_max` to satisfy this gap. This API remains unfinished; we would like any fee bumps of such splices to decrease the value of the spliced output instead of increasing the total value of the inputs of the splice transaction. Co-Authored-By: HAL 9000 --- CHANGELOG.md | 1 + bindings/ldk_node.udl | 2 + src/lib.rs | 119 ++++++++++++++- src/types.rs | 8 + tests/integration_tests_rust.rs | 251 ++++++++++++++++++++++++++++++++ 5 files changed, 373 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3786474ac..2261dbeadc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ `Event::PaymentClaimable`. ## Feature and API updates +- `Node::splice_out_max` withdraws the maximum available channel balance to an on-chain address. - All `Node::open_channel` variants now take an optional `FeeRate` (after `push_to_counterparty_msat`) that, when set, is used for the channel's funding transaction instead of the internal fee estimation. For the `_with_all` variants it also determines the rate at which the maximum funding amount is diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 14f65811de..febc94d91c 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -98,6 +98,8 @@ interface Node { [Throws=NodeError] void splice_out([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, u64 splice_amount_sats, FeeRate? min_fee_rate); [Throws=NodeError] + void splice_out_max([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, [ByRef]Address address, FeeRate? min_fee_rate); + [Throws=NodeError] void bump_channel_funding_fee([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, FeeRate? min_fee_rate); [Throws=NodeError] void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id); diff --git a/src/lib.rs b/src/lib.rs index c2be7adaab..da6bf38227 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,6 +160,7 @@ pub use lightning::ln::channel_state::{ OutboundHTLCStateDetails, }; use lightning::ln::channelmanager::PaymentId; +use lightning::ln::funding::{FundingContributionError, FundingTemplate}; use lightning::ln::msgs::{BaseMessageHandler, SocketAddress}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::routing::gossip::NodeAlias; @@ -2004,6 +2005,11 @@ impl Node { /// Remove funds from an existing channel, sending them to an on-chain address. /// + /// `splice_amount_sats` is the value to be sent to the destination address. + /// This value plus transaction fees must fit within [`ChannelDetails::next_splice_out_maximum_sat`]. + /// Use [`Node::splice_out_max`] to send on-chain the maximum available balance from the + /// channel. + /// /// This provides for decreasing a channel's outbound liquidity without re-balancing or closing /// it. Once negotiation with the counterparty is complete, the channel remains operational /// while waiting for a new funding transaction to confirm. @@ -2027,15 +2033,75 @@ impl Node { return Err(Error::NotRunning); } - let min_fee_rate = maybe_map_fee_rate_opt(min_fee_rate); + self.splice_out_inner( + user_channel_id, + counterparty_node_id, + address, + FundingAmount::Exact { amount_sats: splice_amount_sats }, + maybe_map_fee_rate_opt(min_fee_rate), + ) + } + /// Remove the maximum available funds from a channel, sending them to an on-chain address. + /// + /// This is similar to [`Node::splice_out`], but calculates the destination output value by + /// subtracting splice transaction fees from [`ChannelDetails::next_splice_out_maximum_sat`]. + /// The channel stays open and retains its required reserve and commitment fee budget. + /// Returns [`NodeError::ChannelSplicingFailed`] if the available amount cannot cover fees and + /// a non-dust output. + /// + /// `min_fee_rate` has the same meaning as in [`Node::splice_out`]. The output is calculated + /// using that rate, or the chain source estimate if unset, raised to the minimum RBF feerate + /// when required and within our fee-rate ceiling. + /// + /// The maximum is read when this call constructs the contribution. Changes to channel state + /// or fees can cause negotiation to fail. A concurrent peer-initiated splice can change our + /// fee share, leaving some of the available balance unwithdrawn. Using the full budget also + /// leaves no channel balance available for a higher transaction fee. + /// + /// [`Node::bump_channel_funding_fee`] preserves the destination output value, including for + /// maximum withdrawals. If the channel cannot cover the higher fee, it may fund the entire + /// output and fees from the on-chain wallet instead, leaving the channel balance unwithdrawn. + /// It does not reduce the destination output to pay the additional fee. + /// + /// Success means the contribution was submitted, not that the splice has completed. Monitor + /// [`Event::SpliceNegotiated`] or [`Event::SpliceNegotiationFailed`] for the negotiation result, + /// and [`Event::ChannelReady`] for the channel's transition to the new funding output. + /// + /// # Experimental API + /// + /// This API is experimental. Currently, a splice-out will be marked as an inbound payment if + /// paid to an address associated with the on-chain wallet, but this classification may change + /// in the future. + pub fn splice_out_max( + &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, address: &Address, + min_fee_rate: Option, + ) -> Result<(), Error> { + if !*self.is_running.read().expect("lock") { + return Err(Error::NotRunning); + } + self.splice_out_inner( + user_channel_id, + counterparty_node_id, + address, + FundingAmount::Max, + maybe_map_fee_rate_opt(min_fee_rate), + ) + } + + fn splice_out_inner( + &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, address: &Address, + splice_amount: FundingAmount, min_fee_rate: Option, + ) -> Result<(), Error> { let open_channels = self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); if let Some(channel_details) = open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0) { - if splice_amount_sats > channel_details.next_splice_out_maximum_sat { - return Err(Error::ChannelSplicingFailed); + if let FundingAmount::Exact { amount_sats } = &splice_amount { + if *amount_sats > channel_details.next_splice_out_maximum_sat { + return Err(Error::ChannelSplicingFailed); + } } self.wallet.parse_and_validate_address(address)?; @@ -2071,12 +2137,24 @@ impl Node { _ => min_feerate, }; - let outputs = vec![bitcoin::TxOut { - value: Amount::from_sat(splice_amount_sats), - script_pubkey: address.script_pubkey(), - }]; + let script_pubkey = address.script_pubkey(); + let output = match splice_amount { + FundingAmount::Exact { amount_sats } => { + bitcoin::TxOut { value: Amount::from_sat(amount_sats), script_pubkey } + }, + FundingAmount::Max => max_splice_out_output( + &funding_template, + script_pubkey, + Amount::from_sat(channel_details.next_splice_out_maximum_sat), + feerate, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to calculate maximum splice output: {}", e); + Error::ChannelSplicingFailed + })?, + }; let contribution = - funding_template.splice_out(outputs, feerate, max_feerate).map_err(|e| { + funding_template.splice_out(vec![output], feerate, max_feerate).map_err(|e| { log_error!(self.logger, "Failed to splice channel: {}", e); Error::ChannelSplicingFailed })?; @@ -2107,6 +2185,11 @@ impl Node { /// (RBF). The splice's amount and destination are preserved; only the fee rate is raised. /// Errors if the channel has no pending splice to bump. /// + /// For a splice-out, additional fees are taken from the channel balance when possible. If + /// that balance is insufficient, wallet inputs may fund the entire withdrawal output and + /// fees, leaving the channel balance unwithdrawn. This also applies to withdrawals created + /// with [`Node::splice_out_max`]; the output is not reduced to cover the higher fee. + /// /// If `min_fee_rate` is set, it will be used as the minimum feerate for the replacement instead /// of a feerate estimate retrieved from the configured chain source. As with the estimate, we /// pay the feerate required to replace the pending splice if it is higher, but no more than 1.5 @@ -2579,6 +2662,26 @@ impl Node { } } +// TODO: Replace this helper with upstream LDK drain-output construction so the contribution +// retains maximum-withdrawal semantics through RBF. +// See https://git.rust-bitcoin.org/lightningdevkit/rust-lightning/issues/5305 +fn max_splice_out_output( + funding_template: &FundingTemplate, script_pubkey: bitcoin::ScriptBuf, maximum: Amount, + feerate: bitcoin::FeeRate, +) -> Result { + let minimum_output = script_pubkey.minimal_non_dust(); + let mut output = bitcoin::TxOut { value: minimum_output, script_pubkey }; + let fee = funding_template + .clone() + .splice_out(vec![output.clone()], feerate, feerate)? + .estimated_fee(); + output.value = maximum + .checked_sub(fee) + .filter(|value| *value >= minimum_output) + .ok_or(FundingContributionError::InvalidSpliceValue)?; + Ok(output) +} + impl Drop for Node { fn drop(&mut self) { let _ = self.stop(); diff --git a/src/types.rs b/src/types.rs index c1abd6062d..9eb3fdaa8c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -590,6 +590,14 @@ pub struct ChannelDetails { /// route which is valid. pub next_outbound_htlc_minimum_msat: u64, /// The maximum value, in satoshis, of the next splice out from our channel balance. + /// + /// This includes splice transaction fees and accounts for the reserve required after the + /// splice. [`Node::splice_out`] takes the destination output value, so its amount plus fees + /// must fit within this maximum. [`Node::splice_out_max`] calculates that output value + /// automatically. The maximum may change as channel state or fee estimates change. + /// + /// [`Node::splice_out`]: crate::Node::splice_out + /// [`Node::splice_out_max`]: crate::Node::splice_out_max pub next_splice_out_maximum_sat: u64, /// The number of blocks (after our commitment transaction confirms) that we will need to wait /// until we can claim our funds after we force-close the channel. During this time our diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 9c2895f38d..e0e747c350 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -3128,6 +3128,253 @@ async fn splice_channel() { ); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn splice_out_max() { + do_splice_out_max(false, None, false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn splice_out_max_funder_with_fee_rate() { + // Also exercise the original channel funder, which must retain commitment fees and anchors. + do_splice_out_max(true, Some(FeeRate::from_sat_per_kwu(2_500)), false).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn rbf_splice_out_max() { + do_splice_out_max(false, None, true).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn rbf_splice_out_max_funder_with_fee_rate() { + do_splice_out_max(true, Some(FeeRate::from_sat_per_kwu(2_500)), true).await; +} + +async fn do_splice_out_max(is_funder: bool, fee_rate: Option, bump_fee: bool) { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![ + node_a.onchain_payment().new_address().unwrap(), + node_b.onchain_payment().new_address().unwrap(), + ], + Amount::from_sat(1_000_000), + ) + .await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + let push_msat = if is_funder { 0 } else { 5_300_000 }; + let original_txo = + open_channel_push_amt(&node_a, &node_b, 500_000, Some(push_msat), false, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + let (node, peer) = if is_funder { (&node_a, &node_b) } else { (&node_b, &node_a) }; + let address = node.onchain_payment().new_address().unwrap(); + let minimum_output_sat = address.script_pubkey().minimal_non_dust().to_sat(); + let initial = node.list_channels().remove(0); + assert_eq!( + node.splice_out_max( + &UserChannelId(initial.user_channel_id.0.wrapping_add(1)), + peer.node_id(), + &address, + None + ), + Err(NodeError::ChannelSplicingFailed) + ); + let wrong_network_address = + Address::from_script(&address.script_pubkey(), bitcoin::Network::Bitcoin).unwrap(); + assert_eq!( + node.splice_out_max(&initial.user_channel_id, peer.node_id(), &wrong_network_address, None), + Err(NodeError::InvalidAddress) + ); + if !is_funder { + // The balance covers a non-dust output alone, but fees would leave the output below dust. + assert!(initial.next_splice_out_maximum_sat >= minimum_output_sat); + assert_eq!( + node.splice_out_max(&initial.user_channel_id, peer.node_id(), &address, None), + Err(NodeError::ChannelSplicingFailed) + ); + } + + // Leave the funder 450,000 sats for a large withdrawal. For the non-funder, 5,472 sats + // allows a 477-sat withdrawal: the 183-sat fee leaves exactly the 294-sat dust threshold. + let payment_msat = if is_funder { 50_000_000 } else { 5_472_000 - push_msat }; + let payment_id = + node_a.spontaneous_payment().send(payment_msat, node_b.node_id(), None).unwrap(); + expect_payment_successful_event!(node_a, payment_id, None); + expect_payment_received_event!(node_b, payment_msat); + tokio::time::timeout(Duration::from_secs(common::INTEROP_TIMEOUT_SECS), async { + loop { + if node_a.list_channels()[0].pending_outbound_htlcs.is_empty() + && node_b.list_channels()[0].pending_inbound_htlcs.is_empty() + { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("payment HTLC should settle before splicing"); + let before = node.list_channels().remove(0); + let balance_before = node.list_balances(); + + // A fee that consumes the available balance must fail before submitting a contribution. + assert_eq!( + node.splice_out_max( + &before.user_channel_id, + peer.node_id(), + &address, + Some(FeeRate::from_sat_per_kwu(1_000_000).into()) + ), + Err(NodeError::ChannelSplicingFailed) + ); + node.splice_out_max( + &before.user_channel_id, + peer.node_id(), + &address, + fee_rate.map(Into::into), + ) + .unwrap(); + let txo = expect_splice_negotiated_event!(node, peer.node_id()); + // Repeating the request must not append another withdrawal to a pending contribution. + assert_eq!( + node.splice_out_max( + &before.user_channel_id, + peer.node_id(), + &address, + fee_rate.map(Into::into) + ), + Err(NodeError::ChannelSplicingFailed) + ); + wait_for_classified_funding_payment(node, txo.txid).await; + wait_for_tx(&electrsd.client, txo.txid).await; + let tx = electrsd.client.transaction_get(&txo.txid).unwrap(); + assert_eq!(tx.input.len(), 1); + assert_eq!(tx.input[0].previous_output, original_txo); + assert_eq!(tx.output.len(), 2); + let output = tx.output.iter().find(|o| o.script_pubkey == address.script_pubkey()).unwrap(); + let output_sat = output.value.to_sat(); + assert!(output.value >= output.script_pubkey.minimal_non_dust()); + let fee_sat = + before.channel_value_sats - tx.output.iter().map(|o| o.value.to_sat()).sum::(); + assert!(fee_sat > 0); + if !is_funder { + // The observed fee confirms the earlier failure was a dust remainder, not fee underflow. + assert!(initial.next_splice_out_maximum_sat > fee_sat); + assert!(initial.next_splice_out_maximum_sat - fee_sat < minimum_output_sat); + assert_eq!(output_sat, minimum_output_sat); + } else { + assert!(output_sat > before.outbound_capacity_msat / 1_000); + } + assert_eq!(output_sat + fee_sat, before.next_splice_out_maximum_sat); + let new_channel_value = tx.output[txo.vout as usize].value.to_sat(); + assert_eq!(new_channel_value, before.channel_value_sats - before.next_splice_out_maximum_sat); + + if let Some(fee_rate) = fee_rate { + assert_splice_pays_fee_rate(node, &electrsd.client, txo.txid, fee_rate); + } + + if !bump_fee { + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node.sync_wallets().unwrap(); + peer.sync_wallets().unwrap(); + expect_channel_ready_event!(node, peer.node_id()); + expect_channel_ready_event!(peer, node.node_id()); + let after = node.list_channels().remove(0); + assert_eq!(after.funding_txo, Some(txo)); + assert_eq!(peer.list_channels()[0].funding_txo, Some(txo)); + assert_eq!(after.channel_value_sats, new_channel_value); + assert_eq!(after.next_splice_out_maximum_sat, 0); + assert_eq!( + node.list_balances().total_onchain_balance_sats, + balance_before.total_onchain_balance_sats + output_sat + ); + let payment = funding_payment(node, txo.txid); + assert_eq!(payment.status, PaymentStatus::Succeeded); + assert_eq!(payment.direction, PaymentDirection::Inbound); + assert_eq!(payment.amount_msat, Some(before.next_splice_out_maximum_sat * 1000)); + assert_eq!(payment.fee_paid_msat, Some(fee_sat * 1000)); + + // Draining the available balance still leaves the channel able to receive payments. + let payment_id = peer.spontaneous_payment().send(1_000_000, node.node_id(), None).unwrap(); + expect_payment_successful_event!(peer, payment_id, None); + expect_payment_received_event!(node, 1_000_000); + return; + } + + // Bump by 20%, within the original 1.5x ceiling. With no channel balance left for + // fees, RBF currently preserves the withdrawal and funds it entirely from the wallet. + let bump_fee_rate = FeeRate::from_sat_per_kwu( + fee_rate.unwrap_or(FeeRate::from_sat_per_kwu(250)).to_sat_per_kwu() * 6 / 5, + ); + node.sync_wallets().unwrap(); + peer.sync_wallets().unwrap(); + node.bump_channel_funding_fee( + &before.user_channel_id, + peer.node_id(), + Some(bump_fee_rate.into()), + ) + .unwrap(); + let rbf_txo = expect_splice_negotiated_event!(node, peer.node_id()); + assert_ne!(rbf_txo.txid, txo.txid); + wait_for_classified_funding_payment(node, rbf_txo.txid).await; + wait_for_tx(&electrsd.client, rbf_txo.txid).await; + let rbf_tx = electrsd.client.transaction_get(&rbf_txo.txid).unwrap(); + assert!(rbf_tx.input.len() > 1); + assert!(rbf_tx.input.iter().any(|input| input.previous_output == original_txo)); + let withdrawal_outputs: Vec<_> = + rbf_tx.output.iter().filter(|o| o.script_pubkey == address.script_pubkey()).collect(); + assert_eq!(withdrawal_outputs, vec![output]); + let rbf_channel_value = rbf_tx.output[rbf_txo.vout as usize].value.to_sat(); + assert!(rbf_channel_value >= before.channel_value_sats); + // If BDK reserves more for fees than LDK charges, the surplus goes into the channel + // instead of wallet change. Account for this credit in both balances. + let channel_credit_sat = rbf_channel_value - before.channel_value_sats; + let input_sat = rbf_tx + .input + .iter() + .map(|input| { + let prevout = input.previous_output; + electrsd.client.transaction_get(&prevout.txid).unwrap().output[prevout.vout as usize] + .value + .to_sat() + }) + .sum::(); + let rbf_fee_sat = input_sat - rbf_tx.output.iter().map(|o| o.value.to_sat()).sum::(); + assert!(rbf_fee_sat > fee_sat); + let paid_sat_per_kwu = rbf_fee_sat * 1_000 / rbf_tx.weight().to_wu(); + assert!( + paid_sat_per_kwu.abs_diff(bump_fee_rate.to_sat_per_kwu()) + <= bump_fee_rate.to_sat_per_kwu() / 100 + ); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + node.sync_wallets().unwrap(); + peer.sync_wallets().unwrap(); + expect_channel_ready_event!(node, peer.node_id()); + expect_channel_ready_event!(peer, node.node_id()); + let after = node.list_channels().remove(0); + let balance_after = node.list_balances(); + assert_eq!(after.funding_txo, Some(rbf_txo)); + assert_eq!(peer.list_channels()[0].funding_txo, Some(rbf_txo)); + assert_eq!(after.channel_value_sats, rbf_channel_value); + assert!(after.next_splice_out_maximum_sat >= before.next_splice_out_maximum_sat); + assert_eq!( + balance_after.total_lightning_balance_sats, + balance_before.total_lightning_balance_sats + channel_credit_sat + ); + // The withdrawal pays our own wallet, so only the fee and channel credit leave it. + assert_eq!( + balance_after.total_onchain_balance_sats, + balance_before.total_onchain_balance_sats - rbf_fee_sat - channel_credit_sat + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn splice_channel_with_fee_rate() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -5856,6 +6103,10 @@ async fn live_node_apis_return_not_running_after_stop() { node_a.splice_out(&user_channel_id, node_b_id, &btc_address, 10_000, None), Err(NodeError::NotRunning) ); + assert_eq!( + node_a.splice_out_max(&user_channel_id, node_b_id, &btc_address, None), + Err(NodeError::NotRunning) + ); assert_eq!( node_a.bump_channel_funding_fee(&user_channel_id, node_b_id, None), Err(NodeError::NotRunning)