From d60601cd003da98169adaed2dc9bbe7837168dfe Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 8 Oct 2026 11:26:19 +0200 Subject: [PATCH] Round LSPS2 JIT channel amounts up to whole sats When acting as an LSPS2 service, the amount of a JIT channel was derived from the millisatoshi forward amount with integer division, i.e., it was rounded down. For forward amounts that aren't a whole number of satoshis the channel could end up too small to carry the intercepted HTLC. In the LSP-trusts-client flow the service has already paid the funding fee at that point, and any client can trigger this remotely. We now round up to the next whole satoshi when converting, so the opened channel always has enough capacity for the forward. Reported by Project Loupe. Co-Authored-By: HAL 9000 --- src/liquidity/service/lsps2.rs | 3 ++- tests/integration_tests_rust.rs | 16 +++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 7f49428d29..d186c97c5e 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -448,7 +448,8 @@ where 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 channel_amount_sats = + (amt_to_forward_msat + over_provisioning_msat).div_ceil(1000); let cur_anchor_reserve_sats = total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); let spendable_amount_sats = diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index f58cad6904..30f4b43bc5 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -3837,11 +3837,18 @@ async fn unified_send_receive_bip21_uri() { #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn lsps2_client_service_integration() { - do_lsps2_client_service_integration(true).await; - do_lsps2_client_service_integration(false).await; + do_lsps2_client_service_integration(true, 100_000_000).await; + do_lsps2_client_service_integration(false, 100_000_000).await; } -async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn lsps2_jit_channel_rounds_fractional_msat_up() { + // A forward amount that is not a whole number of satoshis must result in a channel large + // enough to carry it, i.e., the channel amount needs to be rounded up, not down. + do_lsps2_client_service_integration(false, 100_000_100).await; +} + +async fn do_lsps2_client_service_integration(client_trusts_lsp: bool, jit_amount_msat: u64) { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); @@ -3918,7 +3925,6 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let invoice_description = Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); - let jit_amount_msat = 100_000_000; println!("Generating JIT invoice!"); let jit_invoice = client_node @@ -3951,7 +3957,7 @@ async fn do_lsps2_client_service_integration(client_trusts_lsp: bool) { let expected_channel_overprovisioning_msat = (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; let expected_channel_size_sat = - (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; + (expected_received_amount_msat + expected_channel_overprovisioning_msat).div_ceil(1000); let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; assert_eq!(channel_value_sats, expected_channel_size_sat);