From 7453b39c41aa0af3458a1666a5ffa7ce5a3217a2 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 26 Jun 2026 19:43:06 +0000 Subject: [PATCH 01/23] Remove all carriage returns at end-of-lines We only do this for rust files, and leave bat files untouched. Use git show --ignore-cr-at-eol to check that this commit has no other edits. --- src/liquidity/client/mod.rs | 22 +- src/liquidity/service/lsps2.rs | 1074 ++++++++++++++++---------------- src/liquidity/service/mod.rs | 16 +- 3 files changed, 556 insertions(+), 556 deletions(-) diff --git a/src/liquidity/client/mod.rs b/src/liquidity/client/mod.rs index 15ca7e9650..52fad2da20 100644 --- a/src/liquidity/client/mod.rs +++ b/src/liquidity/client/mod.rs @@ -1,11 +1,11 @@ -// This file is Copyright its original authors, visible in version control history. -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in -// accordance with one or both of these licenses. - -pub(crate) mod lsps1; -pub(crate) mod lsps2; - -pub use lsps1::LSPS1OrderStatus; +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +pub(crate) mod lsps1; +pub(crate) mod lsps2; + +pub use lsps1::LSPS1OrderStatus; diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 875438b0fb..1143a08d73 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -1,537 +1,537 @@ -// This file is Copyright its original authors, visible in version control history. -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in -// accordance with one or both of these licenses. - -use std::ops::Deref; -use std::sync::{Arc, RwLock, Weak}; -use std::time::Duration; - -use bitcoin::secp256k1::PublicKey; -use bitcoin::Transaction; -use chrono::Utc; -use lightning::events::HTLCHandlingFailureType; -use lightning::ln::channelmanager::InterceptId; -use lightning::ln::types::ChannelId; -use lightning::sign::EntropySource; -use lightning_liquidity::lsps0::ser::LSPSDateTime; -use lightning_liquidity::lsps2::event::LSPS2ServiceEvent; -use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams; -use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; -use lightning_types::payment::PaymentHash; - -use crate::logger::{log_error, LdkLogger}; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; -use crate::{total_anchor_channels_reserve_sats, Config}; - -const LSPS2_GETINFO_REQUEST_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); -const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; - -pub(crate) struct LSPS2Service { - pub(crate) service_config: LSPS2ServiceConfig, - pub(crate) ldk_service_config: LdkLSPS2ServiceConfig, -} - -pub(crate) struct LSPS2ServiceLiquiditySource -where - L::Target: LdkLogger, -{ - pub(crate) lsps2_service: Option, - pub(crate) wallet: Arc, - pub(crate) channel_manager: Arc, - pub(crate) peer_manager: RwLock>>, - pub(crate) keys_manager: Arc, - pub(crate) liquidity_manager: Arc, - pub(crate) config: Arc, - pub(crate) logger: L, -} - -/// Represents the configuration of the LSPS2 service. -/// -/// See [bLIP-52 / LSPS2] for more information. -/// -/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md -#[derive(Debug, Clone)] -#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -pub struct LSPS2ServiceConfig { - /// A token we may require to be sent by the clients. - /// - /// If set, only requests matching this token will be accepted. - pub require_token: Option, - /// Indicates whether the LSPS service will be announced via the gossip network. - pub advertise_service: bool, - /// The fee we withhold for the channel open from the initial payment. - /// - /// This fee is proportional to the client-requested amount, in parts-per-million. - pub channel_opening_fee_ppm: u32, - /// The proportional overprovisioning for the channel. - /// - /// This determines, in parts-per-million, how much value we'll provision on top of the amount - /// we need to forward the payment to the client. - /// - /// For example, setting this to `100_000` will result in a channel being opened that is 10% - /// larger than then the to-be-forwarded amount (i.e., client-requested amount minus the - /// channel opening fee fee). - pub channel_over_provisioning_ppm: u32, - /// The minimum fee required for opening a channel. - pub min_channel_opening_fee_msat: u64, - /// The minimum number of blocks after confirmation we promise to keep the channel open. - pub min_channel_lifetime: u32, - /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. - pub max_client_to_self_delay: u32, - /// The minimum payment size that we will accept when opening a channel. - pub min_payment_size_msat: u64, - /// The maximum payment size that we will accept when opening a channel. - pub max_payment_size_msat: u64, - /// Use the 'client-trusts-LSP' trust model. - /// - /// When set, the service will delay *broadcasting* the JIT channel's funding transaction until - /// the client claimed sufficient HTLC parts to pay for the channel open. - /// - /// Note this will render the flow incompatible with clients utilizing the 'LSP-trust-client' - /// trust model, i.e., in turn delay *claiming* any HTLCs until they see the funding - /// transaction in the mempool. - /// - /// Please refer to [`bLIP-52`] for more information. - /// - /// [`bLIP-52`]: https://github.com/lightning/blips/blob/master/blip-0052.md#trust-models - pub client_trusts_lsp: bool, - /// When set, we will allow clients to spend their entire channel balance in the channels - /// we open to them. This allows clients to try to steal your channel balance with - /// no financial penalty, so this should only be set if you trust your clients. - /// - /// See [`Node::open_0reserve_channel`] to manually open these channels. - /// - /// [`Node::open_0reserve_channel`]: crate::Node::open_0reserve_channel - pub disable_client_reserve: bool, -} - -impl LSPS2ServiceLiquiditySource -where - L::Target: LdkLogger, -{ - pub(crate) fn set_peer_manager(&self, peer_manager: Weak) { - *self.peer_manager.write().expect("lock") = Some(peer_manager); - } - - pub(crate) fn liquidity_manager(&self) -> Arc { - Arc::clone(&self.liquidity_manager) - } - - pub(crate) fn lsps2_channel_needs_manual_broadcast( - &self, counterparty_node_id: PublicKey, user_channel_id: u128, - ) -> bool { - self.lsps2_service.as_ref().map_or(false, |lsps2_service| { - lsps2_service.service_config.client_trusts_lsp - && self - .liquidity_manager() - .lsps2_service_handler() - .and_then(|handler| { - handler - .channel_needs_manual_broadcast(user_channel_id, &counterparty_node_id) - .ok() - }) - .unwrap_or(false) - }) - } - - pub(crate) fn lsps2_store_funding_transaction( - &self, user_channel_id: u128, counterparty_node_id: PublicKey, funding_tx: Transaction, - ) { - let Some(lsps2_service) = self.lsps2_service.as_ref() else { return }; - if !lsps2_service.service_config.client_trusts_lsp { - // Only necessary for client-trusts-LSP flow - return; - } - - let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); - if let Some(handler) = lsps2_service_handler { - handler - .store_funding_transaction(user_channel_id, &counterparty_node_id, funding_tx) - .unwrap_or_else(|e| { - debug_assert!(false, "Failed to store funding transaction: {:?}", e); - log_error!(self.logger, "Failed to store funding transaction: {:?}", e); - }); - } else { - log_error!(self.logger, "LSPS2 service handler is not available."); - } - } - - pub(crate) fn lsps2_funding_tx_broadcast_safe( - &self, user_channel_id: u128, counterparty_node_id: PublicKey, - ) { - let Some(lsps2_service) = self.lsps2_service.as_ref() else { return }; - if !lsps2_service.service_config.client_trusts_lsp { - // Only necessary for client-trusts-LSP flow - return; - } - - let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); - if let Some(handler) = lsps2_service_handler { - handler - .set_funding_tx_broadcast_safe(user_channel_id, &counterparty_node_id) - .unwrap_or_else(|e| { - debug_assert!( - false, - "Failed to mark funding transaction safe to broadcast: {:?}", - e - ); - log_error!( - self.logger, - "Failed to mark funding transaction safe to broadcast: {:?}", - e - ); - }); - } else { - log_error!(self.logger, "LSPS2 service handler is not available."); - } - } - - pub(crate) async fn handle_channel_ready( - &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - ) { - if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler - .channel_ready(user_channel_id, channel_id, counterparty_node_id) - .await - { - log_error!( - self.logger, - "LSPS2 service failed to handle ChannelReady event: {:?}", - e - ); - } - } - } - - pub(crate) async fn handle_htlc_intercepted( - &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, - payment_hash: PaymentHash, - ) { - if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler - .htlc_intercepted( - intercept_scid, - intercept_id, - expected_outbound_amount_msat, - payment_hash, - ) - .await - { - log_error!( - self.logger, - "LSPS2 service failed to handle HTLCIntercepted event: {:?}", - e - ); - } - } - } - - pub(crate) async fn handle_htlc_handling_failed(&self, failure_type: HTLCHandlingFailureType) { - if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler.htlc_handling_failed(failure_type).await { - log_error!( - self.logger, - "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", - e - ); - } - } - } - - pub(crate) async fn handle_payment_forwarded( - &self, next_channel_id: Option, skimmed_fee_msat: u64, - ) { - if let Some(next_channel_id) = next_channel_id { - if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = - lsps2_service_handler.payment_forwarded(next_channel_id, skimmed_fee_msat).await - { - log_error!( - self.logger, - "LSPS2 service failed to handle PaymentForwarded: {:?}", - e - ); - } - } - } - } - - pub(crate) async fn handle_event(&self, event: LSPS2ServiceEvent) { - match event { - LSPS2ServiceEvent::GetInfo { request_id, counterparty_node_id, token } => { - if let Some(lsps2_service_handler) = - self.liquidity_manager.lsps2_service_handler().as_ref() - { - let service_config = if let Some(service_config) = - self.lsps2_service.as_ref().map(|s| s.service_config.clone()) - { - service_config - } else { - log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); - return; - }; - - if let Some(required) = service_config.require_token { - if token != Some(required) { - log_error!( - self.logger, - "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", - request_id, - counterparty_node_id - ); - lsps2_service_handler.invalid_token_provided(&counterparty_node_id, request_id.clone()).unwrap_or_else(|e| { - debug_assert!(false, "Failed to reject LSPS2 request. This should never happen."); - log_error!( - self.logger, - "Failed to reject LSPS2 request {:?} from counterparty {} due to: {:?}. This should never happen.", - request_id, - counterparty_node_id, - e - ); - }); - return; - } - } - - let valid_until = LSPSDateTime(Utc::now() + LSPS2_GETINFO_REQUEST_EXPIRY); - let opening_fee_params = LSPS2RawOpeningFeeParams { - min_fee_msat: service_config.min_channel_opening_fee_msat, - proportional: service_config.channel_opening_fee_ppm, - valid_until, - min_lifetime: service_config.min_channel_lifetime, - max_client_to_self_delay: service_config.max_client_to_self_delay, - min_payment_size_msat: service_config.min_payment_size_msat, - max_payment_size_msat: service_config.max_payment_size_msat, - }; - - let opening_fee_params_menu = vec![opening_fee_params]; - - if let Err(e) = lsps2_service_handler.opening_fee_params_generated( - &counterparty_node_id, - request_id, - opening_fee_params_menu, - ) { - log_error!( - self.logger, - "Failed to handle generated opening fee params: {:?}", - e - ); - } - } else { - log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); - return; - } - }, - LSPS2ServiceEvent::BuyRequest { - request_id, - counterparty_node_id, - opening_fee_params: _, - payment_size_msat, - } => { - if let Some(lsps2_service_handler) = - self.liquidity_manager.lsps2_service_handler().as_ref() - { - let service_config = if let Some(service_config) = - self.lsps2_service.as_ref().map(|s| s.service_config.clone()) - { - service_config - } else { - log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); - return; - }; - - let user_channel_id: u128 = u128::from_ne_bytes( - self.keys_manager.get_secure_random_bytes()[..16] - .try_into() - .expect("a 16-byte slice should convert into a [u8; 16]"), - ); - let intercept_scid = self.channel_manager.get_intercept_scid(); - - if let Some(payment_size_msat) = payment_size_msat { - // We already check this in `lightning-liquidity`, but better safe than - // sorry. - // - // TODO: We might want to eventually send back an error here, but we - // currently can't and have to trust `lightning-liquidity` is doing the - // right thing. - // - // TODO: Eventually we also might want to make sure that we have sufficient - // liquidity for the channel opening here. - if payment_size_msat > service_config.max_payment_size_msat - || payment_size_msat < service_config.min_payment_size_msat - { - log_error!( - self.logger, - "Rejecting to handle LSPS2 buy request {:?} from counterparty {} as the client requested an invalid payment size.", - request_id, - counterparty_node_id - ); - return; - } - } - - match lsps2_service_handler - .invoice_parameters_generated( - &counterparty_node_id, - request_id, - intercept_scid, - LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, - service_config.client_trusts_lsp, - user_channel_id, - ) - .await - { - Ok(()) => {}, - Err(e) => { - log_error!( - self.logger, - "Failed to provide invoice parameters: {:?}", - e - ); - return; - }, - } - } else { - log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); - return; - } - }, - LSPS2ServiceEvent::OpenChannel { - their_network_key, - amt_to_forward_msat, - opening_fee_msat: _, - user_channel_id, - intercept_scid: _, - } => { - if self.liquidity_manager.lsps2_service_handler().is_none() { - log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); - return; - }; - - let service_config = if let Some(service_config) = - self.lsps2_service.as_ref().map(|s| s.service_config.clone()) - { - service_config - } else { - log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); - return; - }; - - let init_features = if let Some(Some(peer_manager)) = - self.peer_manager.read().expect("lock").as_ref().map(|weak| weak.upgrade()) - { - // Fail if we're not connected to the prospective channel partner. - if let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) { - peer.init_features - } else { - // TODO: We just silently fail here. Eventually we will need to remember - // the pending requests and regularly retry opening the channel until we - // succeed. - log_error!( - self.logger, - "Failed to open LSPS2 channel to {} due to peer not being not connected.", - their_network_key, - ); - return; - } - } else { - debug_assert!(false, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); - log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); - return; - }; - - // Fail if we have insufficient onchain funds available. - let over_provisioning_msat = (amt_to_forward_msat - * service_config.channel_over_provisioning_ppm as u64) - / 1_000_000; - let channel_amount_sats = (amt_to_forward_msat + over_provisioning_msat) / 1000; - let cur_anchor_reserve_sats = - total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); - let spendable_amount_sats = - self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); - let required_funds_sats = channel_amount_sats - + self.config.anchor_channels_config.as_ref().map_or(0, |c| { - if init_features.requires_anchors_zero_fee_htlc_tx() - && !c.trusted_peers_no_reserve.contains(&their_network_key) - { - c.per_channel_reserve_sats - } else { - 0 - } - }); - if spendable_amount_sats < required_funds_sats { - log_error!(self.logger, - "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", - spendable_amount_sats, channel_amount_sats - ); - // TODO: We just silently fail here. Eventually we will need to remember - // the pending requests and regularly retry opening the channel until we - // succeed. - return; - } - - let mut config = self.channel_manager.get_current_config().clone(); - - // If we act as an LSPS2 service, the HTLC-value-in-flight must be 100% of the - // channel value to ensure we can forward the initial payment. That cap only - // applies to unannounced channels, so the channel must also be unannounced. - debug_assert_eq!( - config - .channel_handshake_config - .unannounced_channel_max_inbound_htlc_value_in_flight_percentage, - 100 - ); - debug_assert!(!config.channel_handshake_config.announce_for_forwarding); - debug_assert!(config.accept_forwards_to_priv_channels); - - // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. - // - // TODO: revisit this decision eventually. - config.channel_config.forwarding_fee_base_msat = 0; - config.channel_config.forwarding_fee_proportional_millionths = 0; - - let result = if service_config.disable_client_reserve { - self.channel_manager.create_channel_to_trusted_peer_0reserve( - their_network_key, - channel_amount_sats, - 0, - user_channel_id, - None, - Some(config), - ) - } else { - self.channel_manager.create_channel( - their_network_key, - channel_amount_sats, - 0, - user_channel_id, - None, - Some(config), - ) - }; - - match result { - Ok(_) => {}, - Err(e) => { - // TODO: We just silently fail here. Eventually we will need to remember - // the pending requests and regularly retry opening the channel until we - // succeed. - let zero_reserve_string = - if service_config.disable_client_reserve { "0reserve " } else { "" }; - log_error!( - self.logger, - "Failed to open LSPS2 {}channel to {}: {:?}", - zero_reserve_string, - their_network_key, - e - ); - return; - }, - } - }, - } - } -} +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use std::ops::Deref; +use std::sync::{Arc, RwLock, Weak}; +use std::time::Duration; + +use bitcoin::secp256k1::PublicKey; +use bitcoin::Transaction; +use chrono::Utc; +use lightning::events::HTLCHandlingFailureType; +use lightning::ln::channelmanager::InterceptId; +use lightning::ln::types::ChannelId; +use lightning::sign::EntropySource; +use lightning_liquidity::lsps0::ser::LSPSDateTime; +use lightning_liquidity::lsps2::event::LSPS2ServiceEvent; +use lightning_liquidity::lsps2::msgs::LSPS2RawOpeningFeeParams; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; +use lightning_types::payment::PaymentHash; + +use crate::logger::{log_error, LdkLogger}; +use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; +use crate::{total_anchor_channels_reserve_sats, Config}; + +const LSPS2_GETINFO_REQUEST_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); +const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; + +pub(crate) struct LSPS2Service { + pub(crate) service_config: LSPS2ServiceConfig, + pub(crate) ldk_service_config: LdkLSPS2ServiceConfig, +} + +pub(crate) struct LSPS2ServiceLiquiditySource +where + L::Target: LdkLogger, +{ + pub(crate) lsps2_service: Option, + pub(crate) wallet: Arc, + pub(crate) channel_manager: Arc, + pub(crate) peer_manager: RwLock>>, + pub(crate) keys_manager: Arc, + pub(crate) liquidity_manager: Arc, + pub(crate) config: Arc, + pub(crate) logger: L, +} + +/// Represents the configuration of the LSPS2 service. +/// +/// See [bLIP-52 / LSPS2] for more information. +/// +/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md +#[derive(Debug, Clone)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +pub struct LSPS2ServiceConfig { + /// A token we may require to be sent by the clients. + /// + /// If set, only requests matching this token will be accepted. + pub require_token: Option, + /// Indicates whether the LSPS service will be announced via the gossip network. + pub advertise_service: bool, + /// The fee we withhold for the channel open from the initial payment. + /// + /// This fee is proportional to the client-requested amount, in parts-per-million. + pub channel_opening_fee_ppm: u32, + /// The proportional overprovisioning for the channel. + /// + /// This determines, in parts-per-million, how much value we'll provision on top of the amount + /// we need to forward the payment to the client. + /// + /// For example, setting this to `100_000` will result in a channel being opened that is 10% + /// larger than then the to-be-forwarded amount (i.e., client-requested amount minus the + /// channel opening fee fee). + pub channel_over_provisioning_ppm: u32, + /// The minimum fee required for opening a channel. + pub min_channel_opening_fee_msat: u64, + /// The minimum number of blocks after confirmation we promise to keep the channel open. + pub min_channel_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The minimum payment size that we will accept when opening a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size that we will accept when opening a channel. + pub max_payment_size_msat: u64, + /// Use the 'client-trusts-LSP' trust model. + /// + /// When set, the service will delay *broadcasting* the JIT channel's funding transaction until + /// the client claimed sufficient HTLC parts to pay for the channel open. + /// + /// Note this will render the flow incompatible with clients utilizing the 'LSP-trust-client' + /// trust model, i.e., in turn delay *claiming* any HTLCs until they see the funding + /// transaction in the mempool. + /// + /// Please refer to [`bLIP-52`] for more information. + /// + /// [`bLIP-52`]: https://github.com/lightning/blips/blob/master/blip-0052.md#trust-models + pub client_trusts_lsp: bool, + /// When set, we will allow clients to spend their entire channel balance in the channels + /// we open to them. This allows clients to try to steal your channel balance with + /// no financial penalty, so this should only be set if you trust your clients. + /// + /// See [`Node::open_0reserve_channel`] to manually open these channels. + /// + /// [`Node::open_0reserve_channel`]: crate::Node::open_0reserve_channel + pub disable_client_reserve: bool, +} + +impl LSPS2ServiceLiquiditySource +where + L::Target: LdkLogger, +{ + pub(crate) fn set_peer_manager(&self, peer_manager: Weak) { + *self.peer_manager.write().expect("lock") = Some(peer_manager); + } + + pub(crate) fn liquidity_manager(&self) -> Arc { + Arc::clone(&self.liquidity_manager) + } + + pub(crate) fn lsps2_channel_needs_manual_broadcast( + &self, counterparty_node_id: PublicKey, user_channel_id: u128, + ) -> bool { + self.lsps2_service.as_ref().map_or(false, |lsps2_service| { + lsps2_service.service_config.client_trusts_lsp + && self + .liquidity_manager() + .lsps2_service_handler() + .and_then(|handler| { + handler + .channel_needs_manual_broadcast(user_channel_id, &counterparty_node_id) + .ok() + }) + .unwrap_or(false) + }) + } + + pub(crate) fn lsps2_store_funding_transaction( + &self, user_channel_id: u128, counterparty_node_id: PublicKey, funding_tx: Transaction, + ) { + let Some(lsps2_service) = self.lsps2_service.as_ref() else { return }; + if !lsps2_service.service_config.client_trusts_lsp { + // Only necessary for client-trusts-LSP flow + return; + } + + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler + .store_funding_transaction(user_channel_id, &counterparty_node_id, funding_tx) + .unwrap_or_else(|e| { + debug_assert!(false, "Failed to store funding transaction: {:?}", e); + log_error!(self.logger, "Failed to store funding transaction: {:?}", e); + }); + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + } + } + + pub(crate) fn lsps2_funding_tx_broadcast_safe( + &self, user_channel_id: u128, counterparty_node_id: PublicKey, + ) { + let Some(lsps2_service) = self.lsps2_service.as_ref() else { return }; + if !lsps2_service.service_config.client_trusts_lsp { + // Only necessary for client-trusts-LSP flow + return; + } + + let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler(); + if let Some(handler) = lsps2_service_handler { + handler + .set_funding_tx_broadcast_safe(user_channel_id, &counterparty_node_id) + .unwrap_or_else(|e| { + debug_assert!( + false, + "Failed to mark funding transaction safe to broadcast: {:?}", + e + ); + log_error!( + self.logger, + "Failed to mark funding transaction safe to broadcast: {:?}", + e + ); + }); + } else { + log_error!(self.logger, "LSPS2 service handler is not available."); + } + } + + pub(crate) async fn handle_channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler + .channel_ready(user_channel_id, channel_id, counterparty_node_id) + .await + { + log_error!( + self.logger, + "LSPS2 service failed to handle ChannelReady event: {:?}", + e + ); + } + } + } + + pub(crate) async fn handle_htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler + .htlc_intercepted( + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) + .await + { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCIntercepted event: {:?}", + e + ); + } + } + } + + pub(crate) async fn handle_htlc_handling_failed(&self, failure_type: HTLCHandlingFailureType) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failure_type).await { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", + e + ); + } + } + } + + pub(crate) async fn handle_payment_forwarded( + &self, next_channel_id: Option, skimmed_fee_msat: u64, + ) { + if let Some(next_channel_id) = next_channel_id { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = + lsps2_service_handler.payment_forwarded(next_channel_id, skimmed_fee_msat).await + { + log_error!( + self.logger, + "LSPS2 service failed to handle PaymentForwarded: {:?}", + e + ); + } + } + } + } + + pub(crate) async fn handle_event(&self, event: LSPS2ServiceEvent) { + match event { + LSPS2ServiceEvent::GetInfo { request_id, counterparty_node_id, token } => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + if let Some(required) = service_config.require_token { + if token != Some(required) { + log_error!( + self.logger, + "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", + request_id, + counterparty_node_id + ); + lsps2_service_handler.invalid_token_provided(&counterparty_node_id, request_id.clone()).unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS2 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS2 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + return; + } + } + + let valid_until = LSPSDateTime(Utc::now() + LSPS2_GETINFO_REQUEST_EXPIRY); + let opening_fee_params = LSPS2RawOpeningFeeParams { + min_fee_msat: service_config.min_channel_opening_fee_msat, + proportional: service_config.channel_opening_fee_ppm, + valid_until, + min_lifetime: service_config.min_channel_lifetime, + max_client_to_self_delay: service_config.max_client_to_self_delay, + min_payment_size_msat: service_config.min_payment_size_msat, + max_payment_size_msat: service_config.max_payment_size_msat, + }; + + let opening_fee_params_menu = vec![opening_fee_params]; + + if let Err(e) = lsps2_service_handler.opening_fee_params_generated( + &counterparty_node_id, + request_id, + opening_fee_params_menu, + ) { + log_error!( + self.logger, + "Failed to handle generated opening fee params: {:?}", + e + ); + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: _, + payment_size_msat, + } => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let user_channel_id: u128 = u128::from_ne_bytes( + self.keys_manager.get_secure_random_bytes()[..16] + .try_into() + .expect("a 16-byte slice should convert into a [u8; 16]"), + ); + let intercept_scid = self.channel_manager.get_intercept_scid(); + + if let Some(payment_size_msat) = payment_size_msat { + // We already check this in `lightning-liquidity`, but better safe than + // sorry. + // + // TODO: We might want to eventually send back an error here, but we + // currently can't and have to trust `lightning-liquidity` is doing the + // right thing. + // + // TODO: Eventually we also might want to make sure that we have sufficient + // liquidity for the channel opening here. + if payment_size_msat > service_config.max_payment_size_msat + || payment_size_msat < service_config.min_payment_size_msat + { + log_error!( + self.logger, + "Rejecting to handle LSPS2 buy request {:?} from counterparty {} as the client requested an invalid payment size.", + request_id, + counterparty_node_id + ); + return; + } + } + + match lsps2_service_handler + .invoice_parameters_generated( + &counterparty_node_id, + request_id, + intercept_scid, + LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, + service_config.client_trusts_lsp, + user_channel_id, + ) + .await + { + Ok(()) => {}, + Err(e) => { + log_error!( + self.logger, + "Failed to provide invoice parameters: {:?}", + e + ); + return; + }, + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + LSPS2ServiceEvent::OpenChannel { + their_network_key, + amt_to_forward_msat, + opening_fee_msat: _, + user_channel_id, + intercept_scid: _, + } => { + if self.liquidity_manager.lsps2_service_handler().is_none() { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let init_features = if let Some(Some(peer_manager)) = + self.peer_manager.read().expect("lock").as_ref().map(|weak| weak.upgrade()) + { + // Fail if we're not connected to the prospective channel partner. + if let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) { + peer.init_features + } else { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {} due to peer not being not connected.", + their_network_key, + ); + return; + } + } else { + debug_assert!(false, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + return; + }; + + // Fail if we have insufficient onchain funds available. + let over_provisioning_msat = (amt_to_forward_msat + * service_config.channel_over_provisioning_ppm as u64) + / 1_000_000; + let channel_amount_sats = (amt_to_forward_msat + over_provisioning_msat) / 1000; + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let required_funds_sats = channel_amount_sats + + self.config.anchor_channels_config.as_ref().map_or(0, |c| { + if init_features.requires_anchors_zero_fee_htlc_tx() + && !c.trusted_peers_no_reserve.contains(&their_network_key) + { + c.per_channel_reserve_sats + } else { + 0 + } + }); + if spendable_amount_sats < required_funds_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, channel_amount_sats + ); + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + return; + } + + let mut config = self.channel_manager.get_current_config().clone(); + + // If we act as an LSPS2 service, the HTLC-value-in-flight must be 100% of the + // channel value to ensure we can forward the initial payment. That cap only + // applies to unannounced channels, so the channel must also be unannounced. + debug_assert_eq!( + config + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage, + 100 + ); + debug_assert!(!config.channel_handshake_config.announce_for_forwarding); + debug_assert!(config.accept_forwards_to_priv_channels); + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + // + // TODO: revisit this decision eventually. + config.channel_config.forwarding_fee_base_msat = 0; + config.channel_config.forwarding_fee_proportional_millionths = 0; + + let result = if service_config.disable_client_reserve { + self.channel_manager.create_channel_to_trusted_peer_0reserve( + their_network_key, + channel_amount_sats, + 0, + user_channel_id, + None, + Some(config), + ) + } else { + self.channel_manager.create_channel( + their_network_key, + channel_amount_sats, + 0, + user_channel_id, + None, + Some(config), + ) + }; + + match result { + Ok(_) => {}, + Err(e) => { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + let zero_reserve_string = + if service_config.disable_client_reserve { "0reserve " } else { "" }; + log_error!( + self.logger, + "Failed to open LSPS2 {}channel to {}: {:?}", + zero_reserve_string, + their_network_key, + e + ); + return; + }, + } + }, + } + } +} diff --git a/src/liquidity/service/mod.rs b/src/liquidity/service/mod.rs index 5e3a3b1833..cdbaf54265 100644 --- a/src/liquidity/service/mod.rs +++ b/src/liquidity/service/mod.rs @@ -1,8 +1,8 @@ -// This file is Copyright its original authors, visible in version control history. -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in -// accordance with one or both of these licenses. - -pub(crate) mod lsps2; +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +pub(crate) mod lsps2; From 217847351b8e402eabe413e4e0a5566aa1d2388c Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 27 May 2026 00:27:51 +0000 Subject: [PATCH 02/23] Only pass TRUC packages as multi-transaction vecs `BroadcasterInterface::broadcast_transactions` requires that any passed vector containing multiple transactions must be a single child together with its parents. We will lean on this contract in upcoming commits, so here we fix a case where we broke this contract. --- src/wallet/mod.rs | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index ad4f8d45ee..216d12e31f 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -326,32 +326,25 @@ impl Wallet { } } - if !unconfirmed_outbound_txids.is_empty() { - let txs_to_broadcast: Vec = unconfirmed_outbound_txids - .iter() - .filter_map(|txid| { - locked_wallet.tx_details(*txid).map(|d| (*d.tx).clone()) - }) - .collect(); - - if !txs_to_broadcast.is_empty() { - let tx_refs: Vec<( - &Transaction, - lightning::chain::chaininterface::TransactionType, - )> = - txs_to_broadcast - .iter() - .map(|tx| { - (tx, lightning::chain::chaininterface::TransactionType::Sweep { channels: vec![] }) - }) - .collect(); - self.broadcaster.broadcast_transactions(&tx_refs); - log_info!( - self.logger, - "Rebroadcast {} unconfirmed transactions on chain tip change", - txs_to_broadcast.len() - ); - } + let count: usize = unconfirmed_outbound_txids + .into_iter() + .filter_map(|txid| { + let tx = locked_wallet.tx_details(txid).map(|d| d.tx)?; + let transaction_type = + lightning::chain::chaininterface::TransactionType::Sweep { + channels: vec![], + }; + self.broadcaster + .broadcast_transactions(&[(tx.as_ref(), transaction_type)]); + Some(()) + }) + .count(); + if count != 0 { + log_info!( + self.logger, + "Rebroadcast {} unconfirmed transactions on chain tip change", + count, + ); } }, WalletEvent::TxUnconfirmed { txid, tx, .. } => { From 282addd8985d37753b5f775095056754bf435150 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Sat, 27 Jun 2026 19:43:51 +0000 Subject: [PATCH 03/23] Fix anchor reserves when splicing in all funds In an upcoming commit, we will fix `check_sufficient_funds_for_channel` to check that we have on-chain funds to cover the anchor reserve for an additional anchor channel in the validation of outbound channel opens. Before we do this, we stop using this function to check that any splice-ins leave enough on-chain anchor reserves. This function keeps an anchor reserve for an additional anchor channel on top of the existing set of anchor channels, but after splice-ins, our anchor reserve only needs to cover the existing set of anchor channels. --- src/lib.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index c97e16fe67..df4ff4153a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1370,6 +1370,23 @@ impl Node { Ok(()) } + fn check_sufficient_funds_for_splice_in(&self, amount_sats: u64) -> Result<(), Error> { + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + + if spendable_amount_sats < amount_sats { + log_error!(self.logger, + "Unable to splice channel due to insufficient funds. Available: {}sats, Requested: {}sats", + spendable_amount_sats, amount_sats + ); + return Err(Error::InsufficientFunds); + } + + Ok(()) + } + /// Connect to a node and open a new unannounced channel. /// /// To open an announced channel, see [`Node::open_announced_channel`]. @@ -1640,7 +1657,7 @@ impl Node { }, }; - self.check_sufficient_funds_for_channel(splice_amount_sats, &counterparty_node_id)?; + self.check_sufficient_funds_for_splice_in(splice_amount_sats)?; let funding_template = self .channel_manager From 4950f2d8a2a5f69687a0e2503f1b7f128a96879a Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 26 Jun 2026 19:37:40 +0000 Subject: [PATCH 04/23] Reserve onchain funds for anchor channels when peer sets them optional When we are preparing to open a channel to a peer, we should reserve onchain funds for an anchor channel when the peer's init features signals anchor channels as optional, as channel negotiation with such a peer can result in an anchor channel. Tests written with codex. --- src/lib.rs | 2 +- src/liquidity/service/lsps2.rs | 4 +- tests/integration_tests_rust.rs | 320 +++++++++++++++++++++++++++++++- 3 files changed, 321 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index df4ff4153a..a5f2fa5375 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1334,7 +1334,7 @@ impl Node { .peer_by_node_id(peer_node_id) .ok_or(Error::ConnectionFailed)? .init_features; - let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx(); + let anchor_channel = init_features.supports_anchors_zero_fee_htlc_tx(); Ok(new_channel_anchor_reserve_sats(&self.config, peer_node_id, anchor_channel)) } diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 1143a08d73..524157a671 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -454,7 +454,7 @@ where self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); let required_funds_sats = channel_amount_sats + self.config.anchor_channels_config.as_ref().map_or(0, |c| { - if init_features.requires_anchors_zero_fee_htlc_tx() + if init_features.supports_anchors_zero_fee_htlc_tx() && !c.trusted_peers_no_reserve.contains(&their_network_key) { c.per_channel_reserve_sats @@ -465,7 +465,7 @@ where if spendable_amount_sats < required_funds_sats { log_error!(self.logger, "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", - spendable_amount_sats, channel_amount_sats + spendable_amount_sats, required_funds_sats, ); // TODO: We just silently fail here. Eventually we will need to remember // the pending requests and regularly retry opening the channel until we diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index c3c2f4262b..420a2874e3 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -15,7 +15,9 @@ use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::hashes::Hash; use bitcoin::{Address, Amount, ScriptBuf, Txid}; -use common::logging::{init_log_logger, validate_log_entry, MultiNodeLogger, TestLogWriter}; +use common::logging::{ + init_log_logger, validate_log_entry, MockLogFacadeLogger, MultiNodeLogger, TestLogWriter, +}; use common::{ bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_channel_ready_events, @@ -36,7 +38,7 @@ use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, TransactionType, UnifiedPaymentResult, }; -use ldk_node::{BuildError, Builder, Event, Node, NodeError}; +use ldk_node::{BuildError, Builder, Event, Node, NodeError, ReserveType}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; @@ -2876,6 +2878,165 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps2_rejects_jit_channel_without_anchor_reserve() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.background_sync_config = None; + + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + client_trusts_lsp: false, + disable_client_reserve: false, + }; + + let service_logger = Arc::new(MockLogFacadeLogger::new()); + let service_config = random_config(true); + let anchor_reserve_sats = service_config + .node_config + .anchor_channels_config + .as_ref() + .unwrap() + .per_channel_reserve_sats; + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_custom_logger(service_logger.clone()); + service_builder.enable_liquidity_provider(lsps2_service_config); + let service_node = service_builder.build(service_config.node_entropy.into()).unwrap(); + service_node.start().unwrap(); + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.add_liquidity_source(service_node_id, service_addr, None, true); + let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); + client_node.start().unwrap(); + let client_node_id = client_node.node_id(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); + payer_node.start().unwrap(); + + let service_addr = service_node.onchain_payment().new_address().unwrap(); + let client_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_addr = payer_node.onchain_payment().new_address().unwrap(); + + let reserve_shortfall_margin_sat = 5_000; + let jit_amount_msat = 100_000_000; + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let amount_to_forward_msat = jit_amount_msat - service_fee_msat; + let channel_overprovisioning_msat = + (amount_to_forward_msat * channel_over_provisioning_ppm as u64) / 1_000_000; + let expected_channel_size_sat = (amount_to_forward_msat + channel_overprovisioning_msat) / 1000; + let service_funding_sats = + anchor_reserve_sats + expected_channel_size_sat + reserve_shortfall_margin_sat; + assert!( + service_funding_sats + < anchor_reserve_sats + expected_channel_size_sat + anchor_reserve_sats + ); + + premine_blocks(&bitcoind.client, &electrsd.client).await; + distribute_funds_unconfirmed( + &bitcoind.client, + &electrsd.client, + vec![service_addr], + Amount::from_sat(service_funding_sats), + ) + .await; + distribute_funds_unconfirmed( + &bitcoind.client, + &electrsd.client, + vec![client_addr], + Amount::from_sat(1_000_000), + ) + .await; + distribute_funds_unconfirmed( + &bitcoind.client, + &electrsd.client, + vec![payer_addr], + Amount::from_sat(10_000_000), + ) + .await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd).await; + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let service_balances = service_node.list_balances(); + assert_eq!(service_balances.total_anchor_channels_reserve_sats, anchor_reserve_sats); + assert_eq!( + service_balances.spendable_onchain_balance_sats, + expected_channel_size_sat + reserve_shortfall_margin_sat + ); + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None) + .unwrap(); + + let _payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + + tokio::time::timeout( + std::time::Duration::from_secs(crate::common::INTEROP_TIMEOUT_SECS), + async { + loop { + if service_logger + .retrieve_logs() + .iter() + .any(|log| log.contains("Unable to create channel due to insufficient funds")) + { + break; + } + assert!( + service_node + .list_channels() + .iter() + .all(|c| c.counterparty.node_id != client_node_id), + "LSPS2 service opened a channel without retaining the optional anchor reserve" + ); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + } + }, + ) + .await + .expect(&format!( + "Timed out waiting for LSPS2 insufficient-funds log. Logs: {:?}", + service_logger.retrieve_logs() + )); + + assert!(service_node.list_channels().iter().all(|c| c.counterparty.node_id != client_node_id)); + assert!(client_node.list_channels().iter().all(|c| c.counterparty.node_id != service_node_id)); + + service_node.stop().unwrap(); + client_node.stop().unwrap(); + payer_node.stop().unwrap(); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn facade_logging() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -3813,6 +3974,161 @@ async fn open_channel_with_all_with_anchors() { node_b.stop().unwrap(); } +#[derive(Clone, Copy)] +enum OpenChannelVariant { + Standard, + Announced, + ZeroReserve, + StandardWithAll, + AnnouncedWithAll, + ZeroReserveWithAll, +} + +impl OpenChannelVariant { + fn label(&self) -> &'static str { + match self { + Self::Standard => "open_channel", + Self::Announced => "open_announced_channel", + Self::ZeroReserve => "open_0reserve_channel", + Self::StandardWithAll => "open_channel_with_all", + Self::AnnouncedWithAll => "open_announced_channel_with_all", + Self::ZeroReserveWithAll => "open_0reserve_channel_with_all", + } + } +} + +fn open_channel_variant( + variant: OpenChannelVariant, node_a: &Node, node_b: &Node, channel_amount_sats: u64, +) -> Result<(), NodeError> { + let address = node_b.listening_addresses().unwrap().first().unwrap().clone(); + match variant { + OpenChannelVariant::Standard => node_a + .open_channel(node_b.node_id(), address, channel_amount_sats, None, None) + .map(|_| ()), + OpenChannelVariant::Announced => node_a + .open_announced_channel(node_b.node_id(), address, channel_amount_sats, None, None) + .map(|_| ()), + OpenChannelVariant::ZeroReserve => node_a + .open_0reserve_channel(node_b.node_id(), address, channel_amount_sats, None, None) + .map(|_| ()), + OpenChannelVariant::StandardWithAll => { + node_a.open_channel_with_all(node_b.node_id(), address, None, None).map(|_| ()) + }, + OpenChannelVariant::AnnouncedWithAll => node_a + .open_announced_channel_with_all(node_b.node_id(), address, None, None) + .map(|_| ()), + OpenChannelVariant::ZeroReserveWithAll => { + node_a.open_0reserve_channel_with_all(node_b.node_id(), address, None, None).map(|_| ()) + }, + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn open_channel_variants_reserve_funds_for_anchor_peers() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = random_chain_source(&bitcoind, &electrsd); + + let exact_variants = [ + OpenChannelVariant::Standard, + OpenChannelVariant::Announced, + OpenChannelVariant::ZeroReserve, + ]; + let with_all_variants = [ + OpenChannelVariant::StandardWithAll, + OpenChannelVariant::AnnouncedWithAll, + OpenChannelVariant::ZeroReserveWithAll, + ]; + + let premine_amount_sat = 1_000_000; + let exact_channel_amount_sat = premine_amount_sat - 10_000; + let anchor_reserve_sat = 25_000; + + let mut addresses = Vec::new(); + let mut exact_cases = Vec::new(); + for variant in exact_variants { + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + addresses.push(node_a.onchain_payment().new_address().unwrap()); + addresses.push(node_b.onchain_payment().new_address().unwrap()); + exact_cases.push((variant, node_a, node_b)); + } + + let mut with_all_cases = Vec::new(); + for variant in with_all_variants { + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + addresses.push(node_a.onchain_payment().new_address().unwrap()); + addresses.push(node_b.onchain_payment().new_address().unwrap()); + with_all_cases.push((variant, node_a, node_b)); + } + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + addresses, + Amount::from_sat(premine_amount_sat), + ) + .await; + + for (_, node_a, node_b) in exact_cases.iter().chain(with_all_cases.iter()) { + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + } + + for (variant, node_a, node_b) in exact_cases { + assert_eq!( + Err(NodeError::InsufficientFunds), + open_channel_variant(variant, &node_a, &node_b, exact_channel_amount_sat), + "{} should require funds for the channel amount plus anchor reserve", + variant.label() + ); + node_a.stop().unwrap(); + node_b.stop().unwrap(); + } + + let mut opened_with_all_cases = Vec::new(); + for (variant, node_a, node_b) in with_all_cases { + open_channel_variant(variant, &node_a, &node_b, 0) + .unwrap_or_else(|e| panic!("{} failed: {e:?}", variant.label())); + + let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id()); + let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id()); + assert_eq!(funding_txo_a, funding_txo_b, "{} funding txo mismatch", variant.label()); + wait_for_tx(&electrsd.client, funding_txo_a.txid).await; + + opened_with_all_cases.push((variant, node_a, node_b, funding_txo_a)); + } + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + + for (variant, node_a, node_b, funding_txo) in opened_with_all_cases { + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); + let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); + + let balances = node_a.list_balances(); + assert_eq!(balances.total_onchain_balance_sats, anchor_reserve_sat - 1); + assert_eq!(balances.total_anchor_channels_reserve_sats, anchor_reserve_sat - 1); + assert_eq!(balances.spendable_onchain_balance_sats, 0); + + let channels = node_a.list_channels(); + assert_eq!(channels.len(), 1, "{} should have one channel", variant.label()); + let channel = &channels[0]; + // Also subtract the fees spent to open the channel + assert_eq!(channel.channel_value_sats, premine_amount_sat - anchor_reserve_sat - 155); + assert_eq!(channel.counterparty.node_id, node_b.node_id()); + assert!(channel.counterparty.features.supports_anchors_zero_fee_htlc_tx()); + assert!(!channel.counterparty.features.requires_anchors_zero_fee_htlc_tx()); + assert_eq!(channel.funding_txo.unwrap(), funding_txo); + assert_eq!(channel.reserve_type, Some(ReserveType::Adaptive)); + + node_a.stop().unwrap(); + node_b.stop().unwrap(); + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn open_channel_with_all_without_anchors() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); From 18478561e6ab4fd3d8bd56c40ffffcb600147446 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 2 Jul 2026 05:17:11 +0000 Subject: [PATCH 05/23] f: fix uniffi compilation --- tests/common/logging.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/common/logging.rs b/tests/common/logging.rs index 1e3a8a1c2e..6dfd53e336 100644 --- a/tests/common/logging.rs +++ b/tests/common/logging.rs @@ -61,6 +61,23 @@ impl LogWriter for MockLogFacadeLogger { } } +#[cfg(feature = "uniffi")] +impl LogWriter for MockLogFacadeLogger { + fn log(&self, record: LogRecord) { + let level = MockLogLevel(record.level).into(); + let mut record_builder = log::Record::builder(); + LogFacadeLog::log( + self, + &record_builder + .level(level) + .module_path(Some(&record.module_path)) + .line(Some(record.line)) + .args(format_args!("{}", record.args)) + .build(), + ); + } +} + #[cfg(not(feature = "uniffi"))] struct MockLogRecord<'a>(LogRecord<'a>); struct MockLogLevel(LogLevel); From 4eaf80ad87f0cb18fd7232c09f1c0bc50c79b301 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Tue, 30 Jun 2026 17:06:05 +0000 Subject: [PATCH 06/23] Remove the ability to disable anchor channels We previously allowed users to disable anchor channels and drain their anchor reserve while still having anchor channels open or pending resolution. This was acceptable for keyed anchor channels, as the commitment transaction therein still contained some fees, and had some chance of getting mined into a block without any anchor bumps. In upcoming commits, we will add support for 0FC channels, and their commitment transactions have zero fees and depend entirely on the anchor reserve to reach miners and get confirmed in a block. It is thus dangerous to disable anchor channels and drain the reserve after 0FC channels have been opened. Therefore, we make `AnchorChannelsConfig` required, and prevent this case from ever happening. --- benches/payments.rs | 9 +- src/config.rs | 18 +-- src/event.rs | 40 ++--- src/lib.rs | 43 +++-- src/liquidity/service/lsps2.rs | 20 +-- src/types.rs | 20 +-- tests/common/mod.rs | 31 ++-- tests/common/scenarios/mod.rs | 2 +- tests/integration_tests_hrn.rs | 2 +- tests/integration_tests_migration.rs | 4 +- tests/integration_tests_postgres.rs | 4 +- tests/integration_tests_rust.rs | 224 +++++++++------------------ tests/integration_tests_vss.rs | 4 +- tests/reorg_test.rs | 11 +- 14 files changed, 149 insertions(+), 283 deletions(-) diff --git a/benches/payments.rs b/benches/payments.rs index 52769d7949..926dc5dade 100644 --- a/benches/payments.rs +++ b/benches/payments.rs @@ -121,13 +121,8 @@ fn payment_benchmark(c: &mut Criterion) { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes_with_store( - &chain_source, - false, - true, - false, - common::TestStoreType::Sqlite, - ); + let (node_a, node_b) = + setup_two_nodes_with_store(&chain_source, false, false, common::TestStoreType::Sqlite); let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().build().unwrap(); diff --git a/src/config.rs b/src/config.rs index ad1b911819..96b9c11c7b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -173,19 +173,7 @@ pub struct Config { /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. /// /// Please refer to [`AnchorChannelsConfig`] for further information on Anchor channels. - /// - /// If set to `Some`, we'll try to open new channels with Anchors enabled, i.e., new channels - /// will be negotiated with the `option_anchors_zero_fee_htlc_tx` channel type if supported by - /// the counterparty. Note that this won't prevent us from opening non-Anchor channels if the - /// counterparty doesn't support `option_anchors_zero_fee_htlc_tx`. If set to `None`, new - /// channels will be negotiated with the legacy `option_static_remotekey` channel type only. - /// - /// **Note:** If set to `None` *after* some Anchor channels have already been - /// opened, no dedicated emergency on-chain reserve will be maintained for these channels, - /// which can be dangerous if only insufficient funds are available at the time of channel - /// closure. We *will* however still try to get the Anchor spending transactions confirmed - /// on-chain with the funds available. - pub anchor_channels_config: Option, + pub anchor_channels_config: AnchorChannelsConfig, /// Configuration options for payment routing and pathfinding. /// /// Setting the [`RouteParametersConfig`] provides flexibility to customize how payments are routed, @@ -216,7 +204,7 @@ impl Default for Config { announcement_addresses: None, trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, - anchor_channels_config: Some(AnchorChannelsConfig::default()), + anchor_channels_config: AnchorChannelsConfig::default(), tor_config: None, route_parameters: None, node_alias: None, @@ -401,8 +389,6 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { // will mostly be relevant for inbound channels. let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; - user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = - config.anchor_channels_config.is_some(); user_config.reject_inbound_splices = false; if may_announce_channel(config).is_err() { diff --git a/src/event.rs b/src/event.rs index 93d274ff7f..1013b87831 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1257,24 +1257,6 @@ where } let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); - if anchor_channel && self.config.anchor_channels_config.is_none() { - log_error!( - self.logger, - "Rejecting inbound channel from peer {} due to Anchor channels being disabled.", - counterparty_node_id, - ); - self.channel_manager - .force_close_broadcasting_latest_txn( - &temporary_channel_id, - &counterparty_node_id, - "Channel request rejected".to_string(), - ) - .unwrap_or_else(|e| { - log_error!(self.logger, "Failed to reject channel: {:?}", e) - }); - return Ok(()); - } - let required_reserve_sats = crate::new_channel_anchor_reserve_sats( &self.config, &counterparty_node_id, @@ -1705,19 +1687,17 @@ where .. } => { // Skip bumping channel closes if our counterparty is trusted. - if let Some(anchor_channels_config) = - self.config.anchor_channels_config.as_ref() + if self + .config + .anchor_channels_config + .trusted_peers_no_reserve + .contains(counterparty_node_id) { - if anchor_channels_config - .trusted_peers_no_reserve - .contains(counterparty_node_id) - { - log_debug!(self.logger, - "Ignoring BumpTransactionEvent::ChannelClose for channel {} due to trusted counterparty {}", - channel_id, counterparty_node_id - ); - return Ok(()); - } + log_debug!(self.logger, + "Ignoring BumpTransactionEvent::ChannelClose for channel {} due to trusted counterparty {}", + channel_id, counterparty_node_id + ); + return Ok(()); } }, BumpTransactionEvent::HTLCResolution { .. } => {}, diff --git a/src/lib.rs b/src/lib.rs index a5f2fa5375..4bae85c646 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1150,7 +1150,7 @@ impl Node { self.channel_manager .list_channels() .into_iter() - .map(|c| ChannelDetails::from_ldk(c, self.config.anchor_channels_config.as_ref())) + .map(|c| ChannelDetails::from_ldk(c, &self.config.anchor_channels_config)) .collect() } @@ -2397,21 +2397,20 @@ impl_writeable_tlv_based!(NodeMetrics, { pub(crate) fn total_anchor_channels_reserve_sats( channel_manager: &ChannelManager, config: &Config, ) -> u64 { - config.anchor_channels_config.as_ref().map_or(0, |anchor_channels_config| { - channel_manager - .list_channels() - .into_iter() - .filter(|c| { - !anchor_channels_config.trusted_peers_no_reserve.contains(&c.counterparty.node_id) - && c.channel_shutdown_state - .map_or(true, |s| s != ChannelShutdownState::ShutdownComplete) - && c.channel_type - .as_ref() - .map_or(false, |t| t.requires_anchors_zero_fee_htlc_tx()) - }) - .count() as u64 - * anchor_channels_config.per_channel_reserve_sats - }) + channel_manager + .list_channels() + .into_iter() + .filter(|c| { + !config + .anchor_channels_config + .trusted_peers_no_reserve + .contains(&c.counterparty.node_id) + && c.channel_shutdown_state + .map_or(true, |s| s != ChannelShutdownState::ShutdownComplete) + && c.channel_type.as_ref().map_or(false, |t| t.requires_anchors_zero_fee_htlc_tx()) + }) + .count() as u64 + * config.anchor_channels_config.per_channel_reserve_sats } pub(crate) fn new_channel_anchor_reserve_sats( @@ -2421,13 +2420,11 @@ pub(crate) fn new_channel_anchor_reserve_sats( return 0; } - config.anchor_channels_config.as_ref().map_or(0, |c| { - if c.trusted_peers_no_reserve.contains(peer_node_id) { - 0 - } else { - c.per_channel_reserve_sats - } - }) + if config.anchor_channels_config.trusted_peers_no_reserve.contains(peer_node_id) { + 0 + } else { + config.anchor_channels_config.per_channel_reserve_sats + } } #[cfg(test)] diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 524157a671..ca70cd8d8e 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -453,15 +453,17 @@ where let spendable_amount_sats = self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); let required_funds_sats = channel_amount_sats - + self.config.anchor_channels_config.as_ref().map_or(0, |c| { - if init_features.supports_anchors_zero_fee_htlc_tx() - && !c.trusted_peers_no_reserve.contains(&their_network_key) - { - c.per_channel_reserve_sats - } else { - 0 - } - }); + + if init_features.supports_anchors_zero_fee_htlc_tx() + && !self + .config + .anchor_channels_config + .trusted_peers_no_reserve + .contains(&their_network_key) + { + self.config.anchor_channels_config.per_channel_reserve_sats + } else { + 0 + }; if spendable_amount_sats < required_funds_sats { log_error!(self.logger, "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", diff --git a/src/types.rs b/src/types.rs index e24db4d253..2ac08a48f8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -646,24 +646,16 @@ pub struct ChannelDetails { impl ChannelDetails { pub(crate) fn from_ldk( - value: LdkChannelDetails, anchor_channels_config: Option<&AnchorChannelsConfig>, + value: LdkChannelDetails, anchor_channels_config: &AnchorChannelsConfig, ) -> Self { let reserve_type = value.channel_type.as_ref().map(|channel_type| { if channel_type.supports_anchors_zero_fee_htlc_tx() { - if let Some(config) = anchor_channels_config { - if config.trusted_peers_no_reserve.contains(&value.counterparty.node_id) { - ReserveType::TrustedPeersNoReserve - } else { - ReserveType::Adaptive - } + if anchor_channels_config + .trusted_peers_no_reserve + .contains(&value.counterparty.node_id) + { + ReserveType::TrustedPeersNoReserve } else { - // Edge case: if `AnchorChannelsConfig` was previously set and later - // removed, we can no longer distinguish whether this anchor channel's - // reserve was `Adaptive` or `TrustedPeersNoReserve`. We default to - // `Adaptive` here, which may incorrectly override a prior - // `TrustedPeersNoReserve` designation. This is acceptable since - // unsetting `AnchorChannelsConfig` on a node with existing anchor - // channels is not an expected operation. ReserveType::Adaptive } } else { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a56d46e056..c18244dcf0 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -377,13 +377,9 @@ pub(crate) fn random_node_alias() -> Option { Some(NodeAlias(bytes)) } -pub(crate) fn random_config(anchor_channels: bool) -> TestConfig { +pub(crate) fn random_config() -> TestConfig { let mut node_config = Config::default(); - if !anchor_channels { - node_config.anchor_channels_config = None; - } - node_config.network = Network::Regtest; println!("Setting network: {}", node_config.network); @@ -477,24 +473,22 @@ pub(crate) use setup_builder; pub(crate) mod scenarios; pub(crate) fn setup_two_nodes( - chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, - anchors_trusted_no_reserve: bool, + chain_source: &TestChainSource, allow_0conf: bool, anchors_trusted_no_reserve: bool, ) -> (TestNode, TestNode) { setup_two_nodes_with_store( chain_source, allow_0conf, - anchor_channels, anchors_trusted_no_reserve, TestStoreType::TestSyncStore, ) } pub(crate) fn setup_two_nodes_with_store( - chain_source: &TestChainSource, allow_0conf: bool, anchor_channels: bool, - anchors_trusted_no_reserve: bool, store_type: TestStoreType, + chain_source: &TestChainSource, allow_0conf: bool, anchors_trusted_no_reserve: bool, + store_type: TestStoreType, ) -> (TestNode, TestNode) { println!("== Node A =="); - let mut config_a = random_config(anchor_channels); + let mut config_a = random_config(); config_a.store_type = store_type; if cfg!(hrn_tests) { @@ -505,7 +499,7 @@ pub(crate) fn setup_two_nodes_with_store( let node_a = setup_node(chain_source, config_a); println!("\n== Node B =="); - let mut config_b = random_config(anchor_channels); + let mut config_b = random_config(); config_b.store_type = store_type; if cfg!(hrn_tests) { @@ -520,14 +514,8 @@ pub(crate) fn setup_two_nodes_with_store( if allow_0conf { config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); } - if anchor_channels && anchors_trusted_no_reserve { - config_b - .node_config - .anchor_channels_config - .as_mut() - .unwrap() - .trusted_peers_no_reserve - .push(node_a.node_id()); + if anchors_trusted_no_reserve { + config_b.node_config.anchor_channels_config.trusted_peers_no_reserve.push(node_a.node_id()); } let node_b = setup_node(chain_source, config_b); (node_a, node_b) @@ -1026,7 +1014,8 @@ pub(crate) async fn do_channel_full_cycle( let node_b_anchor_reserve_sat = if node_b .config() .anchor_channels_config - .map_or(true, |acc| acc.trusted_peers_no_reserve.contains(&node_a.node_id())) + .trusted_peers_no_reserve + .contains(&node_a.node_id()) { 0 } else { diff --git a/tests/common/scenarios/mod.rs b/tests/common/scenarios/mod.rs index 7cbf56b8e1..ffbfc2b007 100644 --- a/tests/common/scenarios/mod.rs +++ b/tests/common/scenarios/mod.rs @@ -90,7 +90,7 @@ pub(crate) async fn wait_for_htlcs_settled( /// Build a fresh LDK node configured for interop tests. Uses electrum at the /// docker-compose default port and bumps sync timeouts for combo stress. pub(crate) fn setup_ldk_node() -> Node { - let config = crate::common::random_config(true); + let config = crate::common::random_config(); let mut builder = ldk_node::Builder::from_config(config.node_config); let mut sync_config = ldk_node::config::ElectrumSyncConfig::default(); sync_config.timeouts_config.onchain_wallet_sync_timeout_secs = 180; diff --git a/tests/integration_tests_hrn.rs b/tests/integration_tests_hrn.rs index 9102400398..6e758105a2 100644 --- a/tests/integration_tests_hrn.rs +++ b/tests/integration_tests_hrn.rs @@ -24,7 +24,7 @@ async fn unified_send_to_hrn() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; diff --git a/tests/integration_tests_migration.rs b/tests/integration_tests_migration.rs index ee5ad26c8e..01887501c6 100644 --- a/tests/integration_tests_migration.rs +++ b/tests/integration_tests_migration.rs @@ -148,7 +148,7 @@ async fn migrate_node_across_all_backends() { let connection_string = test_connection_string(); // Set up node B, the Lightning counterparty. - let config_b = common::random_config(false); + let config_b = common::random_config(); let node_b_instance = BackendInstance::new( MigrationBackend::Postgres, &config_b.node_config.storage_dir_path, @@ -167,7 +167,7 @@ async fn migrate_node_across_all_backends() { // Spin up the node we'll migrate on the first backend. The same node config (storage dir, // listening addresses, identity) is reused across every hop — only the backend changes — so // each backend's store lives in its own subdirectory of the one storage dir. - let config = common::random_config(false); + let config = common::random_config(); let node_entropy = config.node_entropy; let node_config = config.node_config; let base_dir = node_config.storage_dir_path.clone(); diff --git a/tests/integration_tests_postgres.rs b/tests/integration_tests_postgres.rs index 0c93c705c2..889d681ba4 100644 --- a/tests/integration_tests_postgres.rs +++ b/tests/integration_tests_postgres.rs @@ -22,7 +22,7 @@ async fn channel_full_cycle_with_postgres_store() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); println!("== Node A =="); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let config_a = common::random_config(true); + let config_a = common::random_config(); let mut builder_a = Builder::from_config(config_a.node_config); builder_a.set_chain_source_esplora(esplora_url.clone(), None); let node_a = builder_a @@ -37,7 +37,7 @@ async fn channel_full_cycle_with_postgres_store() { node_a.start().unwrap(); println!("\n== Node B =="); - let config_b = common::random_config(true); + let config_b = common::random_config(); let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 420a2874e3..1c476fa03c 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -79,7 +79,7 @@ async fn wait_for_classified_funding_payment(node: &Node, funding_txid: Txid) { async fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); do_channel_full_cycle( node_a, node_b, @@ -97,7 +97,7 @@ async fn channel_full_cycle() { async fn channel_full_cycle_force_close() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); do_channel_full_cycle( node_a, node_b, @@ -115,7 +115,7 @@ async fn channel_full_cycle_force_close() { async fn channel_full_cycle_force_close_trusted_no_reserve() { 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, true, true); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true); do_channel_full_cycle( node_a, node_b, @@ -133,7 +133,7 @@ async fn channel_full_cycle_force_close_trusted_no_reserve() { async fn channel_full_cycle_0conf() { 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, true, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, true, false); do_channel_full_cycle( node_a, node_b, @@ -147,29 +147,11 @@ async fn channel_full_cycle_0conf() { .await; } -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn channel_full_cycle_legacy_staticremotekey() { - 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, false); - do_channel_full_cycle( - node_a, - node_b, - &bitcoind.client, - &electrsd.client, - false, - false, - false, - false, - ) - .await; -} - #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_0reserve() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); do_channel_full_cycle( node_a, node_b, @@ -187,7 +169,7 @@ async fn channel_full_cycle_0reserve() { async fn channel_full_cycle_0conf_0reserve() { 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, true, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, true, false); do_channel_full_cycle( node_a, node_b, @@ -205,7 +187,7 @@ async fn channel_full_cycle_0conf_0reserve() { async fn channel_open_fails_when_funds_insufficient() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -245,7 +227,7 @@ async fn multi_hop_sending() { // Setup and fund 5 nodes let mut nodes = Vec::new(); for _ in 0..5 { - let config = random_config(true); + let config = random_config(); let mut sync_config = EsploraSyncConfig::default(); sync_config.background_sync_config = None; setup_builder!(builder, config.node_config); @@ -340,8 +322,8 @@ async fn multi_hop_sending() { async fn split_underpaid_bolt11_payment() { 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, true, false); - let node_c = setup_node(&chain_source, random_config(true)); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); + let node_c = setup_node(&chain_source, random_config()); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -434,7 +416,7 @@ async fn split_underpaid_bolt11_payment() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn start_stop_reinit() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(true); + let config = random_config(); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); @@ -507,7 +489,7 @@ async fn start_stop_reinit() { async fn onchain_send_receive() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -708,7 +690,7 @@ async fn onchain_send_receive() { async fn reorged_onchain_payment_returns_to_unconfirmed() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -776,7 +758,7 @@ async fn reorged_onchain_payment_returns_to_unconfirmed() { async fn onchain_send_all_retains_reserve() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); // Setup nodes let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -862,7 +844,7 @@ async fn onchain_wallet_recovery() { let chain_source = random_chain_source(&bitcoind, &electrsd); - let original_config = random_config(true); + let original_config = random_config(); let original_node_entropy = original_config.node_entropy; let original_node = setup_node(&chain_source, original_config); @@ -903,7 +885,7 @@ async fn onchain_wallet_recovery() { drop(original_node); // Now we start from scratch, only the seed remains the same. - let mut recovered_config = random_config(true); + let mut recovered_config = random_config(); recovered_config.node_entropy = original_node_entropy; recovered_config.wallet_rescan_from_height = Some(0); let recovered_node = setup_node(&chain_source, recovered_config); @@ -945,7 +927,7 @@ async fn onchain_wallet_force_full_scan_rediscovers_esplora_funds() { premine_blocks(&bitcoind.client, &electrsd.client).await; - let address_source_config = random_config(true); + let address_source_config = random_config(); let node_entropy = address_source_config.node_entropy; let address_source_node = setup_node(&chain_source, address_source_config); let addr_1 = address_source_node.onchain_payment().new_address().unwrap(); @@ -954,7 +936,7 @@ async fn onchain_wallet_force_full_scan_rediscovers_esplora_funds() { drop(address_source_node); let premine_amount_sat = 100_000; - let mut stale_config = random_config(true); + let mut stale_config = random_config(); stale_config.node_entropy = node_entropy; stale_config.store_type = TestStoreType::Sqlite; let stale_node = setup_node(&chain_source, stale_config.clone()); @@ -1024,7 +1006,7 @@ async fn onchain_wallet_recovery_rescans_from_birthday_height() { premine_blocks(&bitcoind.client, &electrsd.client).await; // Step 1: bring up an "original" node at the birthday height and generate addresses. - let original_config = random_config(true); + let original_config = random_config(); let original_node_entropy = original_config.node_entropy; let original_node = setup_node(&chain_source, original_config); @@ -1070,7 +1052,7 @@ async fn onchain_wallet_recovery_rescans_from_birthday_height() { // Step 5: restart a fresh node with only the seed and no rescan height. It must NOT see // the funds, because its wallet birthday sits above the funding transactions. - let mut pinned_config = random_config(true); + let mut pinned_config = random_config(); pinned_config.node_entropy = original_node_entropy; let pinned_node = setup_node(&chain_source, pinned_config); pinned_node.sync_wallets().unwrap(); @@ -1084,7 +1066,7 @@ async fn onchain_wallet_recovery_rescans_from_birthday_height() { // Step 6: restart with a rescan height set to the birthday height. Funds must be // re-discovered. - let mut recovered_config = random_config(true); + let mut recovered_config = random_config(); recovered_config.node_entropy = original_node_entropy; recovered_config.wallet_rescan_from_height = Some(birthday_height); let recovered_node = setup_node(&chain_source, recovered_config); @@ -1107,7 +1089,7 @@ async fn build_fails_when_wallet_rescan_height_is_above_tip() { .try_into() .unwrap(); - let config = random_config(false); + let config = random_config(); let entropy = config.node_entropy; setup_builder!(builder, config.node_config); @@ -1134,7 +1116,7 @@ async fn build_aborts_on_first_startup_bitcoind_tip_fetch_failure() { // A fresh node pointed at an unreachable bitcoind RPC endpoint must not silently // fall back to genesis as the wallet birthday. The build must abort cleanly so the // misconfiguration surfaces immediately. - let config = random_config(false); + let config = random_config(); let entropy = config.node_entropy; setup_builder!(builder, config.node_config); @@ -1180,17 +1162,16 @@ async fn run_rbf_test(is_insert_block: bool) { let chain_source_esplora = TestChainSource::Esplora(&electrsd); macro_rules! config_node { - ($chain_source:expr, $anchor_channels:expr) => {{ - let config_a = random_config($anchor_channels); + ($chain_source:expr) => {{ + let config_a = random_config(); let node = setup_node(&$chain_source, config_a); node }}; } - let anchor_channels = false; let nodes = vec![ - config_node!(chain_source_electrsd, anchor_channels), - config_node!(chain_source_bitcoind, anchor_channels), - config_node!(chain_source_esplora, anchor_channels), + config_node!(chain_source_electrsd), + config_node!(chain_source_bitcoind), + config_node!(chain_source_esplora), ]; let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); @@ -1299,7 +1280,7 @@ async fn run_rbf_test(is_insert_block: bool) { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn sign_verify_msg() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(true); + let config = random_config(); let chain_source = random_chain_source(&bitcoind, &electrsd); let node = setup_node(&chain_source, config); @@ -1314,7 +1295,7 @@ async fn sign_verify_msg() { async fn connection_multi_listen() { 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, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let node_id_b = node_b.node_id(); @@ -1334,7 +1315,7 @@ async fn connection_restart_behavior() { async fn do_connection_restart_behavior(persist: 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, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let node_id_a = node_a.node_id(); let node_id_b = node_b.node_id(); @@ -1381,7 +1362,7 @@ async fn do_connection_restart_behavior(persist: bool) { async fn concurrent_connections_succeed() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let node_a = Arc::new(node_a); let node_b = Arc::new(node_b); @@ -1409,7 +1390,7 @@ async fn splice_channel() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let address_b = node_b.onchain_payment().new_address().unwrap(); @@ -1624,7 +1605,7 @@ async fn run_rbf_splice_channel_test(confirm_original: bool) { let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf).unwrap(); let chain_source = random_chain_source(&bitcoind, &electrsd); - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let address_b = node_b.onchain_payment().new_address().unwrap(); @@ -1822,7 +1803,7 @@ async fn run_rbf_splice_channel_test(confirm_original: bool) { async fn funding_payment_graduates_without_channel_ready() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let address_b = node_b.onchain_payment().new_address().unwrap(); @@ -1877,7 +1858,7 @@ async fn funding_payment_graduates_without_channel_ready() { async fn splice_payment_reorged_to_unconfirmed() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let address_b = node_b.onchain_payment().new_address().unwrap(); @@ -1954,7 +1935,7 @@ async fn splice_payment_reorged_to_unconfirmed() { async fn splice_in_rbf_joins_counterparty_splice() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let address_b = node_b.onchain_payment().new_address().unwrap(); @@ -2003,7 +1984,7 @@ async fn splice_in_rbf_joins_counterparty_splice() { async fn simple_bolt12_send_receive() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_amount_sat = 5_000_000; @@ -2245,7 +2226,7 @@ async fn async_payment() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = random_chain_source(&bitcoind, &electrsd); - let mut config_sender = random_config(true); + let mut config_sender = random_config(); config_sender.node_config.listening_addresses = None; config_sender.node_config.node_alias = None; config_sender.log_writer = @@ -2253,20 +2234,20 @@ async fn async_payment() { config_sender.async_payments_role = Some(AsyncPaymentsRole::Client); let node_sender = setup_node(&chain_source, config_sender); - let mut config_sender_lsp = random_config(true); + let mut config_sender_lsp = random_config(); config_sender_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); config_sender_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); let node_sender_lsp = setup_node(&chain_source, config_sender_lsp); - let mut config_receiver_lsp = random_config(true); + let mut config_receiver_lsp = random_config(); config_receiver_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); config_receiver_lsp.async_payments_role = Some(AsyncPaymentsRole::Server); let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp); - let mut config_receiver = random_config(true); + let mut config_receiver = random_config(); config_receiver.node_config.listening_addresses = None; config_receiver.node_config.node_alias = None; config_receiver.log_writer = @@ -2378,7 +2359,7 @@ async fn test_node_announcement_propagation() { let chain_source = random_chain_source(&bitcoind, &electrsd); // Node A will use both listening and announcement addresses - let mut config_a = random_config(true); + let mut config_a = random_config(); let node_a_alias_string = "ldk-node-a".to_string(); let mut node_a_alias_bytes = [0u8; 32]; node_a_alias_bytes[..node_a_alias_string.as_bytes().len()] @@ -2390,7 +2371,7 @@ async fn test_node_announcement_propagation() { config_a.node_config.announcement_addresses = Some(node_a_announcement_addresses.clone()); // Node B will only use listening addresses - let mut config_b = random_config(true); + let mut config_b = random_config(); let node_b_alias_string = "ldk-node-b".to_string(); let mut node_b_alias_bytes = [0u8; 32]; node_b_alias_bytes[..node_b_alias_string.as_bytes().len()] @@ -2475,7 +2456,7 @@ async fn generate_bip21_uri() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -2529,7 +2510,7 @@ async fn generate_bip21_uri() { async fn unified_receive_rejects_msat_overflow() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = random_chain_source(&bitcoind, &electrsd); - let node = setup_node(&chain_source, random_config(true)); + let node = setup_node(&chain_source, random_config()); assert_eq!( Err(NodeError::InvalidAmount), @@ -2542,7 +2523,7 @@ async fn unified_send_receive_bip21_uri() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; @@ -2679,7 +2660,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { disable_client_reserve: false, }; - let service_config = random_config(true); + let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.enable_liquidity_provider(lsps2_service_config); @@ -2689,14 +2670,14 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - let client_config = random_config(true); + let client_config = random_config(); setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.add_liquidity_source(service_node_id, service_addr, None, true); let client_node = client_builder.build(client_config.node_entropy.into()).unwrap(); client_node.start().unwrap(); - let payer_config = random_config(true); + let payer_config = random_config(); setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); @@ -2903,13 +2884,9 @@ async fn lsps2_rejects_jit_channel_without_anchor_reserve() { }; let service_logger = Arc::new(MockLogFacadeLogger::new()); - let service_config = random_config(true); - let anchor_reserve_sats = service_config - .node_config - .anchor_channels_config - .as_ref() - .unwrap() - .per_channel_reserve_sats; + let service_config = random_config(); + let anchor_reserve_sats = + service_config.node_config.anchor_channels_config.per_channel_reserve_sats; setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.set_custom_logger(service_logger.clone()); @@ -2919,7 +2896,7 @@ async fn lsps2_rejects_jit_channel_without_anchor_reserve() { let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - let client_config = random_config(true); + let client_config = random_config(); setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.add_liquidity_source(service_node_id, service_addr, None, true); @@ -2927,7 +2904,7 @@ async fn lsps2_rejects_jit_channel_without_anchor_reserve() { client_node.start().unwrap(); let client_node_id = client_node.node_id(); - let payer_config = random_config(true); + let payer_config = random_config(); setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); @@ -3043,7 +3020,7 @@ async fn facade_logging() { let chain_source = random_chain_source(&bitcoind, &electrsd); let logger = init_log_logger(LevelFilter::Trace); - let mut config = random_config(false); + let mut config = random_config(); config.log_writer = TestLogWriter::LogFacade; println!("== Facade logging starts =="); @@ -3059,7 +3036,7 @@ async fn facade_logging() { async fn spontaneous_send_with_custom_preimage() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let address_a = node_a.onchain_payment().new_address().unwrap(); let premine_sat = 1_000_000; @@ -3126,7 +3103,7 @@ async fn spontaneous_send_with_custom_preimage() { async fn drop_in_async_context() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = random_chain_source(&bitcoind, &electrsd); - let config = random_config(true); + let config = random_config(); let node = setup_node(&chain_source, config); node.stop().unwrap(); } @@ -3157,7 +3134,7 @@ async fn lsps2_client_trusts_lsp() { disable_client_reserve: false, }; - let service_config = random_config(true); + let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.enable_liquidity_provider(lsps2_service_config); @@ -3166,7 +3143,7 @@ async fn lsps2_client_trusts_lsp() { let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - let client_config = random_config(true); + let client_config = random_config(); setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); @@ -3174,7 +3151,7 @@ async fn lsps2_client_trusts_lsp() { client_node.start().unwrap(); let client_node_id = client_node.node_id(); - let payer_config = random_config(true); + let payer_config = random_config(); setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); @@ -3332,7 +3309,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { disable_client_reserve: false, }; - let service_config = random_config(true); + let service_config = random_config(); setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.enable_liquidity_provider(lsps2_service_config); @@ -3342,7 +3319,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { let service_node_id = service_node.node_id(); let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); - let client_config = random_config(true); + let client_config = random_config(); setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.add_liquidity_source(service_node_id, service_addr.clone(), None, true); @@ -3351,7 +3328,7 @@ async fn lsps2_lsp_trusts_client_but_client_does_not_claim() { let client_node_id = client_node.node_id(); - let payer_config = random_config(true); + let payer_config = random_config(); setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let payer_node = payer_builder.build(payer_config.node_entropy.into()).unwrap(); @@ -3442,7 +3419,7 @@ async fn payment_persistence_after_restart() { // Setup nodes manually so we can restart node_a with the same config println!("== Node A =="); - let mut config_a = random_config(true); + let mut config_a = random_config(); config_a.store_type = TestStoreType::Sqlite; let num_payments = 200; @@ -3452,7 +3429,7 @@ async fn payment_persistence_after_restart() { let node_a = setup_node(&chain_source, config_a.clone()); println!("\n== Node B =="); - let config_b = random_config(true); + let config_b = random_config(); let node_b = setup_node(&chain_source, config_b); let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -3734,7 +3711,7 @@ async fn fs_store_persistence_backwards_compatibility() { async fn onchain_fee_bump_rbf() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); // Fund both nodes let addr_a = node_a.onchain_payment().new_address().unwrap(); @@ -3876,7 +3853,7 @@ async fn onchain_fee_bump_rbf() { async fn onchain_fee_bump_rbf_respects_anchor_reserve() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -3925,7 +3902,7 @@ async fn onchain_fee_bump_rbf_respects_anchor_reserve() { async fn open_channel_with_all_with_anchors() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -4046,7 +4023,7 @@ async fn open_channel_variants_reserve_funds_for_anchor_peers() { let mut addresses = Vec::new(); let mut exact_cases = Vec::new(); for variant in exact_variants { - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); addresses.push(node_a.onchain_payment().new_address().unwrap()); addresses.push(node_b.onchain_payment().new_address().unwrap()); exact_cases.push((variant, node_a, node_b)); @@ -4054,7 +4031,7 @@ async fn open_channel_variants_reserve_funds_for_anchor_peers() { let mut with_all_cases = Vec::new(); for variant in with_all_variants { - let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); addresses.push(node_a.onchain_payment().new_address().unwrap()); addresses.push(node_b.onchain_payment().new_address().unwrap()); with_all_cases.push((variant, node_a, node_b)); @@ -4129,62 +4106,11 @@ async fn open_channel_variants_reserve_funds_for_anchor_peers() { } } -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn open_channel_with_all_without_anchors() { - 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, false); - - let addr_a = node_a.onchain_payment().new_address().unwrap(); - let addr_b = node_b.onchain_payment().new_address().unwrap(); - - let premine_amount_sat = 1_000_000; - - premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_a, addr_b], - Amount::from_sat(premine_amount_sat), - ) - .await; - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); - - let funding_txo = open_channel_with_all(&node_a, &node_b, false, &electrsd).await; - - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; - - node_a.sync_wallets().unwrap(); - node_b.sync_wallets().unwrap(); - - let _user_channel_id_a = expect_channel_ready_event!(node_a, node_b.node_id()); - let _user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); - - // Without anchors, there should be no remaining balance - let remaining_balance = node_a.list_balances().spendable_onchain_balance_sats; - assert_eq!( - remaining_balance, 0, - "Remaining balance {remaining_balance} should be zero without anchor reserve" - ); - - // Verify a channel was opened with all the funds accounting for fees - let channels = node_a.list_channels(); - assert_eq!(channels.len(), 1); - let channel = &channels[0]; - assert!(channel.channel_value_sats > premine_amount_sat - 500); - assert_eq!(channel.counterparty.node_id, node_b.node_id()); - assert_eq!(channel.funding_txo.unwrap(), funding_txo); - - node_a.stop().unwrap(); - node_b.stop().unwrap(); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn splice_in_with_all_balance() { 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, true, false); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); @@ -4285,7 +4211,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { client_trusts_lsp: true, disable_client_reserve: false, }; - let cheap_node_config = random_config(true); + let cheap_node_config = random_config(); setup_builder!(cheap_builder, cheap_node_config.node_config); cheap_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); cheap_builder.enable_liquidity_provider(cheap_cfg); @@ -4308,7 +4234,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { client_trusts_lsp: true, disable_client_reserve: false, }; - let expensive_node_config = random_config(true); + let expensive_node_config = random_config(); setup_builder!(expensive_builder, expensive_node_config.node_config); expensive_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); expensive_builder.enable_liquidity_provider(expensive_cfg); @@ -4318,7 +4244,7 @@ async fn do_lsps2_multi_lsp_picks_cheapest(reverse_order: bool) { let expensive_addr = expensive.listening_addresses().unwrap().first().unwrap().clone(); // Client knows both LSPs. Registration order is varied to confirm selection isn't order-based. - let client_config = random_config(true); + let client_config = random_config(); setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); if reverse_order { diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 210e9a8b25..f0838585f7 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -20,7 +20,7 @@ async fn channel_full_cycle_with_vss_store() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); println!("== Node A =="); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let config_a = common::random_config(true); + let config_a = common::random_config(); let mut builder_a = Builder::from_config(config_a.node_config); builder_a.set_chain_source_esplora(esplora_url.clone(), None); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); @@ -35,7 +35,7 @@ async fn channel_full_cycle_with_vss_store() { node_a.start().unwrap(); println!("\n== Node B =="); - let config_b = common::random_config(true); + let config_b = common::random_config(); let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 295d9fdd24..e44d50cb07 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -29,17 +29,16 @@ proptest! { let chain_source_c = random_chain_source(&bitcoind, &electrsd); macro_rules! config_node { - ($chain_source: expr, $anchor_channels: expr) => {{ - let config_a = random_config($anchor_channels); + ($chain_source: expr) => {{ + let config_a = random_config(); let node = setup_node(&$chain_source, config_a); node }}; } - let anchor_channels = true; let nodes = vec![ - config_node!(chain_source_a, anchor_channels), - config_node!(chain_source_b, anchor_channels), - config_node!(chain_source_c, anchor_channels), + config_node!(chain_source_a), + config_node!(chain_source_b), + config_node!(chain_source_c), ]; let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); From f752a315c6955597ce3266ea2c43db3a0c365a66 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 2 Jul 2026 05:17:31 +0000 Subject: [PATCH 07/23] f: documentation of anchor channels config default value --- src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index 96b9c11c7b..38887b9bf3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -126,7 +126,7 @@ pub(crate) const LNURL_AUTH_TIMEOUT_SECS: u64 = 15; /// | `node_alias` | None | /// | `trusted_peers_0conf` | [] | /// | `probing_liquidity_limit_multiplier` | 3 | -/// | `anchor_channels_config` | Some(..) | +/// | `anchor_channels_config` | AnchorChannelsConfig::default() | /// | `route_parameters` | None | /// | `tor_config` | None | /// | `hrn_config` | HumanReadableNamesConfig::default() | From 96df22519ebcb19ed163942755d94502ac79184e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 28 May 2026 18:16:49 +0000 Subject: [PATCH 08/23] Use a patched blockstream-electrs in CI The patch adds support for the `broadcast_package` method added in electrum protocol v1.6. Upcoming commits will require this patch to pass CI. --- .github/workflows/benchmarks.yml | 13 ++++--- .github/workflows/hrn-integration.yml | 13 ++++--- .github/workflows/postgres-integration.yml | 13 ++++--- .github/workflows/rust.yml | 19 ++++++---- .github/workflows/vss-integration.yml | 15 ++++++++ .github/workflows/vss-no-auth-integration.yml | 15 ++++++++ scripts/build_electrs.sh | 35 +++++++++++++++++++ ...tcoind_electrs.sh => download_bitcoind.sh} | 19 ++-------- 8 files changed, 108 insertions(+), 34 deletions(-) create mode 100755 scripts/build_electrs.sh rename scripts/{download_bitcoind_electrs.sh => download_bitcoind.sh} (55%) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 4a884ab2a6..0584e321e0 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -31,13 +31,18 @@ jobs: uses: actions/cache@v4 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "(steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | diff --git a/.github/workflows/hrn-integration.yml b/.github/workflows/hrn-integration.yml index 76a95f93de..bd3e2e2d64 100644 --- a/.github/workflows/hrn-integration.yml +++ b/.github/workflows/hrn-integration.yml @@ -28,13 +28,18 @@ jobs: uses: actions/cache@v4 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true'" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | diff --git a/.github/workflows/postgres-integration.yml b/.github/workflows/postgres-integration.yml index 410136928a..3764d454b1 100644 --- a/.github/workflows/postgres-integration.yml +++ b/.github/workflows/postgres-integration.yml @@ -43,13 +43,18 @@ jobs: uses: actions/cache@v4 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-esplora_a33e97e1-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true'" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 106f2c4f95..8eea352cfb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -60,23 +60,30 @@ jobs: uses: actions/cache@v4 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "matrix.platform != 'windows-latest' && (steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "matrix.platform != 'windows-latest' && steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "matrix.platform != 'windows-latest' && steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - name: Build on Rust ${{ matrix.toolchain }} - run: cargo build --verbose --color always + run: | + cargo build --verbose --color always - name: Build with UniFFI support on Rust ${{ matrix.toolchain }} if: matrix.build-uniffi - run: cargo build --features uniffi --verbose --color always + run: | + cargo build --features uniffi --verbose --color always - name: Check release build on Rust ${{ matrix.toolchain }} run: cargo check --release --verbose --color always - name: Check release build with UniFFI support on Rust ${{ matrix.toolchain }} diff --git a/.github/workflows/vss-integration.yml b/.github/workflows/vss-integration.yml index 7ffea3dd67..24417c88f3 100644 --- a/.github/workflows/vss-integration.yml +++ b/.github/workflows/vss-integration.yml @@ -31,6 +31,21 @@ jobs: uses: actions/checkout@v6 with: path: ldk-node + - name: Enable caching for electrs + id: cache-electrs + uses: actions/cache@v5 + with: + path: bin/electrs-${{ runner.os }}-${{ runner.arch }} + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./ldk-node/scripts/build_electrs.sh + mkdir -p bin + mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} + - name: Set electrs environment variable + run: | + echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - name: Checkout VSS uses: actions/checkout@v6 with: diff --git a/.github/workflows/vss-no-auth-integration.yml b/.github/workflows/vss-no-auth-integration.yml index 8ee2fe54b9..dc3963f00e 100644 --- a/.github/workflows/vss-no-auth-integration.yml +++ b/.github/workflows/vss-no-auth-integration.yml @@ -31,6 +31,21 @@ jobs: uses: actions/checkout@v6 with: path: ldk-node + - name: Enable caching for electrs + id: cache-electrs + uses: actions/cache@v5 + with: + path: bin/electrs-${{ runner.os }}-${{ runner.arch }} + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./ldk-node/scripts/build_electrs.sh + mkdir -p bin + mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} + - name: Set electrs environment variable + run: | + echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - name: Checkout VSS uses: actions/checkout@v6 with: diff --git a/scripts/build_electrs.sh b/scripts/build_electrs.sh new file mode 100755 index 0000000000..6130ca5085 --- /dev/null +++ b/scripts/build_electrs.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -eox pipefail + +# Our Esplora-based tests require `electrs` binaries. Here, we +# download the code, build the binaries, and export their location +# via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the +# `electrsd`/`bitcoind` crates in our tests. + +HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" +ELECTRS_GIT_REPO="https://github.com/tankyleo/blockstream-electrs.git" +ELECTRS_TAG="2026-05-26-electrum-submit-package" +ELECTRS_REV="8c06d8010e43f793b1a65f83695ea846e5cd83ed" +if [[ "$HOST_PLATFORM" != *linux* && "$HOST_PLATFORM" != *darwin* ]]; then + printf "\n\n" + echo "Unsupported platform: $HOST_PLATFORM Exiting.." + exit 1 +fi + +DL_TMP_DIR=$(mktemp -d) +trap 'rm -rf -- "$DL_TMP_DIR"' EXIT + +pushd "$DL_TMP_DIR" +git clone --branch "$ELECTRS_TAG" --depth 1 "$ELECTRS_GIT_REPO" blockstream-electrs +cd blockstream-electrs +CURRENT_HEAD=$(git rev-parse HEAD) +if [ "$CURRENT_HEAD" != "$ELECTRS_REV" ]; then + echo "ERROR: HEAD does not match expected commit" + echo "expected: $ELECTRS_REV" + echo "actual: $CURRENT_HEAD" + exit 1 +fi +RUSTFLAGS="" cargo build +export ELECTRS_EXE="$DL_TMP_DIR"/blockstream-electrs/target/debug/electrs +chmod +x "$ELECTRS_EXE" +popd diff --git a/scripts/download_bitcoind_electrs.sh b/scripts/download_bitcoind.sh similarity index 55% rename from scripts/download_bitcoind_electrs.sh rename to scripts/download_bitcoind.sh index f94e280e3b..102cf826f3 100755 --- a/scripts/download_bitcoind_electrs.sh +++ b/scripts/download_bitcoind.sh @@ -1,24 +1,18 @@ #!/bin/bash set -eox pipefail -# Our Esplora-based tests require `electrs` and `bitcoind` -# binaries. Here, we download the binaries, validate them, and export their -# location via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the +# Our Esplora-based tests require `bitcoind` binaries. Here, we +# download the binaries, validate them, and export their location +# via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the # `electrsd`/`bitcoind` crates in our tests. HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" -ELECTRS_DL_ENDPOINT="https://github.com/RCasatta/electrsd/releases/download/electrs_releases" -ELECTRS_VERSION="esplora_a33e97e1a1fc63fa9c20a116bb92579bbf43b254" BITCOIND_DL_ENDPOINT="https://bitcoincore.org/bin/" BITCOIND_VERSION="29.0" if [[ "$HOST_PLATFORM" == *linux* ]]; then - ELECTRS_DL_FILE_NAME=electrs_linux_"$ELECTRS_VERSION".zip - ELECTRS_DL_HASH="865e26a96e8df77df01d96f2f569dcf9622fc87a8d99a9b8fe30861a4db9ddf1" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-linux-gnu.tar.gz BITCOIND_DL_HASH="a681e4f6ce524c338a105f214613605bac6c33d58c31dc5135bbc02bc458bb6c" elif [[ "$HOST_PLATFORM" == *darwin* ]]; then - ELECTRS_DL_FILE_NAME=electrs_macos_"$ELECTRS_VERSION".zip - ELECTRS_DL_HASH="2d5ff149e8a2482d3658e9b386830dfc40c8fbd7c175ca7cbac58240a9505bcd" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-apple-darwin.tar.gz BITCOIND_DL_HASH="5bb824fc86a15318d6a83a1b821ff4cd4b3d3d0e1ec3d162b805ccf7cae6fca8" else @@ -31,13 +25,6 @@ DL_TMP_DIR=$(mktemp -d) trap 'rm -rf -- "$DL_TMP_DIR"' EXIT pushd "$DL_TMP_DIR" -ELECTRS_DL_URL="$ELECTRS_DL_ENDPOINT"/"$ELECTRS_DL_FILE_NAME" -curl -L -o "$ELECTRS_DL_FILE_NAME" "$ELECTRS_DL_URL" -echo "$ELECTRS_DL_HASH $ELECTRS_DL_FILE_NAME"|shasum -a 256 -c -unzip "$ELECTRS_DL_FILE_NAME" -export ELECTRS_EXE="$DL_TMP_DIR"/electrs -chmod +x "$ELECTRS_EXE" - BITCOIND_DL_URL="$BITCOIND_DL_ENDPOINT"/bitcoin-core-"$BITCOIND_VERSION"/"$BITCOIND_DL_FILE_NAME" curl -L -o "$BITCOIND_DL_FILE_NAME" "$BITCOIND_DL_URL" echo "$BITCOIND_DL_HASH $BITCOIND_DL_FILE_NAME"|shasum -a 256 -c From 17d6d8486e35ed2924e6e6918b772488b5d5b939 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 2 Jul 2026 05:17:43 +0000 Subject: [PATCH 09/23] f: comments in the scripts --- scripts/build_electrs.sh | 4 ++-- scripts/download_bitcoind.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/build_electrs.sh b/scripts/build_electrs.sh index 6130ca5085..2235986c8c 100755 --- a/scripts/build_electrs.sh +++ b/scripts/build_electrs.sh @@ -3,8 +3,8 @@ set -eox pipefail # Our Esplora-based tests require `electrs` binaries. Here, we # download the code, build the binaries, and export their location -# via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the -# `electrsd`/`bitcoind` crates in our tests. +# via `ELECTRS_EXE` which will be used by the `electrsd` crates in +# our tests. HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" ELECTRS_GIT_REPO="https://github.com/tankyleo/blockstream-electrs.git" diff --git a/scripts/download_bitcoind.sh b/scripts/download_bitcoind.sh index 102cf826f3..7582329a98 100755 --- a/scripts/download_bitcoind.sh +++ b/scripts/download_bitcoind.sh @@ -3,8 +3,8 @@ set -eox pipefail # Our Esplora-based tests require `bitcoind` binaries. Here, we # download the binaries, validate them, and export their location -# via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the -# `electrsd`/`bitcoind` crates in our tests. +# via `BITCOIND_EXE` which will be used by the `bitcoind` crates +# in our tests. HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" BITCOIND_DL_ENDPOINT="https://bitcoincore.org/bin/" From 04857aaf1ef02cc5c44bc89773928ab51de69ded Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 24 Jun 2026 05:48:23 +0000 Subject: [PATCH 10/23] Switch cln lnd and eclair interop tests to esplora The mempool/electrs docker image used in those tests only supports submitpackage via the esplora interface, not the electrum interface. --- tests/common/scenarios/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/common/scenarios/mod.rs b/tests/common/scenarios/mod.rs index ffbfc2b007..15d02409e2 100644 --- a/tests/common/scenarios/mod.rs +++ b/tests/common/scenarios/mod.rs @@ -92,10 +92,10 @@ pub(crate) async fn wait_for_htlcs_settled( pub(crate) fn setup_ldk_node() -> Node { let config = crate::common::random_config(); let mut builder = ldk_node::Builder::from_config(config.node_config); - let mut sync_config = ldk_node::config::ElectrumSyncConfig::default(); + let mut sync_config = ldk_node::config::EsploraSyncConfig::default(); sync_config.timeouts_config.onchain_wallet_sync_timeout_secs = 180; sync_config.timeouts_config.lightning_wallet_sync_timeout_secs = 120; - builder.set_chain_source_electrum("tcp://127.0.0.1:50001".to_string(), Some(sync_config)); + builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), Some(sync_config)); let node = builder.build(config.node_entropy).unwrap(); node.start().unwrap(); node From 4e0e4e59fcc97b0bf999ec69790f3b0ddaa6fe01 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 24 Jun 2026 05:48:54 +0000 Subject: [PATCH 11/23] Bump Bitcoin Core version used in kotlin and python tests We bump the Bitcoin Core version used in kotlin and python tests to support ephemeral dust. This is required for 0FC channels. --- tests/docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index e71fd70fba..5459e8eda7 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: bitcoin: - image: blockstream/bitcoind:27.2 + image: blockstream/bitcoind:29.1 platform: linux/amd64 command: [ From b8ebaac27720ee9b9c0995f746095fed29dd0163 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 28 May 2026 01:28:46 +0000 Subject: [PATCH 12/23] Add configuration knob to enable 0FC channels In upcoming commits we will read this knob to determine whether to negotiate 0FC channels. For now, we make a best-effort attempt to make sure the configured chain source supports 0FC channels if this knob is set. Do this roundtrip at the same time we make a roundtrip to retrieve the feerates to keep startup as fast as possible. --- bindings/ldk_node.udl | 1 + src/chain/bitcoind.rs | 49 +++++++++++++++++++++++++++++++++++++++++ src/chain/electrum.rs | 41 ++++++++++++++++++++++++++++++++++ src/chain/esplora.rs | 25 +++++++++++++++++++++ src/chain/mod.rs | 51 +++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 19 +++++++++++----- src/error.rs | 5 +++++ src/lib.rs | 15 +++++++++++-- 8 files changed, 199 insertions(+), 7 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 7c0edc5359..46814f6d2d 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -231,6 +231,7 @@ enum NodeError { "LnurlAuthFailed", "LnurlAuthTimeout", "InvalidLnurl", + "ChainSourceNotSupported", }; typedef dictionary NodeStatus; diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 6bfa8ffd27..0899d8dcac 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -119,6 +119,30 @@ impl BitcoindChainSource { self.api_client.utxo_source() } + pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + let node_version_result = tokio::time::timeout( + Duration::from_secs(CHAIN_POLLING_TIMEOUT_SECS), + self.api_client.get_node_version(), + ) + .await + .map_err(|e| { + log_error!(self.logger, "Failed to get node version: {:?}", e); + Error::ConnectionFailed + })?; + + let node_version = node_version_result.map_err(|e| { + log_error!(self.logger, "Failed to get node version: {:?}", e); + Error::ConnectionFailed + })?; + + // v26 first shipped the `submitpackage` RPC, but we need v29 to relay ephemeral dust + if node_version < 290000 { + log_error!(self.logger, "Bitcoin backend MUST be greater than or equal to v29"); + return Err(Error::ChainSourceNotSupported); + } + Ok(()) + } + pub(super) async fn continuously_sync_wallets( &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, onchain_wallet: Arc, channel_manager: Arc, @@ -748,6 +772,31 @@ impl BitcoindClient { } } + pub(crate) async fn get_node_version(&self) -> Result { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_node_version_inner(Arc::clone(rpc_client)) + .await + .map_err(BitcoindClientError::Rpc) + }, + BitcoindClient::Rest { rpc_client, .. } => { + // Bitcoin Core's REST interface does not support `getnetworkinfo` + // so we use the RPC client. + Self::get_node_version_inner(Arc::clone(rpc_client)) + .await + .map_err(BitcoindClientError::Rpc) + }, + } + } + + async fn get_node_version_inner(rpc_client: Arc) -> Result { + rpc_client.call_method::("getnetworkinfo", &[]).await.and_then(|value| { + value["version"].as_u64().ok_or(RpcClientError::InvalidData(String::from( + "The version field in the `getnetworkinfo` response should be a u64", + ))) + }) + } + /// Broadcasts the provided transaction. pub(crate) async fn broadcast_transaction( &self, tx: &Transaction, diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 23c930d983..c4564a60d2 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -303,6 +303,47 @@ impl ElectrumChainSource { Ok(()) } + pub(crate) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + let electrum_client: Arc = if let Some(client) = + self.electrum_runtime_status.read().expect("lock").client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before checking submitpackage support" + ); + return Err(Error::ConnectionFailed); + }; + + // TODO: Use `protocol_version` API once shipped in + // https://github.com/bitcoindevkit/rust-electrum-client/pull/213. + // + // This could still accept an Electrum server running against Bitcoin Core v26 + // through v28, which does not relay ephemeral dust. + electrum_client + .electrum_client + .transaction_broadcast_package(&super::dummy_package()) + .map_err(|e| { + if let electrum_client::Error::AllAttemptsErrored(_) = e { + log_error!( + self.logger, + "Electrum server does not support submitpackage: {:?}", + e + ); + Error::ChainSourceNotSupported + } else { + log_error!( + self.logger, + "Failed to check support for submitpackage on the Electrum server: {}", + e + ); + Error::ConnectionFailed + } + })?; + Ok(()) + } + pub(crate) async fn process_broadcast_package(&self, package: Vec) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 0754986e8b..9cf8a37729 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -80,6 +80,31 @@ impl EsploraChainSource { }) } + pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + // This could still accept an Esplora server running against Bitcoin Core v26 + // through v28, which does not relay ephemeral dust. + self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err( + |e| { + if let esplora_client::Error::HttpResponse { status: 404, message } = e { + log_error!( + self.logger, + "Esplora server does not support submitpackage: {}", + message + ); + Error::ChainSourceNotSupported + } else { + log_error!( + self.logger, + "Failed to check support for submitpackage on the Esplora server: {}", + e + ); + Error::ConnectionFailed + } + }, + )?; + Ok(()) + } + pub(super) async fn sync_onchain_wallet( &self, onchain_wallet: Arc, ) -> Result<(), Error> { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 8a8115e4f5..4490b3a042 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -29,6 +29,37 @@ use crate::runtime::Runtime; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; +/// We use this parent-child TRUC package to make sure the configured chain source supports +/// broadcasting packages via the `submitpackage` Bitcoin Core RPC. +const PARENT_TXID: &str = "9a015f93fac6cb203c2b994e18b85176eb0354a22a468255516f3c6002d3f696"; +const PARENT_HEX: &str = + "0300000000010160d0cdb72f2ddf719f40ca32f44614c67577fc75996140544003915683c34a310000000000fd\ + ffffff0201000000000000000451024e73876100000000000022512042731375894dad3b25092cd0f713dc5bee4\ + a71e30a95e1db3d880906d7eba1fa01409327942924218e4eb1635a7cce6706fcb37b8bbb61a2f0b86357356681\ + 4e09419a3501e02252043bb237d479304632282fe9159db9e9a6ae6ec5bedea9f0f115a97b0e00"; +const CHILD_TXID: &str = "d011b3ff78cdfb8b93822639ea87771847936b04bb83afc8763a7c02a386ae26"; +const CHILD_HEX: &str = + "0300000000010296f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0000000000ff\ + ffffff96f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0100000000fdffffff015\ + 660000000000000225120ac18cd599a1be003595854e2eeec18dbe1c92d04b0ba05812d04445e3fcf16bc000140\ + 1462a35808d77a164f0a23a84c4721d1545befd09ad19945bb8aa0ea5576953a9699038725f944b1bc429942ef4\ + 7e6504a554babf022cb15db53be2d8c1dbfe5a97b0e00"; + +fn dummy_package() -> [bitcoin::Transaction; 2] { + use bitcoin::consensus::Decodable; + use bitcoin::hex::FromHex; + use bitcoin::Transaction; + let parent_tx_bytes = Vec::from_hex(PARENT_HEX).expect("read from a constant"); + let child_tx_bytes = Vec::from_hex(CHILD_HEX).expect("read from a constant"); + let parent = + Transaction::consensus_decode(&mut &parent_tx_bytes[..]).expect("read from a constant"); + let child = + Transaction::consensus_decode(&mut &child_tx_bytes[..]).expect("read from a constant"); + assert_eq!(parent.compute_txid().to_string(), PARENT_TXID); + assert_eq!(child.compute_txid().to_string(), CHILD_TXID); + [parent, child] +} + pub(crate) enum WalletSyncStatus { Completed, InProgress { subscribers: tokio::sync::broadcast::Sender> }, @@ -438,6 +469,26 @@ impl ChainSource { } } + pub(crate) async fn validate_zero_fee_commitments_support_if_required( + &self, submit_package_support_required: bool, + ) -> Result<(), Error> { + if !submit_package_support_required { + return Ok(()); + } + + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.validate_zero_fee_commitments_support().await + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.validate_zero_fee_commitments_support().await + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.validate_zero_fee_commitments_support().await + }, + } + } + pub(crate) async fn continuously_process_broadcast_queue( &self, mut stop_tx_bcast_receiver: tokio::sync::watch::Receiver<()>, ) { diff --git a/src/config.rs b/src/config.rs index 38887b9bf3..1d7137f2ee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,7 +54,8 @@ pub const DEFAULT_LOG_FILENAME: &'static str = "ldk_node.log"; /// The default storage directory. pub const DEFAULT_STORAGE_DIR_PATH: &str = "/tmp/ldk_node"; -// The default Esplora server we're using. +// The default Esplora server we're using. It supports `submitpackage`, check using POST on the +// `/txs/package` endpoint. pub(crate) const DEFAULT_ESPLORA_SERVER_URL: &str = "https://blockstream.info/api"; // The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold @@ -288,10 +289,11 @@ impl Default for HumanReadableNamesConfig { /// /// ### Defaults /// -/// | Parameter | Value | -/// |----------------------------|--------| -/// | `trusted_peers_no_reserve` | [] | -/// | `per_channel_reserve_sats` | 25000 | +/// | Parameter | Value | +/// |-------------------------------|--------| +/// | `trusted_peers_no_reserve` | [] | +/// | `per_channel_reserve_sats` | 25000 | +/// | `enable_zero_fee_commitments` | false | /// /// /// [BOLT 3]: https://github.com/lightning/bolts/blob/master/03-transactions.md#htlc-timeout-and-htlc-success-transactions @@ -327,6 +329,12 @@ pub struct AnchorChannelsConfig { /// might not suffice to successfully spend the Anchor output and have the HTLC transactions /// confirmed on-chain, i.e., you may want to adjust this value accordingly. pub per_channel_reserve_sats: u64, + /// In addition to `option_anchors_zero_fee_htlc_tx`, we will also attempt to negotiate + /// `option_zero_fee_commitments`. Zero-fee commitment channels remove all commitment + /// feerate negotiation from the channel, and instead source *all* the fees required to + /// confirm the commitment from the anchor reserve at the time of broadcast. + /// See [BOLT 3] for more technical details. + pub enable_zero_fee_commitments: bool, } impl Default for AnchorChannelsConfig { @@ -334,6 +342,7 @@ impl Default for AnchorChannelsConfig { Self { trusted_peers_no_reserve: Vec::new(), per_channel_reserve_sats: DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS, + enable_zero_fee_commitments: false, } } } diff --git a/src/error.rs b/src/error.rs index d07212b008..8546af0dd2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -137,6 +137,8 @@ pub enum Error { LnurlAuthTimeout, /// The provided lnurl is invalid. InvalidLnurl, + /// The configured chain source is not supported. + ChainSourceNotSupported, } impl fmt::Display for Error { @@ -222,6 +224,9 @@ impl fmt::Display for Error { Self::LnurlAuthFailed => write!(f, "LNURL-auth authentication failed."), Self::LnurlAuthTimeout => write!(f, "LNURL-auth authentication timed out."), Self::InvalidLnurl => write!(f, "The provided lnurl is invalid."), + Self::ChainSourceNotSupported => { + write!(f, "The configured chain source is not supported.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 4bae85c646..85c39612dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -285,9 +285,20 @@ impl Node { e })?; - // Block to ensure we update our fee rate cache once on startup + // Block to ensure we update our fee rate cache once on startup. + // Also take this opportunity to make sure our chain source supports 0FC channels + // if they are enabled. + // + // TODO: drop 0FC chain source validation when support is ubiquitous let chain_source = Arc::clone(&self.chain_source); - self.runtime.block_on(async move { chain_source.update_fee_rate_estimates().await })?; + self.runtime.block_on(async move { + tokio::try_join!( + chain_source.update_fee_rate_estimates(), + chain_source.validate_zero_fee_commitments_support_if_required( + self.config.anchor_channels_config.enable_zero_fee_commitments + ) + ) + })?; // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); From 088c0841d1cc4a9da171f87a41476884f1723323 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 2 Jul 2026 05:17:48 +0000 Subject: [PATCH 13/23] f: do not block when validating electrum chain source --- src/chain/electrum.rs | 45 +++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index c4564a60d2..e3409d715d 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -321,27 +321,30 @@ impl ElectrumChainSource { // // This could still accept an Electrum server running against Bitcoin Core v26 // through v28, which does not relay ephemeral dust. - electrum_client - .electrum_client - .transaction_broadcast_package(&super::dummy_package()) - .map_err(|e| { - if let electrum_client::Error::AllAttemptsErrored(_) = e { - log_error!( - self.logger, - "Electrum server does not support submitpackage: {:?}", - e - ); - Error::ChainSourceNotSupported - } else { - log_error!( - self.logger, - "Failed to check support for submitpackage on the Electrum server: {}", - e - ); - Error::ConnectionFailed - } - })?; - Ok(()) + let spawn_fut = electrum_client.runtime.spawn_blocking({ + let electrum_client = Arc::clone(&electrum_client.electrum_client); + move || electrum_client.transaction_broadcast_package(&super::dummy_package()) + }); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), + spawn_fut, + ); + + match timeout_fut.await { + Ok(Ok(Ok(_))) => Ok(()), + Ok(Ok(Err(electrum_client::Error::AllAttemptsErrored(e)))) => { + log_error!(self.logger, "Electrum server does not support submitpackage: {:?}", e); + Err(Error::ChainSourceNotSupported) + }, + e => { + log_error!( + self.logger, + "Failed to check support for submitpackage on the Electrum server: {:?}", + e + ); + Err(Error::ConnectionFailed) + }, + } } pub(crate) async fn process_broadcast_package(&self, package: Vec) { From 40d3d65c49f9408b9c4fabe74af0f48ace4c6037 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 1 Jul 2026 13:46:28 +0000 Subject: [PATCH 14/23] f: always validate support for submitpackage we may use this call to broadcast keyed anchor bumps --- src/chain/bitcoind.rs | 2 +- src/chain/electrum.rs | 2 +- src/chain/esplora.rs | 2 +- src/chain/mod.rs | 14 ++++---------- src/lib.rs | 7 ++----- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 0899d8dcac..40092e4e66 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -119,7 +119,7 @@ impl BitcoindChainSource { self.api_client.utxo_source() } - pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + pub(super) async fn validate_submit_package_support(&self) -> Result<(), Error> { let node_version_result = tokio::time::timeout( Duration::from_secs(CHAIN_POLLING_TIMEOUT_SECS), self.api_client.get_node_version(), diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index e3409d715d..4358b00f2d 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -303,7 +303,7 @@ impl ElectrumChainSource { Ok(()) } - pub(crate) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + pub(crate) async fn validate_submit_package_support(&self) -> Result<(), Error> { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() { diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 9cf8a37729..fcac145b38 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -80,7 +80,7 @@ impl EsploraChainSource { }) } - pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + pub(super) async fn validate_submit_package_support(&self) -> Result<(), Error> { // This could still accept an Esplora server running against Bitcoin Core v26 // through v28, which does not relay ephemeral dust. self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err( diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 4490b3a042..ce5723dd38 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -469,22 +469,16 @@ impl ChainSource { } } - pub(crate) async fn validate_zero_fee_commitments_support_if_required( - &self, submit_package_support_required: bool, - ) -> Result<(), Error> { - if !submit_package_support_required { - return Ok(()); - } - + pub(crate) async fn validate_submit_package_support(&self) -> Result<(), Error> { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { - esplora_chain_source.validate_zero_fee_commitments_support().await + esplora_chain_source.validate_submit_package_support().await }, ChainSourceKind::Electrum(electrum_chain_source) => { - electrum_chain_source.validate_zero_fee_commitments_support().await + electrum_chain_source.validate_submit_package_support().await }, ChainSourceKind::Bitcoind(bitcoind_chain_source) => { - bitcoind_chain_source.validate_zero_fee_commitments_support().await + bitcoind_chain_source.validate_submit_package_support().await }, } } diff --git a/src/lib.rs b/src/lib.rs index 85c39612dc..3b5331893e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -286,17 +286,14 @@ impl Node { })?; // Block to ensure we update our fee rate cache once on startup. - // Also take this opportunity to make sure our chain source supports 0FC channels - // if they are enabled. + // Also take this opportunity to make sure our chain source supports submitpackage. // // TODO: drop 0FC chain source validation when support is ubiquitous let chain_source = Arc::clone(&self.chain_source); self.runtime.block_on(async move { tokio::try_join!( chain_source.update_fee_rate_estimates(), - chain_source.validate_zero_fee_commitments_support_if_required( - self.config.anchor_channels_config.enable_zero_fee_commitments - ) + chain_source.validate_submit_package_support() ) })?; From 69ffd665f198fc5868400a35bb6ee2344ea754ce Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 1 Jul 2026 18:57:05 +0000 Subject: [PATCH 15/23] f: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e012a146..7dcf6942e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated. - Users of the VSS storage backend must upgrade their VSS server to at least version `v0.1.0-alpha.0` before upgrading LDK Node. +- The Bitcoin node used to broadcast transactions must relay TRUC, P2A, and ephemeral dust. Bitcoin + Core v29 and above satisfy this requirement. Esplora chain sources also need to support the + `/txs/package` endpoint, and Electrum chain sources need to support the `broadcast_package` + method added in Electrum protocol v1.6. ## Feature and API updates - The Bitcoin Core RPC and REST chain-source builder methods now accept an optional From 2f6032bf22520f6c106e79b0e16e543bc398f6c7 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 1 Jul 2026 19:04:03 +0000 Subject: [PATCH 16/23] f: drop enable_zero_fee_commitments --- src/config.rs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1d7137f2ee..dcc380ed91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -289,11 +289,10 @@ impl Default for HumanReadableNamesConfig { /// /// ### Defaults /// -/// | Parameter | Value | -/// |-------------------------------|--------| -/// | `trusted_peers_no_reserve` | [] | -/// | `per_channel_reserve_sats` | 25000 | -/// | `enable_zero_fee_commitments` | false | +/// | Parameter | Value | +/// |----------------------------|--------| +/// | `trusted_peers_no_reserve` | [] | +/// | `per_channel_reserve_sats` | 25000 | /// /// /// [BOLT 3]: https://github.com/lightning/bolts/blob/master/03-transactions.md#htlc-timeout-and-htlc-success-transactions @@ -329,12 +328,6 @@ pub struct AnchorChannelsConfig { /// might not suffice to successfully spend the Anchor output and have the HTLC transactions /// confirmed on-chain, i.e., you may want to adjust this value accordingly. pub per_channel_reserve_sats: u64, - /// In addition to `option_anchors_zero_fee_htlc_tx`, we will also attempt to negotiate - /// `option_zero_fee_commitments`. Zero-fee commitment channels remove all commitment - /// feerate negotiation from the channel, and instead source *all* the fees required to - /// confirm the commitment from the anchor reserve at the time of broadcast. - /// See [BOLT 3] for more technical details. - pub enable_zero_fee_commitments: bool, } impl Default for AnchorChannelsConfig { @@ -342,7 +335,6 @@ impl Default for AnchorChannelsConfig { Self { trusted_peers_no_reserve: Vec::new(), per_channel_reserve_sats: DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS, - enable_zero_fee_commitments: false, } } } From a2e99dcdd5600dbd8b02ca5db3aae0830935221c Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 27 May 2026 17:35:10 +0000 Subject: [PATCH 17/23] Sort packages received via `BroadcasterInterface` Implementations of `BroadcasterInterface` cannot assume any topological ordering on the transactions received, so here we order the received transactions before adding them to the broadcast queue. Any consumers of the queue can now assume all transactions received to be topologically sorted. Codex wrote the tests. --- src/chain/bitcoind.rs | 5 +- src/chain/electrum.rs | 5 +- src/chain/esplora.rs | 7 +- src/chain/mod.rs | 10 +-- src/tx_broadcaster.rs | 196 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 209 insertions(+), 14 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 40092e4e66..21d181c294 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -41,6 +41,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::tx_broadcaster::SortedTransactions; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -595,12 +596,12 @@ impl BitcoindChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, package: Vec) { + pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 // features, we should eventually switch to use `submitpackage` via the // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual // transactions. - for tx in &package { + for tx in txs.iter() { let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 4358b00f2d..6ebc6d4888 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -34,6 +34,7 @@ use crate::fee_estimator::{ use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, LdkLogger, Logger}; use crate::runtime::Runtime; +use crate::tx_broadcaster::SortedTransactions; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::PersistedNodeMetrics; @@ -347,7 +348,7 @@ impl ElectrumChainSource { } } - pub(crate) async fn process_broadcast_package(&self, package: Vec) { + pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() { @@ -357,7 +358,7 @@ impl ElectrumChainSource { return; }; - for tx in package { + for tx in txs.into_inner() { electrum_client.broadcast(tx).await; } } diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index fcac145b38..794bf9d484 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -11,7 +11,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use bdk_esplora::EsploraAsyncExt; -use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; +use bitcoin::{FeeRate, Network, Script, Txid}; use esplora_client::AsyncClient as EsploraAsyncClient; use lightning::chain::{Confirm, Filter, WatchedOutput}; use lightning::util::ser::Writeable; @@ -25,6 +25,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, LdkLogger, Logger}; +use crate::tx_broadcaster::SortedTransactions; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -390,8 +391,8 @@ impl EsploraChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, package: Vec) { - for tx in &package { + pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) { + for tx in txs.iter() { let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), diff --git a/src/chain/mod.rs b/src/chain/mod.rs index ce5723dd38..428caefabf 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use std::time::Duration; -use bitcoin::{Script, Transaction, Txid}; +use bitcoin::{Script, Txid}; use lightning::chain::{BlockLocator, Filter}; use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient}; @@ -512,16 +512,16 @@ impl ChainSource { continue; }, }; - let txs: Vec = package.into_transactions(); + let package = package.into_sorted_transactions(); match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { - esplora_chain_source.process_broadcast_package(txs).await + esplora_chain_source.process_transaction_broadcast(package).await }, ChainSourceKind::Electrum(electrum_chain_source) => { - electrum_chain_source.process_broadcast_package(txs).await + electrum_chain_source.process_transaction_broadcast(package).await }, ChainSourceKind::Bitcoind(bitcoind_chain_source) => { - bitcoind_chain_source.process_broadcast_package(txs).await + bitcoind_chain_source.process_transaction_broadcast(package).await }, } } diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 5722a3ebe3..013fd7adf3 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -36,8 +36,52 @@ impl BroadcastPackage { } /// Consumes the package into its transactions, ready for the chain client. - pub(crate) fn into_transactions(self) -> Vec { - self.0.into_iter().map(|(tx, _)| tx).collect() + pub(crate) fn into_sorted_transactions(self) -> SortedTransactions { + let txs = self.0.into_iter().map(|(tx, _)| tx).collect(); + SortedTransactions::sort_parents_child_package_topologically(txs) + } +} + +pub(crate) struct SortedTransactions(Vec); + +impl SortedTransactions { + pub(crate) fn sort_parents_child_package_topologically( + mut txs: Vec, + ) -> SortedTransactions { + if txs.len() == 0 || txs.len() == 1 { + return SortedTransactions(txs); + } + let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect(); + let any_spends_from_package = |tx: &Transaction| -> bool { + tx.input.iter().any(|input| txids.contains(&input.previous_output.txid)) + }; + txs.sort_by_key(any_spends_from_package); + + #[cfg(debug_assertions)] + { + let child = txs.last().expect("txs is not empty"); + let child_input_txids: Vec<_> = + child.input.iter().map(|input| input.previous_output.txid).collect(); + let parents = &txs[..txs.len() - 1]; + let parent_txids: Vec<_> = parents.iter().map(|parent| parent.compute_txid()).collect(); + // Make sure all the parent txids are parents of the child transaction + debug_assert!(parent_txids.iter().all(|txid| child_input_txids.contains(&txid))); + // Make sure there are no grandparents + debug_assert_eq!(txs.iter().filter(|tx| any_spends_from_package(tx)).count(), 1); + } + + SortedTransactions(txs) + } + + pub(crate) fn into_inner(self) -> Vec { + self.0 + } +} + +impl Deref for SortedTransactions { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 } } @@ -108,3 +152,151 @@ where }); } } + +#[cfg(test)] +mod tests { + use bitcoin::hashes::Hash; + use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness}; + + use super::SortedTransactions; + + fn txin(txid: Txid, vout: u32) -> TxIn { + TxIn { + previous_output: OutPoint { txid, vout }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + } + } + + fn txout(value_sat: u64) -> TxOut { + TxOut { value: Amount::from_sat(value_sat), script_pubkey: ScriptBuf::new() } + } + + fn parent_tx(seed: u8) -> Transaction { + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![txin(Txid::from_byte_array([seed; 32]), 0)], + output: vec![txout(1_000 + u64::from(seed))], + } + } + + fn child_tx(parents: &[&Transaction]) -> Transaction { + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: parents + .iter() + .enumerate() + .map(|(idx, parent)| txin(parent.compute_txid(), idx as u32)) + .collect(), + output: vec![txout(1_000)], + } + } + + fn assert_parents_before_child( + txs: &[Transaction], expected_child: Txid, expected_parents: &[Txid], + ) { + assert_eq!(txs.last().map(Transaction::compute_txid), Some(expected_child)); + assert_eq!(txs.len(), expected_parents.len() + 1); + + let parent_txids = + txs[..txs.len() - 1].iter().map(Transaction::compute_txid).collect::>(); + for expected_parent in expected_parents { + assert!(parent_txids.contains(expected_parent)); + } + } + + #[test] + fn topological_sort_leaves_sorted_package_unchanged() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let child = child_tx(&[&parent_a, &parent_b]); + + let original_txids = + [parent_a.compute_txid(), parent_b.compute_txid(), child.compute_txid()]; + let txs = vec![parent_a, parent_b, child]; + + let package = SortedTransactions::sort_parents_child_package_topologically(txs); + + assert_eq!( + package.iter().map(Transaction::compute_txid).collect::>(), + original_txids + ); + } + + #[test] + fn topological_sort_moves_single_parent_child_from_front_to_end() { + let parent = parent_tx(1); + let child = child_tx(&[&parent]); + let parent_txids = [parent.compute_txid()]; + let child_txid = child.compute_txid(); + let txs = vec![child, parent]; + + let package = SortedTransactions::sort_parents_child_package_topologically(txs); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_moves_child_from_front_to_end() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let child = child_tx(&[&parent_a, &parent_b]); + let parent_txids = [parent_a.compute_txid(), parent_b.compute_txid()]; + let child_txid = child.compute_txid(); + let txs = vec![child, parent_a, parent_b]; + + let package = SortedTransactions::sort_parents_child_package_topologically(txs); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_moves_child_from_front_with_multiple_parents_to_end() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let parent_c = parent_tx(3); + let child = child_tx(&[&parent_a, &parent_b, &parent_c]); + let parent_txids = + [parent_a.compute_txid(), parent_b.compute_txid(), parent_c.compute_txid()]; + let child_txid = child.compute_txid(); + let txs = vec![child, parent_a, parent_b, parent_c]; + + let package = SortedTransactions::sort_parents_child_package_topologically(txs); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_moves_child_from_middle_to_end() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let child = child_tx(&[&parent_a, &parent_b]); + let parent_txids = [parent_a.compute_txid(), parent_b.compute_txid()]; + let child_txid = child.compute_txid(); + let txs = vec![parent_a, child, parent_b]; + + let package = SortedTransactions::sort_parents_child_package_topologically(txs); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_leaves_single_transaction_package_unchanged() { + let parent = parent_tx(1); + let parent_txid = parent.compute_txid(); + let txs = vec![parent]; + + let package = SortedTransactions::sort_parents_child_package_topologically(txs); + + assert_eq!(package.len(), 1); + assert_eq!(package[0].compute_txid(), parent_txid); + } + + #[test] + fn topological_sort_accepts_empty_vec() { + SortedTransactions::sort_parents_child_package_topologically(Vec::new()); + } +} From efd44f1214f8a0c50354584e99bcbf1e645a4589 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 25 Jun 2026 16:43:19 +0000 Subject: [PATCH 18/23] Use helper functions to log broadcast errors These will be useful when we add support for broadcasting packages in an upcoming commit. --- src/chain/bitcoind.rs | 36 ++++++--------- src/chain/electrum.rs | 42 +++++++---------- src/chain/esplora.rs | 105 ++++++++++++++++++++---------------------- 3 files changed, 81 insertions(+), 102 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 21d181c294..8ace12a9f5 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -596,16 +596,25 @@ impl BitcoindChainSource { Ok(()) } + fn log_broadcast_error(&self, e: impl core::fmt::Display, txids: &[Txid], txs: &[Transaction]) { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + } + pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 // features, we should eventually switch to use `submitpackage` via the // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual // transactions. - for tx in txs.iter() { + let txs = txs.into_inner(); + for tx in txs { let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), - self.api_client.broadcast_transaction(tx), + self.api_client.broadcast_transaction(&tx), ); match timeout_fut.await { Ok(res) => match res { @@ -613,28 +622,9 @@ impl BitcoindChainSource { debug_assert_eq!(id, txid); log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, - Err(e) => { - log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); + Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), }, + Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), } } } diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 6ebc6d4888..21fe67ecf4 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -602,14 +602,24 @@ impl ElectrumRuntimeClient { }) } + fn log_broadcast_error(&self, e: impl core::fmt::Display, txids: &[Txid], txs: &[Transaction]) { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in txs { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + } + async fn broadcast(&self, tx: Transaction) { let electrum_client = Arc::clone(&self.electrum_client); let txid = tx.compute_txid(); - let tx_bytes = tx.encode(); + let tx = Arc::new([tx]); - let spawn_fut = - self.runtime.spawn_blocking(move || electrum_client.transaction_broadcast(&tx)); + let spawn_fut = self.runtime.spawn_blocking({ + let tx = Arc::clone(&tx); + move || electrum_client.transaction_broadcast(tx.first().expect("The length is 1")) + }); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), spawn_fut, @@ -617,31 +627,13 @@ impl ElectrumRuntimeClient { match timeout_fut.await { Ok(res) => match res { - Ok(_) => { + Ok(Ok(txid)) => { log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, - Err(e) => { - log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx_bytes) - ); - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx_bytes) - ); + Ok(Err(e)) => self.log_broadcast_error(e, &[txid], tx.as_ref()), + Err(e) => self.log_broadcast_error(e, &[txid], tx.as_ref()), }, + Err(e) => self.log_broadcast_error(e, &[txid], tx.as_ref()), } } diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 794bf9d484..39e9c957f8 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -391,6 +391,55 @@ impl EsploraChainSource { Ok(()) } + fn log_http_error(&self, e: esplora_client::Error, txids: &[Txid], txs: &SortedTransactions) { + match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 && txs.len() == 1 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. + log_trace!( + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + message + ); + log_trace!(self.logger, "Failed to broadcast transaction(s) {:?}", txids); + } else { + log_error!( + self.logger, + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message + ); + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}", txids); + } + log_trace!(self.logger, "Failed broadcast transaction(s) bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + _ => { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction(s) bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + } + } + + fn log_broadcast_error( + &self, e: impl core::fmt::Display, txids: &[Txid], txs: &SortedTransactions, + ) { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + } + pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) { for tx in txs.iter() { let txid = tx.compute_txid(); @@ -403,61 +452,9 @@ impl EsploraChainSource { Ok(()) => { log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. - log_trace!( - self.logger, - "Failed to broadcast due to HTTP connection error: {}", - message - ); - } else { - log_error!( - self.logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, - message - ); - } - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - _ => { - log_error!( - self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); + Err(e) => self.log_http_error(e, &[txid], &txs), }, + Err(e) => self.log_broadcast_error(e, &[txid], &txs), } } } From 225ed455841667a2b750ceae3ed3d81bda98bb09 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 25 Jun 2026 16:02:34 +0000 Subject: [PATCH 19/23] Submit TRUC packages via all chain sources We rely on the `BroadcasterInterface` contract whereby any multi-transaction vector must be a single child and its parents, and must be broadcasted together as a package using `submitpackage`. In a prior commit, we added the guarantee that any packages received from the broadcast queue are already topologically sorted, and hence can be passed directly to the `submit_package` Bitcoin Core RPC. --- src/chain/bitcoind.rs | 109 ++++++++++++++++++++++++++++++++++-------- src/chain/electrum.rs | 48 ++++++++++++++++++- src/chain/esplora.rs | 61 +++++++++++++++++------ src/tx_broadcaster.rs | 8 +++- 4 files changed, 187 insertions(+), 39 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 8ace12a9f5..ab664c7cab 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -596,7 +596,9 @@ impl BitcoindChainSource { Ok(()) } - fn log_broadcast_error(&self, e: impl core::fmt::Display, txids: &[Txid], txs: &[Transaction]) { + fn log_broadcast_error( + &self, e: impl core::fmt::Display, txids: &[Txid], txs: &SortedTransactions, + ) { log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); log_trace!(self.logger, "Failed broadcast transaction bytes:"); for tx in txs.iter() { @@ -605,27 +607,43 @@ impl BitcoindChainSource { } pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) { - // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 - // features, we should eventually switch to use `submitpackage` via the - // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual - // transactions. - let txs = txs.into_inner(); - for tx in txs { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), - self.api_client.broadcast_transaction(&tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(id) => { - debug_assert_eq!(id, txid); - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + match txs.len() { + 0 => (), + 1 => { + let tx = txs.first().expect("The length is 1"); + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), + self.api_client.broadcast_transaction(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(id) => { + debug_assert_eq!(id, txid); + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => self.log_broadcast_error(e, &[txid], &txs), }, - Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), - }, - Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), - } + Err(e) => self.log_broadcast_error(e, &[txid], &txs), + } + }, + 2.. => { + let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), + self.api_client.submit_package(&txs), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(result) => { + log_trace!(self.logger, "Successfully broadcast package {:?}", txids); + log_trace!(self.logger, "Successfully broadcast package {}", result); + }, + Err(e) => self.log_broadcast_error(e, &txids, &txs), + }, + Err(e) => self.log_broadcast_error(e, &txids, &txs), + } + }, } } } @@ -816,6 +834,38 @@ impl BitcoindClient { rpc_client.call_method::("sendrawtransaction", &[tx_json]).await } + /// Submits the provided package + pub(crate) async fn submit_package( + &self, package: &SortedTransactions, + ) -> Result { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::submit_package_inner(Arc::clone(rpc_client), package) + .await + .map_err(BitcoindClientError::Rpc) + }, + BitcoindClient::Rest { rpc_client, .. } => { + // Bitcoin Core's REST interface does not support submitting packages + // so we use the RPC client. + Self::submit_package_inner(Arc::clone(rpc_client), package) + .await + .map_err(BitcoindClientError::Rpc) + }, + } + } + + async fn submit_package_inner( + rpc_client: Arc, package: &SortedTransactions, + ) -> Result { + let package_serialized: Vec<_> = + package.iter().map(|tx| bitcoin::consensus::encode::serialize_hex(tx)).collect(); + let package_json = serde_json::json!(package_serialized); + rpc_client + .call_method::("submitpackage", &[package_json]) + .await + .map(|response| response.0) + } + /// Retrieve the fee estimate needed for a transaction to begin /// confirmation within the provided `num_blocks`. pub(crate) async fn get_fee_estimate_for_target( @@ -1367,6 +1417,23 @@ impl TryInto for JsonResponse { } } +pub struct SubmitPackageResponse(String); + +impl TryInto for JsonResponse { + type Error = String; + fn try_into(self) -> Result { + let response = self.0.to_string(); + let res = self.0.as_object().ok_or("Failed to parse submitpackage response".to_string())?; + + match res["package_msg"].as_str() { + Some("success") => Ok(SubmitPackageResponse(response)), + Some(_) | None => { + return Err(response); + }, + } + } +} + #[derive(Debug, Clone)] pub(crate) struct MempoolEntry { /// The transaction id diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 21fe67ecf4..31f6e68154 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -358,8 +358,12 @@ impl ElectrumChainSource { return; }; - for tx in txs.into_inner() { - electrum_client.broadcast(tx).await; + match txs.len() { + 0 => (), + 1 => { + electrum_client.broadcast(txs.try_into_single_tx().expect("The length is 1")).await + }, + 2.. => electrum_client.submit_package(txs).await, } } } @@ -637,6 +641,46 @@ impl ElectrumRuntimeClient { } } + async fn submit_package(&self, package: SortedTransactions) { + let electrum_client = Arc::clone(&self.electrum_client); + + let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect(); + let package = Arc::new(package); + + let spawn_fut = self.runtime.spawn_blocking({ + let package = Arc::clone(&package); + move || electrum_client.transaction_broadcast_package(&package) + }); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), + spawn_fut, + ); + + match timeout_fut.await { + Ok(res) => match res { + Ok(Ok(result)) => { + if result.success { + log_trace!( + self.logger, + "Successfully broadcast transaction(s) {:?}", + txids + ); + log_trace!( + self.logger, + "Successfully broadcast transaction(s) {:?}", + result + ); + } else { + self.log_broadcast_error(format!("{:?}", result), &txids, &package); + } + }, + Ok(Err(e)) => self.log_broadcast_error(e, &txids, &package), + Err(e) => self.log_broadcast_error(e, &txids, &package), + }, + Err(e) => self.log_broadcast_error(e, &txids, &package), + } + } + async fn get_fee_rate_cache_update( &self, ) -> Result, Error> { diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 39e9c957f8..ce6f9c8d5b 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -441,21 +441,54 @@ impl EsploraChainSource { } pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) { - for tx in txs.iter() { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), - self.esplora_client.broadcast(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + match txs.len() { + 0 => (), + 1 => { + let tx = txs.first().expect("The length is 1"); + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), + self.esplora_client.broadcast(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => self.log_http_error(e, &[txid], &txs), }, - Err(e) => self.log_http_error(e, &[txid], &txs), - }, - Err(e) => self.log_broadcast_error(e, &[txid], &txs), - } + Err(e) => self.log_broadcast_error(e, &[txid], &txs), + } + }, + 2.. => { + let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), + self.esplora_client.submit_package(&txs, None, None), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(result) => { + if result.package_msg.eq_ignore_ascii_case("success") { + log_trace!( + self.logger, + "Successfully broadcast transactions {:?}", + txids + ); + log_trace!( + self.logger, + "Successfully broadcast transactions {:?}", + result + ); + } else { + self.log_broadcast_error(format!("{:?}", result), &txids, &txs); + } + }, + Err(e) => self.log_http_error(e, &txids, &txs), + }, + Err(e) => self.log_broadcast_error(e, &txids, &txs), + } + }, } } } diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 013fd7adf3..30b128cd15 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -73,8 +73,12 @@ impl SortedTransactions { SortedTransactions(txs) } - pub(crate) fn into_inner(self) -> Vec { - self.0 + pub(crate) fn try_into_single_tx(mut self) -> Result { + if self.0.len() == 1 { + Ok(self.0.pop().expect("The length is 1")) + } else { + Err(()) + } } } From 1aa6804b122b42355ad8733ed7328e3a3284f7ed Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 29 Jun 2026 23:21:46 +0000 Subject: [PATCH 20/23] Read even bits to check the anchor channel type --- src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 2ac08a48f8..5198d12cf4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -649,7 +649,7 @@ impl ChannelDetails { value: LdkChannelDetails, anchor_channels_config: &AnchorChannelsConfig, ) -> Self { let reserve_type = value.channel_type.as_ref().map(|channel_type| { - if channel_type.supports_anchors_zero_fee_htlc_tx() { + if channel_type.requires_anchors_zero_fee_htlc_tx() { if anchor_channels_config .trusted_peers_no_reserve .contains(&value.counterparty.node_id) From 9d3a7d4a39fa88f1f027c7a297a9746bca79fe81 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 29 Oct 2025 07:00:04 +0000 Subject: [PATCH 21/23] Include 0FC channels in anchor channel checks --- src/event.rs | 2 +- src/lib.rs | 18 +++++++++++++++--- src/liquidity/service/lsps2.rs | 3 ++- src/types.rs | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/event.rs b/src/event.rs index 1013b87831..066460ee4b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1256,7 +1256,7 @@ where } } - let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); + let anchor_channel = crate::requires_anchor_channel_type(&channel_type); let required_reserve_sats = crate::new_channel_anchor_reserve_sats( &self.config, &counterparty_node_id, diff --git a/src/lib.rs b/src/lib.rs index 3b5331893e..b757adea5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,7 +161,9 @@ use lightning_background_processor::process_events_async; pub use lightning_invoice; pub use lightning_liquidity; pub use lightning_types; -use lightning_types::features::NodeFeatures as LdkNodeFeatures; +use lightning_types::features::{ + ChannelTypeFeatures, InitFeatures, NodeFeatures as LdkNodeFeatures, +}; use liquidity::LiquiditySource; use lnurl_auth::LnurlAuth; use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; @@ -214,6 +216,16 @@ impl LeakChecker { } } +fn supports_anchor_channel_type(init_features: &InitFeatures) -> bool { + init_features.supports_anchors_zero_fee_htlc_tx() + || init_features.supports_anchor_zero_fee_commitments() +} + +fn requires_anchor_channel_type(channel_type: &ChannelTypeFeatures) -> bool { + channel_type.requires_anchors_zero_fee_htlc_tx() + || channel_type.requires_anchor_zero_fee_commitments() +} + /// The main interface object of LDK Node, wrapping the necessary LDK and BDK functionalities. /// /// Needs to be initialized and instantiated through [`Builder::build`]. @@ -1342,7 +1354,7 @@ impl Node { .peer_by_node_id(peer_node_id) .ok_or(Error::ConnectionFailed)? .init_features; - let anchor_channel = init_features.supports_anchors_zero_fee_htlc_tx(); + let anchor_channel = supports_anchor_channel_type(&init_features); Ok(new_channel_anchor_reserve_sats(&self.config, peer_node_id, anchor_channel)) } @@ -2415,7 +2427,7 @@ pub(crate) fn total_anchor_channels_reserve_sats( .contains(&c.counterparty.node_id) && c.channel_shutdown_state .map_or(true, |s| s != ChannelShutdownState::ShutdownComplete) - && c.channel_type.as_ref().map_or(false, |t| t.requires_anchors_zero_fee_htlc_tx()) + && c.channel_type.as_ref().map_or(false, requires_anchor_channel_type) }) .count() as u64 * config.anchor_channels_config.per_channel_reserve_sats diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index ca70cd8d8e..6d2f62c9f2 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -452,8 +452,9 @@ where total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); let spendable_amount_sats = self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let anchor_channel = crate::supports_anchor_channel_type(&init_features); let required_funds_sats = channel_amount_sats - + if init_features.supports_anchors_zero_fee_htlc_tx() + + if anchor_channel && !self .config .anchor_channels_config diff --git a/src/types.rs b/src/types.rs index 5198d12cf4..5552877ef8 100644 --- a/src/types.rs +++ b/src/types.rs @@ -649,7 +649,7 @@ impl ChannelDetails { value: LdkChannelDetails, anchor_channels_config: &AnchorChannelsConfig, ) -> Self { let reserve_type = value.channel_type.as_ref().map(|channel_type| { - if channel_type.requires_anchors_zero_fee_htlc_tx() { + if crate::requires_anchor_channel_type(channel_type) { if anchor_channels_config .trusted_peers_no_reserve .contains(&value.counterparty.node_id) From beaf17a1fc1781001be1625528473a0c650fa878 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 1 Jul 2026 05:50:32 +0000 Subject: [PATCH 22/23] Negotiate 0FC channels if configured --- src/config.rs | 7 +++++-- tests/common/mod.rs | 9 +++++---- tests/integration_tests_rust.rs | 5 +++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/config.rs b/src/config.rs index dcc380ed91..286b670040 100644 --- a/src/config.rs +++ b/src/config.rs @@ -171,7 +171,8 @@ pub struct Config { /// used to send pre-flight probes. pub probing_liquidity_limit_multiplier: u64, /// Configuration options pertaining to Anchor channels, i.e., channels for which the - /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. + /// `option_zero_fee_commitments` or `option_anchors_zero_fee_htlc_tx` channel type is + /// negotiated. /// /// Please refer to [`AnchorChannelsConfig`] for further information on Anchor channels. pub anchor_channels_config: AnchorChannelsConfig, @@ -270,7 +271,7 @@ impl Default for HumanReadableNamesConfig { } /// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the -/// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. +/// `option_zero_fee_commitments` or `option_anchors_zero_fee_htlc_tx` channel type is negotiated. /// /// Prior to the introduction of Anchor channels, the on-chain fees paying for the transactions /// issued on channel closure were pre-determined and locked-in at the time of the channel @@ -390,6 +391,8 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { // will mostly be relevant for inbound channels. let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; + user_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = + config.anchor_channels_config.enable_zero_fee_commitments; user_config.reject_inbound_splices = false; if may_announce_channel(config).is_err() { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index c18244dcf0..72fb8190c3 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -380,6 +380,9 @@ pub(crate) fn random_node_alias() -> Option { pub(crate) fn random_config() -> TestConfig { let mut node_config = Config::default(); + let zero_fee_commitments = rand::random_bool(0.5); + node_config.anchor_channels_config.enable_zero_fee_commitments = zero_fee_commitments; + node_config.network = Network::Regtest; println!("Setting network: {}", node_config.network); @@ -1406,10 +1409,8 @@ pub(crate) async fn do_channel_full_cycle( let node_a_outbound_capacity_msat = node_a.list_channels()[0].outbound_capacity_msat; let node_a_reserve_msat = node_a.list_channels()[0].unspendable_punishment_reserve.unwrap() * 1000; - // TODO: Zero-fee commitment channels are anchor channels, but do not allocate any - // funds to the anchor, so this will need to be updated when we ship these channels - // in ldk-node. - let node_a_anchors_msat = if expect_anchor_channel { 2 * 330 * 1000 } else { 0 }; + let zero_fee_commitments = node_a.list_channels()[0].feerate_sat_per_1000_weight == 0; + let node_a_anchors_msat = if zero_fee_commitments { 0 } else { 2 * 330 * 1000 }; let funding_amount_msat = node_a.list_channels()[0].channel_value_sats * 1000; // Node B does not have any reserve, so we only subtract a few items on node A's // side to arrive at node B's capacity diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 1c476fa03c..b83e5aa93b 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1421,8 +1421,9 @@ async fn splice_channel() { let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); let opening_transaction_fee_sat = 156; - let closing_transaction_fee_sat = 614; - let anchor_output_sat = 330; + let zero_fee_commitments = node_a.list_channels()[0].feerate_sat_per_1000_weight == 0; + let closing_transaction_fee_sat = if zero_fee_commitments { 0 } else { 614 }; + let anchor_output_sat = if zero_fee_commitments { 0 } else { 330 }; assert_eq!( node_a.list_balances().total_onchain_balance_sats, From e61e3bc1839eb6b4a7c06246ba33ee2504258ffd Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 1 Jul 2026 19:08:37 +0000 Subject: [PATCH 23/23] f: turn 0fc channels on by default --- src/config.rs | 3 +-- tests/common/mod.rs | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index 286b670040..badf7b6ad1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -391,8 +391,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { // will mostly be relevant for inbound channels. let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; - user_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = - config.anchor_channels_config.enable_zero_fee_commitments; + user_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; user_config.reject_inbound_splices = false; if may_announce_channel(config).is_err() { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 72fb8190c3..ffc10313ae 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -380,9 +380,6 @@ pub(crate) fn random_node_alias() -> Option { pub(crate) fn random_config() -> TestConfig { let mut node_config = Config::default(); - let zero_fee_commitments = rand::random_bool(0.5); - node_config.anchor_channels_config.enable_zero_fee_commitments = zero_fee_commitments; - node_config.network = Network::Regtest; println!("Setting network: {}", node_config.network);