Skip to content

Commit 3f57558

Browse files
committed
Properly consider blinded paths in InFlightHtlcs
When paying a BOLT 12 invoice an amount greater than any one of multiple blinded paths, we need to track how much is in-flight across the paths when retrying. Here we do so, tracking blinded path usage in a new field in `InFlightHtlcs`. Fixes #2737
1 parent a5d0767 commit 3f57558

File tree

2 files changed

+149
-8
lines changed

2 files changed

+149
-8
lines changed

lightning/src/routing/router.rs

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,18 @@ where
323323
type ScoreParams = <S::Target as ScoreLookUp>::ScoreParams;
324324
#[rustfmt::skip]
325325
fn channel_penalty_msat(&self, candidate: &CandidateRouteHop, usage: ChannelUsage, score_params: &Self::ScoreParams) -> u64 {
326+
if let CandidateRouteHop::Blinded(blinded_candidate) = candidate {
327+
if let Some(used_liquidity) = self.inflight_htlcs.used_blinded_liquidity_msat(
328+
*blinded_candidate.source_node_id, blinded_candidate.hint.blinding_point(),
329+
) {
330+
let usage = ChannelUsage {
331+
inflight_htlc_msat: usage.inflight_htlc_msat.saturating_add(used_liquidity),
332+
..usage
333+
};
334+
335+
return self.scorer.channel_penalty_msat(candidate, usage, score_params);
336+
}
337+
}
326338
let target = match candidate.target() {
327339
Some(target) => target,
328340
None => return self.scorer.channel_penalty_msat(candidate, usage, score_params),
@@ -356,12 +368,16 @@ pub struct InFlightHtlcs {
356368
// key is less than its destination. See `InFlightHtlcs::used_liquidity_msat` for more
357369
// details.
358370
unblinded_hops: HashMap<(u64, bool), u64>,
371+
/// A map with liquidity value (in msat) keyed by the introduction point of a blinded path and
372+
/// the blinding point. In general blinding points should be globally unique, but just in case
373+
/// we add the introduction point as well.
374+
blinded_hops: HashMap<(NodeId, PublicKey), u64>,
359375
}
360376

361377
impl InFlightHtlcs {
362378
/// Constructs an empty `InFlightHtlcs`.
363379
pub fn new() -> Self {
364-
InFlightHtlcs { unblinded_hops: new_hash_map() }
380+
InFlightHtlcs { unblinded_hops: new_hash_map(), blinded_hops: new_hash_map() }
365381
}
366382

367383
/// Takes in a path with payer's node id and adds the path's details to `InFlightHtlcs`.
@@ -373,6 +389,19 @@ impl InFlightHtlcs {
373389
let mut cumulative_msat = 0;
374390
if let Some(tail) = &path.blinded_tail {
375391
cumulative_msat += tail.final_value_msat;
392+
if tail.hops.len() > 1 {
393+
// Single-hop blinded paths aren't really "blinded" paths, as they terminate at the
394+
// introduction point. In that case, we don't need to track anything.
395+
let last_hop = path.hops.last().unwrap();
396+
let intro_node = NodeId::from_pubkey(&last_hop.pubkey);
397+
// The amount we send into the blinded path is the sum of the blinded path final
398+
// amount and the fee we pay in it, which is the `fee_msat` of the last hop.
399+
let blinded_path_sent_amt = last_hop.fee_msat + cumulative_msat;
400+
self.blinded_hops
401+
.entry((intro_node, tail.blinding_point))
402+
.and_modify(|used_liquidity_msat| *used_liquidity_msat += blinded_path_sent_amt)
403+
.or_insert(blinded_path_sent_amt);
404+
}
376405
}
377406

378407
// total_inflight_map needs to be direction-sensitive when keeping track of the HTLC value
@@ -414,6 +443,13 @@ impl InFlightHtlcs {
414443
) -> Option<u64> {
415444
self.unblinded_hops.get(&(channel_scid, source < target)).map(|v| *v)
416445
}
446+
447+
/// Returns liquidity in msat given the blinded path introduction point and blinding point.
448+
pub fn used_blinded_liquidity_msat(
449+
&self, introduction_point: NodeId, blinding_point: PublicKey,
450+
) -> Option<u64> {
451+
self.blinded_hops.get(&(introduction_point, blinding_point)).map(|v| *v)
452+
}
417453
}
418454

419455
/// A hop in a route, and additional metadata about it. "Hop" is defined as a node and the channel
@@ -3890,8 +3926,9 @@ mod tests {
38903926
use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId, P2PGossipSync};
38913927
use crate::routing::router::{
38923928
add_random_cltv_offset, build_route_from_hops_internal, default_node_features, get_route,
3893-
BlindedTail, CandidateRouteHop, InFlightHtlcs, Path, PaymentParameters, PublicHopCandidate,
3894-
Route, RouteHint, RouteHintHop, RouteHop, RouteParameters, RoutingFees,
3929+
BlindedPathCandidate, BlindedTail, CandidateRouteHop, InFlightHtlcs, Path,
3930+
PaymentParameters, PublicHopCandidate, Route, RouteHint, RouteHintHop, RouteHop,
3931+
RouteParameters, RoutingFees, ScorerAccountingForInFlightHtlcs,
38953932
DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE,
38963933
};
38973934
use crate::routing::scoring::{
@@ -3923,7 +3960,7 @@ mod tests {
39233960

39243961
use crate::io::Cursor;
39253962
use crate::prelude::*;
3926-
use crate::sync::Arc;
3963+
use crate::sync::{Arc, Mutex};
39273964

39283965
#[rustfmt::skip]
39293966
fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey,
@@ -7960,9 +7997,9 @@ mod tests {
79607997

79617998
#[test]
79627999
#[rustfmt::skip]
7963-
fn blinded_path_inflight_processing() {
7964-
// Ensure we'll score the channel that's inbound to a blinded path's introduction node, and
7965-
// account for the blinded tail's final amount_msat.
8000+
fn one_hop_blinded_path_inflight_processing() {
8001+
// Ensure we'll score the channel that's inbound to a one-hop blinded path's introduction
8002+
// node, and account for the blinded tail's final amount_msat.
79668003
let mut inflight_htlcs = InFlightHtlcs::new();
79678004
let path = Path {
79688005
hops: vec![RouteHop {
@@ -7994,6 +8031,99 @@ mod tests {
79948031
inflight_htlcs.process_path(&path, ln_test_utils::pubkey(44));
79958032
assert_eq!(*inflight_htlcs.unblinded_hops.get(&(42, true)).unwrap(), 301);
79968033
assert_eq!(*inflight_htlcs.unblinded_hops.get(&(43, false)).unwrap(), 201);
8034+
assert!(inflight_htlcs.blinded_hops.is_empty());
8035+
}
8036+
8037+
struct UsageTrackingScorer(Mutex<Option<ChannelUsage>>);
8038+
8039+
impl ScoreLookUp for UsageTrackingScorer {
8040+
type ScoreParams = ();
8041+
fn channel_penalty_msat(&self, _: &CandidateRouteHop, usage: ChannelUsage, _: &()) -> u64 {
8042+
let mut inner = self.0.lock().unwrap();
8043+
assert!(inner.is_none());
8044+
*inner = Some(usage);
8045+
0
8046+
}
8047+
}
8048+
8049+
#[test]
8050+
fn blinded_path_inflight_processing() {
8051+
// Ensure we'll score the channel that's inbound to a blinded path's introduction node, and
8052+
// account for the blinded tail's final amount_msat as well as track the blinded path
8053+
// in-flight.
8054+
let mut inflight_htlcs = InFlightHtlcs::new();
8055+
let blinding_point = ln_test_utils::pubkey(48);
8056+
let mut blinded_hops = Vec::new();
8057+
for i in 0..2 {
8058+
blinded_hops.push(
8059+
BlindedHop { blinded_node_id: ln_test_utils::pubkey(49 + i as u8), encrypted_payload: Vec::new() },
8060+
);
8061+
}
8062+
let intro_point = ln_test_utils::pubkey(43);
8063+
let path = Path {
8064+
hops: vec![
8065+
RouteHop {
8066+
pubkey: ln_test_utils::pubkey(42),
8067+
node_features: NodeFeatures::empty(),
8068+
short_channel_id: 42,
8069+
channel_features: ChannelFeatures::empty(),
8070+
fee_msat: 100,
8071+
cltv_expiry_delta: 0,
8072+
maybe_announced_channel: false,
8073+
},
8074+
RouteHop {
8075+
pubkey: intro_point,
8076+
node_features: NodeFeatures::empty(),
8077+
short_channel_id: 43,
8078+
channel_features: ChannelFeatures::empty(),
8079+
fee_msat: 1,
8080+
cltv_expiry_delta: 0,
8081+
maybe_announced_channel: false,
8082+
}
8083+
],
8084+
blinded_tail: Some(BlindedTail {
8085+
trampoline_hops: vec![],
8086+
hops: blinded_hops.clone(),
8087+
blinding_point,
8088+
excess_final_cltv_expiry_delta: 0,
8089+
final_value_msat: 200,
8090+
}),
8091+
};
8092+
inflight_htlcs.process_path(&path, ln_test_utils::pubkey(44));
8093+
assert_eq!(*inflight_htlcs.unblinded_hops.get(&(42, true)).unwrap(), 301);
8094+
assert_eq!(*inflight_htlcs.unblinded_hops.get(&(43, false)).unwrap(), 201);
8095+
let intro_node_id = NodeId::from_pubkey(&ln_test_utils::pubkey(43));
8096+
assert_eq!(*inflight_htlcs.blinded_hops.get(&(intro_node_id, blinding_point)).unwrap(), 201);
8097+
8098+
let tracking_scorer = UsageTrackingScorer(Mutex::new(None));
8099+
let inflight_scorer =
8100+
ScorerAccountingForInFlightHtlcs::new(&tracking_scorer, &inflight_htlcs);
8101+
8102+
let blinded_payinfo = BlindedPayInfo {
8103+
fee_base_msat: 100,
8104+
fee_proportional_millionths: 500,
8105+
htlc_minimum_msat: 1000,
8106+
htlc_maximum_msat: 100_000_000,
8107+
cltv_expiry_delta: 15,
8108+
features: BlindedHopFeatures::empty(),
8109+
};
8110+
let blinded_path = BlindedPaymentPath::from_blinded_path_and_payinfo(
8111+
intro_point, blinding_point, blinded_hops, blinded_payinfo,
8112+
);
8113+
8114+
let candidate = CandidateRouteHop::Blinded(BlindedPathCandidate {
8115+
source_node_id: &intro_node_id,
8116+
hint: &blinded_path,
8117+
hint_idx: 0,
8118+
source_node_counter: 0,
8119+
});
8120+
let empty_usage = ChannelUsage {
8121+
amount_msat: 42,
8122+
inflight_htlc_msat: 0,
8123+
effective_capacity: EffectiveCapacity::HintMaxHTLC { amount_msat: 500 },
8124+
};
8125+
inflight_scorer.channel_penalty_msat(&candidate, empty_usage, &());
8126+
assert_eq!(tracking_scorer.0.lock().unwrap().unwrap().inflight_htlc_msat, 201);
79978127
}
79988128

79998129
#[test]

lightning/src/routing/scoring.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ use crate::prelude::hash_map::Entry;
5757
use crate::prelude::*;
5858
use crate::routing::gossip::{DirectedChannelInfo, EffectiveCapacity, NetworkGraph, NodeId};
5959
use crate::routing::log_approx;
60-
use crate::routing::router::{CandidateRouteHop, Path, PublicHopCandidate};
60+
use crate::routing::router::{BlindedPathCandidate, CandidateRouteHop, Path, PublicHopCandidate};
6161
use crate::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
6262
use crate::util::logger::Logger;
6363
use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer};
@@ -1682,6 +1682,17 @@ where
16821682
CandidateRouteHop::PublicHop(PublicHopCandidate { info, short_channel_id }) => {
16831683
(short_channel_id, info.target())
16841684
},
1685+
CandidateRouteHop::Blinded(BlindedPathCandidate { hint, .. }) => {
1686+
let total_inflight_amount_msat =
1687+
usage.amount_msat.saturating_add(usage.inflight_htlc_msat);
1688+
if usage.amount_msat > hint.payinfo.htlc_maximum_msat {
1689+
return u64::max_value();
1690+
} else if total_inflight_amount_msat > hint.payinfo.htlc_maximum_msat {
1691+
return score_params.considered_impossible_penalty_msat;
1692+
} else {
1693+
return 0;
1694+
}
1695+
},
16851696
_ => return 0,
16861697
};
16871698
let source = candidate.source();

0 commit comments

Comments
 (0)