Skip to content

Commit 73fea3b

Browse files
nepetdaywalker90
authored andcommitted
lsps-plugin: client skips opening fee params with inverted payment limits
bLIP-52 gives `min_payment_size_msat` and `max_payment_size_msat` as "the limits of the payment size, inclusive". An `opening_fee_params` entry with min above max allows no payment size at all, so a JIT channel bought with it can't be paid. The spec has no rule for this case; skipping such entries is our choice. When selecting from the `lsps2.get_info` menu, `lsps-jitchannel` now treats such an entry like an expired one: it is skipped with a warning, and if no entry is left the call fails before `lsps2.buy`. min equal to max stays valid. The selection is moved into `select_opening_fee_params()` so it can be unit-tested; the expiry rule is unchanged. Behavior change for the client. Changelog-Fixed: lsps-plugin: `lsps-jitchannel` no longer selects fee parameters whose `min_payment_size_msat` is above `max_payment_size_msat`, which no payment can satisfy.
1 parent 236f588 commit 73fea3b

1 file changed

Lines changed: 89 additions & 14 deletions

File tree

‎plugins/lsps-plugin/src/client.rs‎

Lines changed: 89 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,31 @@ async fn on_lsps_lsps2_approve(
297297
Ok(serde_json::Value::default())
298298
}
299299

300+
/// Selects the opening fee parameters to buy a JIT channel with.
301+
///
302+
/// Simple strategy for now: choose the first usable option as LSPS2 requires
303+
/// the menu to be ordered by increasing fees. Could be more sophisticated
304+
/// (e.g., user choice). An option is unusable if it expires within a minute
305+
/// (extra time for network delay) or if its `min_payment_size_msat` is
306+
/// above its `max_payment_size_msat`, so no payment size fits.
307+
fn select_opening_fee_params(
308+
menu: &[OpeningFeeParams],
309+
now: chrono::DateTime<Utc>,
310+
) -> Option<&OpeningFeeParams> {
311+
menu.iter().find(|params| {
312+
let fut_now = now + Duration::minutes(1);
313+
if params.valid_until.as_chrono() <= fut_now {
314+
warn!("Ignoring expired fee params from LSP {:?}", params);
315+
return false;
316+
}
317+
if let Err(e) = params.check_limits() {
318+
warn!("Ignoring unusable fee params from LSP {:?}: {}", params, e);
319+
return false;
320+
}
321+
true
322+
})
323+
}
324+
300325
/// RPC Method handler for `lsps-jitchannel`.
301326
/// Calls lsps2.get_info, selects parameters, calculates fee, calls lsps2.buy,
302327
/// creates invoice.
@@ -337,20 +362,7 @@ async fn on_lsps_lsps2_invoice(
337362
.await?;
338363

339364
// 2. Select Fee Parameters.
340-
// Simple strategy for now: choose the first valid option as LSPS2 requires
341-
// this to be the cheapest. Could be more sophisticated (e.g., user choice).
342-
let selected_params = info_res
343-
.opening_fee_params_menu
344-
.iter()
345-
.find(|params| {
346-
// Basic validation on client side: check expiry and promise length
347-
let fut_now = Utc::now() + Duration::minutes(1); // Add some extra time for network delay
348-
let expiry_valid = params.valid_until.as_chrono() > fut_now;
349-
if !expiry_valid {
350-
warn!("Ignoring expired fee params from LSP {:?}", params);
351-
}
352-
expiry_valid
353-
})
365+
let selected_params = select_opening_fee_params(&info_res.opening_fee_params_menu, Utc::now())
354366
.cloned() // Clone the selected params
355367
.ok_or_else(|| {
356368
anyhow!(
@@ -923,3 +935,66 @@ struct DatastoreRecord {
923935
jit_channel_scid: ShortChannelId,
924936
client_trusts_lsp: bool,
925937
}
938+
939+
#[cfg(test)]
940+
mod tests {
941+
use super::*;
942+
use chrono::TimeZone as _;
943+
use lsps_types::{lsps0::Ppm, lsps2::Promise};
944+
945+
fn now() -> chrono::DateTime<Utc> {
946+
Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap()
947+
}
948+
949+
fn params(min_payment_size: u64, max_payment_size: u64, promise: &str) -> OpeningFeeParams {
950+
OpeningFeeParams {
951+
min_fee_msat: Msat::from_msat(1_000),
952+
proportional: Ppm::from_ppm(1_000),
953+
valid_until: (now() + Duration::hours(1)).into(),
954+
min_lifetime: 144,
955+
max_client_to_self_delay: 2016,
956+
min_payment_size_msat: Msat::from_msat(min_payment_size),
957+
max_payment_size_msat: Msat::from_msat(max_payment_size),
958+
promise: Promise::try_from(promise).unwrap(),
959+
}
960+
}
961+
962+
#[test]
963+
fn select_skips_params_with_min_above_max() {
964+
let menu = vec![params(2_000, 1_000, "inverted"), params(1_000, 2_000, "ok")];
965+
let selected = select_opening_fee_params(&menu, now()).unwrap();
966+
assert_eq!(selected.promise.as_str(), "ok");
967+
}
968+
969+
#[test]
970+
fn select_accepts_min_equal_max() {
971+
let menu = vec![params(1_000, 1_000, "equal")];
972+
let selected = select_opening_fee_params(&menu, now()).unwrap();
973+
assert_eq!(selected.promise.as_str(), "equal");
974+
}
975+
976+
#[test]
977+
fn select_fails_if_all_params_have_min_above_max() {
978+
let menu = vec![params(2_000, 1_000, "a"), params(5_000, 4_999, "b")];
979+
assert!(select_opening_fee_params(&menu, now()).is_none());
980+
}
981+
982+
#[test]
983+
fn select_skips_params_expiring_within_a_minute() {
984+
let mut soon = params(1_000, 2_000, "soon");
985+
soon.valid_until = (now() + Duration::seconds(60)).into();
986+
let menu = vec![soon, params(1_000, 2_000, "later")];
987+
let selected = select_opening_fee_params(&menu, now()).unwrap();
988+
assert_eq!(selected.promise.as_str(), "later");
989+
}
990+
991+
#[test]
992+
fn select_takes_the_first_usable_params() {
993+
let menu = vec![
994+
params(1_000, 2_000, "first"),
995+
params(1_000, 2_000, "second"),
996+
];
997+
let selected = select_opening_fee_params(&menu, now()).unwrap();
998+
assert_eq!(selected.promise.as_str(), "first");
999+
}
1000+
}

0 commit comments

Comments
 (0)