@@ -267,7 +267,8 @@ impl Path {
267
267
268
268
/// The length of this path, including the blinded hops of its [`Path::blinded_tail`], if any.
269
269
pub fn len ( & self ) -> usize {
270
- self . hops . len ( ) + self . blinded_tail . as_ref ( ) . map_or ( 0 , |path| path. blinded_hops . len ( ) )
270
+ self . hops . len ( ) + self . blinded_tail . as_ref ( )
271
+ . map_or ( 0 , |path| path. blinded_hops . len ( ) . saturating_sub ( 2 ) )
271
272
}
272
273
273
274
pub ( crate ) fn iter ( & self ) -> core:: slice:: Iter < RouteHop > {
@@ -451,6 +452,10 @@ const MEDIAN_HOP_CLTV_EXPIRY_DELTA: u32 = 40;
451
452
// down from (1300-93) / 61 = 19.78... to arrive at a conservative estimate of 19.
452
453
const MAX_PATH_LENGTH_ESTIMATE : u8 = 19 ;
453
454
455
+ /// We need to create RouteHintHops for blinded pathfinding, but we don't have an scid, so use a
456
+ /// dummy value.
457
+ const BLINDED_PATH_SCID : u64 = 0 ;
458
+
454
459
/// The recipient of a payment.
455
460
#[ derive( Clone , Debug , Hash , PartialEq , Eq ) ]
456
461
pub struct PaymentParameters {
@@ -605,6 +610,14 @@ impl PaymentParameters {
605
610
Self { route_hints : Hints :: Clear ( route_hints) , ..self }
606
611
}
607
612
613
+ /// Includes blinded hints for routing to the payee.
614
+ ///
615
+ /// (C-not exported) since bindings don't support move semantics
616
+ #[ cfg( test) ] // TODO: make this public when we allow sending to blinded recipients
617
+ pub fn with_blinded_route_hints ( self , blinded_route_hints : Vec < ( BlindedPayInfo , BlindedPath ) > ) -> Self {
618
+ Self { route_hints : Hints :: Blinded ( blinded_route_hints) , ..self }
619
+ }
620
+
608
621
/// Includes a payment expiration in seconds relative to the UNIX epoch.
609
622
///
610
623
/// (C-not exported) since bindings don't support move semantics
@@ -644,6 +657,15 @@ pub enum Hints {
644
657
Clear ( Vec < RouteHint > ) ,
645
658
}
646
659
660
+ impl Hints {
661
+ fn blinded_len ( & self ) -> usize {
662
+ match self {
663
+ Self :: Blinded ( hints) => hints. len ( ) ,
664
+ Self :: Clear ( _) => 0 ,
665
+ }
666
+ }
667
+ }
668
+
647
669
/// A list of hops along a payment path terminating with a channel to the recipient.
648
670
#[ derive( Clone , Debug , Hash , Eq , PartialEq ) ]
649
671
pub struct RouteHint ( pub Vec < RouteHintHop > ) ;
@@ -1103,7 +1125,18 @@ where L::Target: Logger {
1103
1125
}
1104
1126
}
1105
1127
} ,
1106
- _ => todo ! ( )
1128
+ Hints :: Blinded ( hints) => {
1129
+ for ( _, blinded_path) in hints. iter ( ) {
1130
+ let intro_node_is_payee = blinded_path. introduction_node_id == payment_params. payee_pubkey ;
1131
+ if blinded_path. blinded_hops . len ( ) > 1 && intro_node_is_payee {
1132
+ return Err ( LightningError { err : "Blinded path cannot have the payee as the source" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1133
+ } else if !intro_node_is_payee && blinded_path. blinded_hops . len ( ) == 1 {
1134
+ return Err ( LightningError { err : format ! ( "1-hop blinded path introduction node id {} did not match payee {}" , blinded_path. introduction_node_id, payment_params. payee_pubkey) , action : ErrorAction :: IgnoreError } ) ;
1135
+ } else if blinded_path. blinded_hops . len ( ) == 0 {
1136
+ return Err ( LightningError { err : "0-hop blinded path provided" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1137
+ }
1138
+ }
1139
+ }
1107
1140
}
1108
1141
if payment_params. max_total_cltv_expiry_delta <= final_cltv_expiry_delta {
1109
1142
return Err ( LightningError { err : "Can't find a route where the maximum total CLTV expiry delta is below the final CLTV expiry." . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
@@ -1216,6 +1249,28 @@ where L::Target: Logger {
1216
1249
}
1217
1250
}
1218
1251
1252
+ // Marshall route hints
1253
+ let mut route_hints = Vec :: with_capacity ( payment_params. route_hints . blinded_len ( ) ) ;
1254
+ let route_hints_ref = match & payment_params. route_hints {
1255
+ Hints :: Clear ( hints) => hints,
1256
+ Hints :: Blinded ( blinded_hints) => {
1257
+ for ( blinded_payinfo, blinded_path) in blinded_hints {
1258
+ route_hints. push ( RouteHint ( vec ! [ RouteHintHop {
1259
+ src_node_id: blinded_path. introduction_node_id,
1260
+ short_channel_id: BLINDED_PATH_SCID ,
1261
+ fees: RoutingFees {
1262
+ base_msat: blinded_payinfo. fee_base_msat,
1263
+ proportional_millionths: blinded_payinfo. fee_proportional_millionths,
1264
+ } ,
1265
+ cltv_expiry_delta: blinded_payinfo. cltv_expiry_delta,
1266
+ htlc_minimum_msat: Some ( blinded_payinfo. htlc_minimum_msat) ,
1267
+ htlc_maximum_msat: Some ( blinded_payinfo. htlc_maximum_msat) ,
1268
+ } ] ) ) ;
1269
+ }
1270
+ & route_hints
1271
+ }
1272
+ } ;
1273
+
1219
1274
// The main heap containing all candidate next-hops sorted by their score (max(fee,
1220
1275
// htlc_minimum)). Ideally this would be a heap which allowed cheap score reduction instead of
1221
1276
// adding duplicate entries when we find a better path to a given node.
@@ -1628,11 +1683,7 @@ where L::Target: Logger {
1628
1683
// If a caller provided us with last hops, add them to routing targets. Since this happens
1629
1684
// earlier than general path finding, they will be somewhat prioritized, although currently
1630
1685
// it matters only if the fees are exactly the same.
1631
- let route_hints = match & payment_params. route_hints {
1632
- Hints :: Clear ( hints) => hints,
1633
- _ => todo ! ( )
1634
- } ;
1635
- for route in route_hints. iter ( ) . filter ( |route| !route. 0 . is_empty ( ) ) {
1686
+ for route in route_hints_ref. iter ( ) . filter ( |route| !route. 0 . is_empty ( ) ) {
1636
1687
let first_hop_in_route = & ( route. 0 ) [ 0 ] ;
1637
1688
let have_hop_src_in_graph =
1638
1689
// Only add the hops in this route to our candidate set if either
@@ -2051,7 +2102,16 @@ where L::Target: Logger {
2051
2102
for results_vec in selected_paths {
2052
2103
let mut hops = Vec :: new ( ) ;
2053
2104
for res in results_vec { hops. push ( res?) ; }
2054
- paths. push ( Path { hops, blinded_tail : None } ) ;
2105
+ let mut blinded_tail = None ;
2106
+ if let Hints :: Blinded ( hints) = & payment_params. route_hints {
2107
+ blinded_tail = hints. iter ( )
2108
+ . find ( |( _, p) | {
2109
+ let intro_node_idx = if p. blinded_hops . len ( ) == 1 { hops. len ( ) - 1 } else { hops. len ( ) - 2 } ;
2110
+ p. introduction_node_id == hops[ intro_node_idx] . pubkey
2111
+ } )
2112
+ . map ( |( _, p) | p. clone ( ) ) ;
2113
+ }
2114
+ paths. push ( Path { hops, blinded_tail } ) ;
2055
2115
}
2056
2116
let route = Route {
2057
2117
paths,
@@ -2232,12 +2292,14 @@ mod tests {
2232
2292
use crate :: routing:: utxo:: UtxoResult ;
2233
2293
use crate :: routing:: router:: { get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features,
2234
2294
Path , PaymentParameters , Route , RouteHint , RouteHintHop , RouteHop , RoutingFees ,
2235
- DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA , MAX_PATH_LENGTH_ESTIMATE } ;
2295
+ BLINDED_PATH_SCID , DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA , MAX_PATH_LENGTH_ESTIMATE } ;
2236
2296
use crate :: routing:: scoring:: { ChannelUsage , FixedPenaltyScorer , Score , ProbabilisticScorer , ProbabilisticScoringParameters } ;
2237
2297
use crate :: routing:: test_utils:: { add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel} ;
2298
+ use crate :: blinded_path:: { BlindedHop , BlindedPath } ;
2238
2299
use crate :: chain:: transaction:: OutPoint ;
2239
2300
use crate :: chain:: keysinterface:: EntropySource ;
2240
- use crate :: ln:: features:: { ChannelFeatures , InitFeatures , NodeFeatures } ;
2301
+ use crate :: offers:: invoice:: BlindedPayInfo ;
2302
+ use crate :: ln:: features:: { BlindedHopFeatures , ChannelFeatures , InitFeatures , NodeFeatures } ;
2241
2303
use crate :: ln:: msgs:: { ErrorAction , LightningError , UnsignedChannelUpdate , MAX_VALUE_MSAT } ;
2242
2304
use crate :: ln:: channelmanager;
2243
2305
use crate :: util:: config:: UserConfig ;
@@ -5728,6 +5790,122 @@ mod tests {
5728
5790
let route = get_route ( & our_id, & payment_params, & network_graph. read_only ( ) , None , 100 , 42 , Arc :: clone ( & logger) , & scorer, & random_seed_bytes) ;
5729
5791
assert ! ( route. is_ok( ) ) ;
5730
5792
}
5793
+
5794
+ #[ test]
5795
+ fn simple_blinded_route_hints ( ) {
5796
+ do_simple_route_hints ( 1 , 2 , 2 ) ;
5797
+ do_simple_route_hints ( 2 , 3 , 3 ) ;
5798
+ do_simple_route_hints ( 3 , 3 , 4 ) ;
5799
+ }
5800
+
5801
+ fn do_simple_route_hints ( num_blinded_hops : usize , expected_hops_len : usize , expected_path_len : usize ) {
5802
+ // Check that we can generate a route to a blinded path with the expected hops.
5803
+ let ( secp_ctx, network, _, _, logger) = build_graph ( ) ;
5804
+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
5805
+ let network_graph = network. read_only ( ) ;
5806
+
5807
+ let scorer = ln_test_utils:: TestScorer :: new ( ) ;
5808
+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
5809
+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
5810
+
5811
+ let payee_pubkey = if num_blinded_hops == 1 { nodes[ 2 ] } else { ln_test_utils:: pubkey ( 45 ) } ;
5812
+ let mut blinded_path = BlindedPath {
5813
+ introduction_node_id : nodes[ 2 ] ,
5814
+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
5815
+ blinded_hops : Vec :: with_capacity ( num_blinded_hops) ,
5816
+ } ;
5817
+ for i in 0 ..num_blinded_hops {
5818
+ blinded_path. blinded_hops . push (
5819
+ BlindedHop { blinded_node_id : ln_test_utils:: pubkey ( 42 + i as u8 ) , encrypted_payload : vec ! [ 0 ; 32 ] } ,
5820
+ ) ;
5821
+ }
5822
+ let blinded_payinfo = BlindedPayInfo {
5823
+ fee_base_msat : 100 ,
5824
+ fee_proportional_millionths : 500 ,
5825
+ htlc_minimum_msat : 1000 ,
5826
+ htlc_maximum_msat : 100_000_000 ,
5827
+ cltv_expiry_delta : 15 ,
5828
+ features : BlindedHopFeatures :: empty ( ) ,
5829
+ } ;
5830
+
5831
+ let payment_params = PaymentParameters :: from_node_id ( payee_pubkey, 0 )
5832
+ . with_blinded_route_hints ( vec ! [ ( blinded_payinfo, blinded_path. clone( ) ) ] ) ;
5833
+ let route = get_route ( & our_id, & payment_params, & network_graph, None , 1001 , 0 ,
5834
+ Arc :: clone ( & logger) , & scorer, & random_seed_bytes) . unwrap ( ) ;
5835
+ assert_eq ! ( route. paths. len( ) , 1 ) ;
5836
+ assert_eq ! ( route. paths[ 0 ] . hops. len( ) , expected_hops_len) ;
5837
+ assert_eq ! ( route. paths[ 0 ] . len( ) , expected_path_len) ;
5838
+ assert_eq ! ( route. paths[ 0 ] . hops[ route. paths[ 0 ] . hops. len( ) - 1 ] . pubkey, payee_pubkey) ;
5839
+ assert_eq ! ( route. paths[ 0 ] . blinded_tail, Some ( blinded_path) ) ;
5840
+ if num_blinded_hops > 1 {
5841
+ assert_eq ! ( route. paths[ 0 ] . hops[ route. paths[ 0 ] . hops. len( ) - 1 ] . short_channel_id, BLINDED_PATH_SCID ) ;
5842
+ }
5843
+ }
5844
+
5845
+ #[ test]
5846
+ fn blinded_path_routing_errors ( ) {
5847
+ // Check that we can generate a route to a blinded path with the expected hops.
5848
+ let ( secp_ctx, network, _, _, logger) = build_graph ( ) ;
5849
+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
5850
+ let network_graph = network. read_only ( ) ;
5851
+
5852
+ let scorer = ln_test_utils:: TestScorer :: new ( ) ;
5853
+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
5854
+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
5855
+
5856
+ let mut invalid_blinded_path = BlindedPath {
5857
+ introduction_node_id : nodes[ 2 ] ,
5858
+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
5859
+ blinded_hops : vec ! [
5860
+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 43 ) , encrypted_payload: vec![ 0 ; 43 ] } ,
5861
+ ] ,
5862
+ } ;
5863
+ let blinded_payinfo = BlindedPayInfo {
5864
+ fee_base_msat : 100 ,
5865
+ fee_proportional_millionths : 500 ,
5866
+ htlc_minimum_msat : 1000 ,
5867
+ htlc_maximum_msat : 100_000_000 ,
5868
+ cltv_expiry_delta : 15 ,
5869
+ features : BlindedHopFeatures :: empty ( ) ,
5870
+ } ;
5871
+
5872
+ let payee_pubkey = ln_test_utils:: pubkey ( 45 ) ;
5873
+ let payment_params = PaymentParameters :: from_node_id ( payee_pubkey, 0 )
5874
+ . with_blinded_route_hints ( vec ! [ ( blinded_payinfo. clone( ) , invalid_blinded_path. clone( ) ) ] ) ;
5875
+ match get_route ( & our_id, & payment_params, & network_graph, None , 1001 , 0 ,
5876
+ Arc :: clone ( & logger) , & scorer, & random_seed_bytes)
5877
+ {
5878
+ Err ( LightningError { err, .. } ) => {
5879
+ assert_eq ! ( err, format!( "1-hop blinded path introduction node id {} did not match payee {}" , nodes[ 2 ] , payee_pubkey) ) ;
5880
+ } ,
5881
+ _ => panic ! ( "Expected error" )
5882
+ }
5883
+
5884
+ invalid_blinded_path. introduction_node_id = payee_pubkey;
5885
+ invalid_blinded_path. blinded_hops . push ( BlindedHop { blinded_node_id : ln_test_utils:: pubkey ( 43 ) , encrypted_payload : vec ! [ 0 ; 44 ] } ) ;
5886
+ let payment_params = PaymentParameters :: from_node_id ( payee_pubkey, 0 )
5887
+ . with_blinded_route_hints ( vec ! [ ( blinded_payinfo. clone( ) , invalid_blinded_path. clone( ) ) ] ) ;
5888
+ match get_route ( & our_id, & payment_params, & network_graph, None , 1001 , 0 ,
5889
+ Arc :: clone ( & logger) , & scorer, & random_seed_bytes)
5890
+ {
5891
+ Err ( LightningError { err, .. } ) => {
5892
+ assert_eq ! ( err, "Blinded path cannot have the payee as the source" ) ;
5893
+ } ,
5894
+ _ => panic ! ( "Expected error" )
5895
+ }
5896
+
5897
+ invalid_blinded_path. blinded_hops . clear ( ) ;
5898
+ let payment_params = PaymentParameters :: from_node_id ( payee_pubkey, 0 )
5899
+ . with_blinded_route_hints ( vec ! [ ( blinded_payinfo, invalid_blinded_path) ] ) ;
5900
+ match get_route ( & our_id, & payment_params, & network_graph, None , 1001 , 0 ,
5901
+ Arc :: clone ( & logger) , & scorer, & random_seed_bytes)
5902
+ {
5903
+ Err ( LightningError { err, .. } ) => {
5904
+ assert_eq ! ( err, "0-hop blinded path provided" ) ;
5905
+ } ,
5906
+ _ => panic ! ( "Expected error" )
5907
+ }
5908
+ }
5731
5909
}
5732
5910
5733
5911
#[ cfg( all( test, not( feature = "no-std" ) ) ) ]
0 commit comments