@@ -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) ,
@@ -350,45 +362,68 @@ where
350362/// A data structure for tracking in-flight HTLCs. May be used during pathfinding to account for
351363/// in-use channel liquidity.
352364#[ derive( Clone ) ]
353- pub struct InFlightHtlcs (
365+ pub struct InFlightHtlcs {
354366 // A map with liquidity value (in msat) keyed by a short channel id and the direction the HTLC
355367 // is traveling in. The direction boolean is determined by checking if the HTLC source's public
356368 // key is less than its destination. See `InFlightHtlcs::used_liquidity_msat` for more
357369 // details.
358- HashMap < ( u64 , bool ) , u64 > ,
359- ) ;
370+ 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 > ,
375+ }
360376
361377impl InFlightHtlcs {
362378 /// Constructs an empty `InFlightHtlcs`.
363- #[ rustfmt:: skip]
364- pub fn new ( ) -> Self { InFlightHtlcs ( new_hash_map ( ) ) }
379+ pub fn new ( ) -> Self {
380+ InFlightHtlcs { unblinded_hops : new_hash_map ( ) , blinded_hops : new_hash_map ( ) }
381+ }
365382
366383 /// Takes in a path with payer's node id and adds the path's details to `InFlightHtlcs`.
367- #[ rustfmt:: skip]
368384 pub fn process_path ( & mut self , path : & Path , payer_node_id : PublicKey ) {
369- if path. hops . is_empty ( ) { return } ;
385+ if path. hops . is_empty ( ) {
386+ return ;
387+ }
370388
371389 let mut cumulative_msat = 0 ;
372390 if let Some ( tail) = & path. blinded_tail {
373391 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_trampoline_hop =
396+ tail. trampoline_hops . last ( ) . map ( |hop| ( hop. pubkey , hop. fee_msat ) ) ;
397+ let last_normal_hop = path. hops . last ( ) . unwrap ( ) ;
398+ let last_hop = last_trampoline_hop
399+ . unwrap_or ( ( last_normal_hop. pubkey , last_normal_hop. fee_msat ) ) ;
400+ let intro_node = NodeId :: from_pubkey ( & last_hop. 0 ) ;
401+ // The amount we send into the blinded path is the sum of the blinded path final
402+ // amount and the fee we pay in it, which is the `fee_msat` of the last hop.
403+ let blinded_path_sent_amt = last_hop. 1 + cumulative_msat;
404+ self . blinded_hops
405+ . entry ( ( intro_node, tail. blinding_point ) )
406+ . and_modify ( |used_liquidity_msat| * used_liquidity_msat += blinded_path_sent_amt)
407+ . or_insert ( blinded_path_sent_amt) ;
408+ }
374409 }
375410
376411 // total_inflight_map needs to be direction-sensitive when keeping track of the HTLC value
377412 // that is held up. However, the `hops` array, which is a path returned by `find_route` in
378413 // the router excludes the payer node. In the following lines, the payer's information is
379414 // hardcoded with an inflight value of 0 so that we can correctly represent the first hop
380415 // in our sliding window of two.
381- let reversed_hops_with_payer = path. hops . iter ( ) . rev ( ) . skip ( 1 )
382- . map ( |hop| hop. pubkey )
383- . chain ( core:: iter:: once ( payer_node_id) ) ;
416+ let reversed_hops = path. hops . iter ( ) . rev ( ) . skip ( 1 ) . map ( |hop| hop. pubkey ) ;
417+ let reversed_hops_with_payer = reversed_hops. chain ( core:: iter:: once ( payer_node_id) ) ;
384418
385419 // Taking the reversed vector from above, we zip it with just the reversed hops list to
386420 // work "backwards" of the given path, since the last hop's `fee_msat` actually represents
387421 // the total amount sent.
388422 for ( next_hop, prev_hop) in path. hops . iter ( ) . rev ( ) . zip ( reversed_hops_with_payer) {
389423 cumulative_msat += next_hop. fee_msat ;
390- self . 0
391- . entry ( ( next_hop. short_channel_id , NodeId :: from_pubkey ( & prev_hop) < NodeId :: from_pubkey ( & next_hop. pubkey ) ) )
424+ let direction = NodeId :: from_pubkey ( & prev_hop) < NodeId :: from_pubkey ( & next_hop. pubkey ) ;
425+ self . unblinded_hops
426+ . entry ( ( next_hop. short_channel_id , direction) )
392427 . and_modify ( |used_liquidity_msat| * used_liquidity_msat += cumulative_msat)
393428 . or_insert ( cumulative_msat) ;
394429 }
@@ -399,7 +434,7 @@ impl InFlightHtlcs {
399434 pub fn add_inflight_htlc (
400435 & mut self , source : & NodeId , target : & NodeId , channel_scid : u64 , used_msat : u64 ,
401436 ) {
402- self . 0
437+ self . unblinded_hops
403438 . entry ( ( channel_scid, source < target) )
404439 . and_modify ( |used_liquidity_msat| * used_liquidity_msat += used_msat)
405440 . or_insert ( used_msat) ;
@@ -410,19 +445,14 @@ impl InFlightHtlcs {
410445 pub fn used_liquidity_msat (
411446 & self , source : & NodeId , target : & NodeId , channel_scid : u64 ,
412447 ) -> Option < u64 > {
413- self . 0 . get ( & ( channel_scid, source < target) ) . map ( |v| * v)
448+ self . unblinded_hops . get ( & ( channel_scid, source < target) ) . map ( |v| * v)
414449 }
415- }
416-
417- impl Writeable for InFlightHtlcs {
418- #[ rustfmt:: skip]
419- fn write < W : Writer > ( & self , writer : & mut W ) -> Result < ( ) , io:: Error > { self . 0 . write ( writer) }
420- }
421450
422- impl Readable for InFlightHtlcs {
423- fn read < R : io:: Read > ( reader : & mut R ) -> Result < Self , DecodeError > {
424- let infight_map: HashMap < ( u64 , bool ) , u64 > = Readable :: read ( reader) ?;
425- Ok ( Self ( infight_map) )
451+ /// Returns liquidity in msat given the blinded path introduction point and blinding point.
452+ pub fn used_blinded_liquidity_msat (
453+ & self , introduction_point : NodeId , blinding_point : PublicKey ,
454+ ) -> Option < u64 > {
455+ self . blinded_hops . get ( & ( introduction_point, blinding_point) ) . map ( |v| * v)
426456 }
427457}
428458
@@ -3899,8 +3929,9 @@ mod tests {
38993929 use crate :: routing:: gossip:: { EffectiveCapacity , NetworkGraph , NodeId , P2PGossipSync } ;
39003930 use crate :: routing:: router:: {
39013931 add_random_cltv_offset, build_route_from_hops_internal, default_node_features, get_route,
3902- BlindedTail , CandidateRouteHop , InFlightHtlcs , Path , PaymentParameters , PublicHopCandidate ,
3903- Route , RouteHint , RouteHintHop , RouteHop , RouteParameters , RoutingFees ,
3932+ BlindedPathCandidate , BlindedTail , CandidateRouteHop , InFlightHtlcs , Path ,
3933+ PaymentParameters , PublicHopCandidate , Route , RouteHint , RouteHintHop , RouteHop ,
3934+ RouteParameters , RoutingFees , ScorerAccountingForInFlightHtlcs ,
39043935 DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA , MAX_PATH_LENGTH_ESTIMATE ,
39053936 } ;
39063937 use crate :: routing:: scoring:: {
@@ -3932,7 +3963,7 @@ mod tests {
39323963
39333964 use crate :: io:: Cursor ;
39343965 use crate :: prelude:: * ;
3935- use crate :: sync:: Arc ;
3966+ use crate :: sync:: { Arc , Mutex } ;
39363967
39373968 #[ rustfmt:: skip]
39383969 fn get_channel_details ( short_channel_id : Option < u64 > , node_id : PublicKey ,
@@ -7969,9 +8000,9 @@ mod tests {
79698000
79708001 #[ test]
79718002 #[ rustfmt:: skip]
7972- fn blinded_path_inflight_processing ( ) {
7973- // Ensure we'll score the channel that's inbound to a blinded path's introduction node, and
7974- // account for the blinded tail's final amount_msat.
8003+ fn one_hop_blinded_path_inflight_processing ( ) {
8004+ // Ensure we'll score the channel that's inbound to a one-hop blinded path's introduction
8005+ // node, and account for the blinded tail's final amount_msat.
79758006 let mut inflight_htlcs = InFlightHtlcs :: new ( ) ;
79768007 let path = Path {
79778008 hops : vec ! [ RouteHop {
@@ -8001,8 +8032,108 @@ mod tests {
80018032 } ) ,
80028033 } ;
80038034 inflight_htlcs. process_path ( & path, ln_test_utils:: pubkey ( 44 ) ) ;
8004- assert_eq ! ( * inflight_htlcs. 0 . get( & ( 42 , true ) ) . unwrap( ) , 301 ) ;
8005- assert_eq ! ( * inflight_htlcs. 0 . get( & ( 43 , false ) ) . unwrap( ) , 201 ) ;
8035+ assert_eq ! ( * inflight_htlcs. unblinded_hops. get( & ( 42 , true ) ) . unwrap( ) , 301 ) ;
8036+ assert_eq ! ( * inflight_htlcs. unblinded_hops. get( & ( 43 , false ) ) . unwrap( ) , 201 ) ;
8037+ assert ! ( inflight_htlcs. blinded_hops. is_empty( ) ) ;
8038+ }
8039+
8040+ struct UsageTrackingScorer ( Mutex < Option < ChannelUsage > > ) ;
8041+
8042+ impl ScoreLookUp for UsageTrackingScorer {
8043+ type ScoreParams = ( ) ;
8044+ fn channel_penalty_msat ( & self , _: & CandidateRouteHop , usage : ChannelUsage , _: & ( ) ) -> u64 {
8045+ let mut inner = self . 0 . lock ( ) . unwrap ( ) ;
8046+ assert ! ( inner. is_none( ) ) ;
8047+ * inner = Some ( usage) ;
8048+ 0
8049+ }
8050+ }
8051+
8052+ #[ test]
8053+ fn blinded_path_inflight_processing ( ) {
8054+ // Ensure we'll score the channel that's inbound to a blinded path's introduction node, and
8055+ // account for the blinded tail's final amount_msat as well as track the blinded path
8056+ // in-flight.
8057+ let mut inflight_htlcs = InFlightHtlcs :: new ( ) ;
8058+ let blinding_point = ln_test_utils:: pubkey ( 48 ) ;
8059+ let mut blinded_hops = Vec :: new ( ) ;
8060+ for i in 0 ..2 {
8061+ blinded_hops. push ( BlindedHop {
8062+ blinded_node_id : ln_test_utils:: pubkey ( 49 + i as u8 ) ,
8063+ encrypted_payload : Vec :: new ( ) ,
8064+ } ) ;
8065+ }
8066+ let intro_point = ln_test_utils:: pubkey ( 43 ) ;
8067+ let path = Path {
8068+ hops : vec ! [
8069+ RouteHop {
8070+ pubkey: ln_test_utils:: pubkey( 42 ) ,
8071+ node_features: NodeFeatures :: empty( ) ,
8072+ short_channel_id: 42 ,
8073+ channel_features: ChannelFeatures :: empty( ) ,
8074+ fee_msat: 100 ,
8075+ cltv_expiry_delta: 0 ,
8076+ maybe_announced_channel: false ,
8077+ } ,
8078+ RouteHop {
8079+ pubkey: intro_point,
8080+ node_features: NodeFeatures :: empty( ) ,
8081+ short_channel_id: 43 ,
8082+ channel_features: ChannelFeatures :: empty( ) ,
8083+ fee_msat: 1 ,
8084+ cltv_expiry_delta: 0 ,
8085+ maybe_announced_channel: false ,
8086+ } ,
8087+ ] ,
8088+ blinded_tail : Some ( BlindedTail {
8089+ trampoline_hops : vec ! [ ] ,
8090+ hops : blinded_hops. clone ( ) ,
8091+ blinding_point,
8092+ excess_final_cltv_expiry_delta : 0 ,
8093+ final_value_msat : 200 ,
8094+ } ) ,
8095+ } ;
8096+ inflight_htlcs. process_path ( & path, ln_test_utils:: pubkey ( 44 ) ) ;
8097+ assert_eq ! ( * inflight_htlcs. unblinded_hops. get( & ( 42 , true ) ) . unwrap( ) , 301 ) ;
8098+ assert_eq ! ( * inflight_htlcs. unblinded_hops. get( & ( 43 , false ) ) . unwrap( ) , 201 ) ;
8099+ let intro_node_id = NodeId :: from_pubkey ( & ln_test_utils:: pubkey ( 43 ) ) ;
8100+ assert_eq ! (
8101+ * inflight_htlcs. blinded_hops. get( & ( intro_node_id, blinding_point) ) . unwrap( ) ,
8102+ 201
8103+ ) ;
8104+
8105+ let tracking_scorer = UsageTrackingScorer ( Mutex :: new ( None ) ) ;
8106+ let inflight_scorer =
8107+ ScorerAccountingForInFlightHtlcs :: new ( & tracking_scorer, & inflight_htlcs) ;
8108+
8109+ let blinded_payinfo = BlindedPayInfo {
8110+ fee_base_msat : 100 ,
8111+ fee_proportional_millionths : 500 ,
8112+ htlc_minimum_msat : 1000 ,
8113+ htlc_maximum_msat : 100_000_000 ,
8114+ cltv_expiry_delta : 15 ,
8115+ features : BlindedHopFeatures :: empty ( ) ,
8116+ } ;
8117+ let blinded_path = BlindedPaymentPath :: from_blinded_path_and_payinfo (
8118+ intro_point,
8119+ blinding_point,
8120+ blinded_hops,
8121+ blinded_payinfo,
8122+ ) ;
8123+
8124+ let candidate = CandidateRouteHop :: Blinded ( BlindedPathCandidate {
8125+ source_node_id : & intro_node_id,
8126+ hint : & blinded_path,
8127+ hint_idx : 0 ,
8128+ source_node_counter : 0 ,
8129+ } ) ;
8130+ let empty_usage = ChannelUsage {
8131+ amount_msat : 42 ,
8132+ inflight_htlc_msat : 0 ,
8133+ effective_capacity : EffectiveCapacity :: HintMaxHTLC { amount_msat : 500 } ,
8134+ } ;
8135+ inflight_scorer. channel_penalty_msat ( & candidate, empty_usage, & ( ) ) ;
8136+ assert_eq ! ( tracking_scorer. 0 . lock( ) . unwrap( ) . unwrap( ) . inflight_htlc_msat, 201 ) ;
80068137 }
80078138
80088139 #[ test]
0 commit comments