Skip to content

Commit d7375f6

Browse files
committed
lsps2: Filter direct fallback capacity
Only count inbound capacity from graph-known counterparties with usable forwarding parameters. Otherwise a direct-recipient fallback can hide the JIT path even though the payer cannot use that capacity. Co-Authored-By: HAL 9000
1 parent 5157243 commit d7375f6

2 files changed

Lines changed: 130 additions & 17 deletions

File tree

src/builder.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1943,6 +1943,7 @@ fn build_with_store_internal(
19431943
Arc::clone(&scorer),
19441944
scoring_fee_params,
19451945
),
1946+
Arc::clone(&network_graph),
19461947
Arc::clone(&keys_manager),
19471948
));
19481949

@@ -2329,6 +2330,7 @@ fn build_with_store_internal(
23292330
Arc::clone(&scorer),
23302331
probing_fee_params,
23312332
),
2333+
Arc::clone(&network_graph),
23322334
Arc::clone(&keys_manager),
23332335
));
23342336
Arc::new(HighDegreeStrategy::new(

src/liquidity/client/lsps2/router.rs

Lines changed: 128 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@ use lightning::blinded_path::IntroductionNode;
1717
use lightning::impl_writeable_tlv_based;
1818
use lightning::ln::channel_state::ChannelDetails;
1919
use lightning::ln::channelmanager::{PaymentId, MIN_FINAL_CLTV_EXPIRY_DELTA};
20+
use lightning::routing::gossip::NodeId;
2021
use lightning::routing::router::{InFlightHtlcs, Route, RouteParameters, Router};
2122
use lightning::sign::{EntropySource, ReceiveAuthKey};
2223
use lightning::types::features::BlindedHopFeatures;
2324
use lightning::types::payment::PaymentHash;
25+
use std::sync::Arc;
2426

2527
use crate::payment::PaymentMetadata;
28+
use crate::types::Graph;
2629

2730
/// Parameters needed to construct an LSPS2 blinded payment path.
2831
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -50,13 +53,14 @@ impl_writeable_tlv_based!(LSPS2LeaseParameters, {
5053
/// A router wrapper that uses ordinary payment paths when available and LSPS2 JIT paths otherwise.
5154
pub struct LSPS2Router<R: Router, ES: EntropySource> {
5255
inner_router: R,
56+
network_graph: Arc<Graph>,
5357
entropy_source: ES,
5458
}
5559

5660
impl<R: Router, ES: EntropySource> LSPS2Router<R, ES> {
5761
/// Constructs an LSPS2-aware wrapper around `inner_router`.
58-
pub fn new(inner_router: R, entropy_source: ES) -> Self {
59-
Self { inner_router, entropy_source }
62+
pub fn new(inner_router: R, network_graph: Arc<Graph>, entropy_source: ES) -> Self {
63+
Self { inner_router, network_graph, entropy_source }
6064
}
6165

6266
fn payment_parameters(&self, payment_context: &PaymentContext) -> Vec<LSPS2LeaseParameters> {
@@ -103,15 +107,26 @@ impl<R: Router, ES: EntropySource> Router for LSPS2Router<R, ES> {
103107
) -> Result<Vec<BlindedPaymentPath>, ()> {
104108
let parameters = self.payment_parameters(&tlvs.payment_context);
105109
let allow_mpp = parameters.iter().all(|params| params.payment_size_msat.is_some());
110+
let is_eligible_first_hop = |channel: &&ChannelDetails| {
111+
self.network_graph
112+
.read_only()
113+
.node(&NodeId::from_pubkey(&channel.counterparty.node_id))
114+
.is_some() && channel
115+
.counterparty
116+
.forwarding_info
117+
.clone()
118+
.is_some_and(|info| PaymentRelay::try_from(info).is_ok())
119+
};
106120
let direct_path_has_sufficient_liquidity = amount_msats.map_or(true, |amount_msats| {
107121
if allow_mpp {
108122
first_hops
109123
.iter()
124+
.filter(is_eligible_first_hop)
110125
.map(|channel| channel.inbound_capacity_msat)
111126
.fold(0u64, u64::saturating_add)
112127
>= amount_msats
113128
} else {
114-
first_hops.iter().any(|channel| {
129+
first_hops.iter().filter(is_eligible_first_hop).any(|channel| {
115130
channel.inbound_capacity_msat >= amount_msats
116131
&& channel.inbound_htlc_minimum_msat.unwrap_or(0) <= amount_msats
117132
&& channel.inbound_htlc_maximum_msat.unwrap_or(u64::MAX) >= amount_msats
@@ -135,9 +150,9 @@ impl<R: Router, ES: EntropySource> Router for LSPS2Router<R, ES> {
135150
);
136151
// The default router may fall back to a direct path for an announced recipient even when no
137152
// channel-backed paths have enough inbound liquidity. Only accept that fallback when the
138-
// locally known channels can receive the resolved amount: across channels when MPP is allowed,
139-
// or on one channel, including its HTLC bounds, when MPP is disabled. Otherwise it would hide
140-
// the need for a JIT path and leave an invoice that cannot be paid.
153+
// locally known, blinded-path-eligible channels can receive the resolved amount: across channels
154+
// when MPP is allowed, or on one channel, including its HTLC bounds, when MPP is disabled.
155+
// Otherwise it would hide the need for a JIT path and leave an invoice that cannot be paid.
141156
//
142157
// Always prefer usable ordinary paths. Besides avoiding an unnecessary channel open, this
143158
// prevents an MPP payer from splitting one payment across regular and JIT paths. The LSP only
@@ -222,16 +237,21 @@ mod tests {
222237
use super::*;
223238

224239
use bitcoin::secp256k1::SecretKey;
240+
use bitcoin::Network;
225241
use core::sync::atomic::{AtomicUsize, Ordering};
226242
use lightning::blinded_path::payment::{Bolt12OfferContext, PaymentConstraints};
227-
use lightning::ln::channel_state::{ChannelCounterparty, ChannelShutdownState};
243+
use lightning::ln::channel_state::{
244+
ChannelCounterparty, ChannelShutdownState, CounterpartyForwardingInfo,
245+
};
228246
use lightning::ln::types::ChannelId;
229247
use lightning::offers::invoice_request::InvoiceRequestFields;
230248
use lightning::offers::offer::OfferId;
231-
use lightning::types::features::InitFeatures;
249+
use lightning::types::features::{ChannelFeatures, InitFeatures};
232250
use lightning::types::payment::PaymentSecret;
233251
use std::collections::BTreeMap;
234252

253+
use crate::logger::Logger;
254+
235255
#[derive(Clone)]
236256
struct TestEntropy;
237257

@@ -327,7 +347,11 @@ mod tests {
327347
node_id: pubkey(13),
328348
features: InitFeatures::empty(),
329349
unspendable_punishment_reserve: 0,
330-
forwarding_info: None,
350+
forwarding_info: Some(CounterpartyForwardingInfo {
351+
fee_base_msat: 0,
352+
fee_proportional_millionths: 0,
353+
cltv_expiry_delta: 18,
354+
}),
331355
outbound_htlc_minimum_msat: None,
332356
outbound_htlc_maximum_msat: None,
333357
},
@@ -364,6 +388,26 @@ mod tests {
364388
}
365389
}
366390

391+
fn empty_graph() -> Arc<Graph> {
392+
Arc::new(Graph::new(Network::Regtest, Arc::new(Logger::new_log_facade())))
393+
}
394+
395+
fn announced_graph(recipient: PublicKey, include_peer: bool) -> Arc<Graph> {
396+
let graph = empty_graph();
397+
let counterparty = if include_peer { pubkey(13) } else { pubkey(14) };
398+
graph
399+
.add_channel_from_partial_announcement(
400+
42,
401+
None,
402+
0,
403+
ChannelFeatures::empty(),
404+
NodeId::from_pubkey(&recipient),
405+
NodeId::from_pubkey(&counterparty),
406+
)
407+
.unwrap();
408+
graph
409+
}
410+
367411
fn payment_tlvs(metadata: BTreeMap<u64, Vec<u8>>) -> ReceiveTlvs {
368412
ReceiveTlvs {
369413
payment_secret: PaymentSecret([2; 32]),
@@ -398,7 +442,7 @@ mod tests {
398442
};
399443
let metadata = payment_metadata(parameters);
400444
let inner_router = MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::None };
401-
let router = LSPS2Router::new(inner_router, TestEntropy);
445+
let router = LSPS2Router::new(inner_router, empty_graph(), TestEntropy);
402446

403447
let paths = router
404448
.create_blinded_payment_paths(
@@ -433,7 +477,7 @@ mod tests {
433477
};
434478
let metadata = payment_metadata(parameters);
435479
let inner_router = MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::None };
436-
let router = LSPS2Router::new(inner_router, TestEntropy);
480+
let router = LSPS2Router::new(inner_router, empty_graph(), TestEntropy);
437481

438482
assert!(router
439483
.create_blinded_payment_paths(
@@ -458,7 +502,7 @@ mod tests {
458502
};
459503
let metadata = payment_metadata(parameters);
460504
let inner_router = MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::None };
461-
let router = LSPS2Router::new(inner_router, TestEntropy);
505+
let router = LSPS2Router::new(inner_router, empty_graph(), TestEntropy);
462506

463507
let paths = router
464508
.create_blinded_payment_paths(
@@ -488,7 +532,7 @@ mod tests {
488532
let metadata = payment_metadata(parameters);
489533
let inner_router =
490534
MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::ChannelBacked };
491-
let router = LSPS2Router::new(inner_router, TestEntropy);
535+
let router = LSPS2Router::new(inner_router, empty_graph(), TestEntropy);
492536
let recipient = pubkey(10);
493537

494538
let paths = router
@@ -515,7 +559,7 @@ mod tests {
515559
let recipient = pubkey(10);
516560
let inner_router =
517561
MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::DirectRecipient };
518-
let router = LSPS2Router::new(inner_router, TestEntropy);
562+
let router = LSPS2Router::new(inner_router, empty_graph(), TestEntropy);
519563

520564
assert!(
521565
router
@@ -544,7 +588,7 @@ mod tests {
544588
};
545589
let inner_router =
546590
MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::DirectRecipient };
547-
let router = LSPS2Router::new(inner_router, TestEntropy);
591+
let router = LSPS2Router::new(inner_router, announced_graph(recipient, true), TestEntropy);
548592

549593
let paths = router
550594
.create_blinded_payment_paths(
@@ -560,6 +604,73 @@ mod tests {
560604
assert_eq!(paths[0].introduction_node(), &IntroductionNode::NodeId(recipient));
561605
}
562606

607+
#[test]
608+
fn ignores_capacity_without_forwarding_info() {
609+
let recipient = pubkey(10);
610+
let lsp_node_id = pubkey(11);
611+
let parameters = LSPS2LeaseParameters {
612+
lsp_node_id,
613+
intercept_scid: 42,
614+
cltv_expiry_delta: 48,
615+
payment_size_msat: Some(3_000),
616+
valid_until: u64::MAX,
617+
};
618+
let mut missing_forwarding_info = first_hop(3_000, None, None);
619+
missing_forwarding_info.counterparty.forwarding_info = None;
620+
let inner_router =
621+
MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::DirectRecipient };
622+
let router = LSPS2Router::new(inner_router, announced_graph(recipient, true), TestEntropy);
623+
let paths = router
624+
.create_blinded_payment_paths(
625+
recipient,
626+
ReceiveAuthKey([3; 32]),
627+
vec![missing_forwarding_info],
628+
payment_tlvs(payment_metadata(parameters)),
629+
Some(3_000),
630+
&Secp256k1::new(),
631+
)
632+
.unwrap();
633+
634+
assert_eq!(
635+
paths[0].introduction_node(),
636+
&IntroductionNode::NodeId(lsp_node_id),
637+
"capacity without forwarding parameters hid the JIT path"
638+
);
639+
}
640+
641+
#[test]
642+
fn ignores_capacity_from_unannounced_peer() {
643+
let recipient = pubkey(10);
644+
let lsp_node_id = pubkey(11);
645+
let parameters = LSPS2LeaseParameters {
646+
lsp_node_id,
647+
intercept_scid: 42,
648+
cltv_expiry_delta: 48,
649+
payment_size_msat: Some(3_000),
650+
valid_until: u64::MAX,
651+
};
652+
653+
let inner_router =
654+
MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::DirectRecipient };
655+
let router = LSPS2Router::new(inner_router, announced_graph(recipient, false), TestEntropy);
656+
let paths = router
657+
.create_blinded_payment_paths(
658+
recipient,
659+
ReceiveAuthKey([3; 32]),
660+
vec![first_hop(3_000, None, None)],
661+
payment_tlvs(payment_metadata(parameters)),
662+
Some(3_000),
663+
&Secp256k1::new(),
664+
)
665+
.unwrap();
666+
667+
assert_eq!(
668+
paths[0].introduction_node(),
669+
&IntroductionNode::NodeId(lsp_node_id),
670+
"capacity through an unannounced peer hid the JIT path"
671+
);
672+
}
673+
563674
#[test]
564675
fn requires_one_sufficient_non_mpp_channel() {
565676
let recipient = pubkey(10);
@@ -573,7 +684,7 @@ mod tests {
573684
};
574685
let inner_router =
575686
MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::DirectRecipient };
576-
let router = LSPS2Router::new(inner_router, TestEntropy);
687+
let router = LSPS2Router::new(inner_router, announced_graph(recipient, true), TestEntropy);
577688

578689
let paths = router
579690
.create_blinded_payment_paths(
@@ -602,7 +713,7 @@ mod tests {
602713
};
603714
let inner_router =
604715
MockRouter { calls: AtomicUsize::new(0), path_kind: MockPathKind::DirectRecipient };
605-
let router = LSPS2Router::new(inner_router, TestEntropy);
716+
let router = LSPS2Router::new(inner_router, empty_graph(), TestEntropy);
606717

607718
let paths = router
608719
.create_blinded_payment_paths(

0 commit comments

Comments
 (0)