@@ -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
361377impl 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]
0 commit comments