From eb2a9d8b3550da62157384de5e378f8eb4bc1ea4 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 20 Oct 2026 14:17:06 -0500 Subject: [PATCH 1/2] Saturate anchor reserve arithmetic Setting AnchorChannelsConfig::per_channel_reserve_sats to u64::MAX is a natural way to say "never open or accept anchor channels with untrusted peers", since no wallet can cover that reserve. However, the reserve sums used unchecked arithmetic: multiplying it by the number of existing anchor channels, or adding the reserve for a new channel, overflowed and panicked in debug builds. Values close to a power of two could also wrap to a tiny reserve in release builds. Use saturating arithmetic so an oversized reserve is treated as unsatisfiable instead. This was found by Project Loupe. This change was made with the help of Claude Code. --- src/lib.rs | 11 ++++++----- src/liquidity/service/lsps2.rs | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index fb295d3ff9..346ed9cbb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1402,7 +1402,8 @@ impl Node { total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); let new_channel_reserve = self.new_channel_anchor_reserve_sats(&peer_info.node_id)?; - let total_anchor_reserve_sats = cur_anchor_reserve_sats + new_channel_reserve; + let total_anchor_reserve_sats = + cur_anchor_reserve_sats.saturating_add(new_channel_reserve); let fee_rate = fee_rate.unwrap_or_else(|| { self.fee_estimator.estimate_fee_rate(ConfirmationTarget::ChannelFunding) @@ -1529,7 +1530,7 @@ impl Node { if for_new_channel { let required_funds_sats = - amount_sats + self.new_channel_anchor_reserve_sats(peer_node_id)?; + amount_sats.saturating_add(self.new_channel_anchor_reserve_sats(peer_node_id)?); if spendable_amount_sats < required_funds_sats { log_error!(self.logger, @@ -2717,7 +2718,7 @@ impl_writeable_tlv_based!(NodeMetrics, { pub(crate) fn total_anchor_channels_reserve_sats( channel_manager: &ChannelManager, config: &Config, ) -> u64 { - channel_manager + let num_reserved_channels = channel_manager .list_channels() .into_iter() .filter(|c| { @@ -2729,8 +2730,8 @@ pub(crate) fn total_anchor_channels_reserve_sats( .map_or(true, |s| s != ChannelShutdownState::ShutdownComplete) && c.channel_type.as_ref().map_or(true, requires_anchor_channel_type) }) - .count() as u64 - * config.anchor_channels_config.per_channel_reserve_sats + .count() as u64; + num_reserved_channels.saturating_mul(config.anchor_channels_config.per_channel_reserve_sats) } pub(crate) fn new_channel_anchor_reserve_sats( diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 7f49428d29..5533357928 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -460,7 +460,8 @@ where &their_network_key, anchor_channel, ); - let required_funds_sats = channel_amount_sats + additional_reserve_required; + let required_funds_sats = + channel_amount_sats.saturating_add(additional_reserve_required); if spendable_amount_sats < required_funds_sats { log_error!(self.logger, "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", From 3ba6ec0c45ea39bf36703a7e061bcab1ddbcffa3 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 20 Oct 2026 14:22:09 -0500 Subject: [PATCH 2/2] Saturate LSPS channel size arithmetic The LSPS2 service computed the JIT channel size by multiplying the intercepted HTLC amount by the configured over-provisioning rate, and the LSPS1 client summed the caller-provided channel balances, both with unchecked arithmetic. Oversized inputs overflowed, panicking in debug builds and wrapping to a bogus channel size in release builds. Use saturating arithmetic so such inputs fall through to the existing insufficient-funds and LSP-limit checks. This was found by Project Loupe. This change was made with the help of Claude Code. --- src/liquidity/client/lsps1.rs | 2 +- src/liquidity/service/lsps2.rs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/liquidity/client/lsps1.rs b/src/liquidity/client/lsps1.rs index 79dc655fe6..12965ac17f 100644 --- a/src/liquidity/client/lsps1.rs +++ b/src/liquidity/client/lsps1.rs @@ -98,7 +98,7 @@ where })?; let lsp_limits = self.lsps1_request_opening_params(node_id).await?.supported_options; - let channel_size_sat = lsp_balance_sat + client_balance_sat; + let channel_size_sat = lsp_balance_sat.saturating_add(client_balance_sat); if channel_size_sat < lsp_limits.min_channel_balance_sat || channel_size_sat > lsp_limits.max_channel_balance_sat diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 5533357928..14fff4b624 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -445,10 +445,11 @@ where }; // Fail if we have insufficient onchain funds available. - let over_provisioning_msat = (amt_to_forward_msat - * service_config.channel_over_provisioning_ppm as u64) + let over_provisioning_msat = amt_to_forward_msat + .saturating_mul(service_config.channel_over_provisioning_ppm as u64) / 1_000_000; - let channel_amount_sats = (amt_to_forward_msat + over_provisioning_msat) / 1000; + let channel_amount_sats = + amt_to_forward_msat.saturating_add(over_provisioning_msat) / 1000; let cur_anchor_reserve_sats = total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); let spendable_amount_sats =