@@ -435,6 +435,10 @@ const MEDIAN_HOP_CLTV_EXPIRY_DELTA: u32 = 40;
435
435
// down from (1300-93) / 61 = 19.78... to arrive at a conservative estimate of 19.
436
436
const MAX_PATH_LENGTH_ESTIMATE : u8 = 19 ;
437
437
438
+ /// We need to create RouteHintHops for blinded pathfinding, but we don't have an scid, so use a
439
+ /// dummy value.
440
+ const BLINDED_PATH_SCID : u64 = 0 ;
441
+
438
442
/// The recipient of a payment.
439
443
#[ derive( Clone , Debug , Hash , PartialEq , Eq ) ]
440
444
pub struct PaymentParameters {
@@ -589,6 +593,14 @@ impl PaymentParameters {
589
593
Self { route_hints : Hints :: Clear ( route_hints) , ..self }
590
594
}
591
595
596
+ /// Includes blinded hints for routing to the payee.
597
+ ///
598
+ /// (C-not exported) since bindings don't support move semantics
599
+ #[ cfg( test) ] // TODO: make this public when we allow sending to blinded recipients
600
+ pub fn with_blinded_route_hints ( self , blinded_route_hints : Vec < ( BlindedPayInfo , BlindedPath ) > ) -> Self {
601
+ Self { route_hints : Hints :: Blinded ( blinded_route_hints) , ..self }
602
+ }
603
+
592
604
/// Includes a payment expiration in seconds relative to the UNIX epoch.
593
605
///
594
606
/// (C-not exported) since bindings don't support move semantics
@@ -628,6 +640,15 @@ pub enum Hints {
628
640
Clear ( Vec < RouteHint > ) ,
629
641
}
630
642
643
+ impl Hints {
644
+ fn blinded_len ( & self ) -> usize {
645
+ match self {
646
+ Self :: Blinded ( hints) => hints. len ( ) ,
647
+ Self :: Clear ( _) => 0 ,
648
+ }
649
+ }
650
+ }
651
+
631
652
/// A list of hops along a payment path terminating with a channel to the recipient.
632
653
#[ derive( Clone , Debug , Hash , Eq , PartialEq ) ]
633
654
pub struct RouteHint ( pub Vec < RouteHintHop > ) ;
@@ -1087,7 +1108,18 @@ where L::Target: Logger {
1087
1108
}
1088
1109
}
1089
1110
} ,
1090
- _ => todo ! ( )
1111
+ Hints :: Blinded ( hints) => {
1112
+ for ( _, blinded_path) in hints. iter ( ) {
1113
+ let intro_node_is_payee = blinded_path. introduction_node_id == payment_params. payee_pubkey ;
1114
+ if blinded_path. blinded_hops . len ( ) > 1 && intro_node_is_payee {
1115
+ return Err ( LightningError { err : "Blinded path cannot have the payee as the source." . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1116
+ } else if !intro_node_is_payee && blinded_path. blinded_hops . len ( ) == 1 {
1117
+ 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 } ) ;
1118
+ } else if blinded_path. blinded_hops . len ( ) == 0 {
1119
+ return Err ( LightningError { err : "0-hop blinded path provided" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1120
+ }
1121
+ }
1122
+ }
1091
1123
}
1092
1124
if payment_params. max_total_cltv_expiry_delta <= final_cltv_expiry_delta {
1093
1125
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 } ) ;
@@ -1200,6 +1232,28 @@ where L::Target: Logger {
1200
1232
}
1201
1233
}
1202
1234
1235
+ // Marshall route hints
1236
+ let mut route_hints = Vec :: with_capacity ( payment_params. route_hints . blinded_len ( ) ) ;
1237
+ let route_hints_ref = match & payment_params. route_hints {
1238
+ Hints :: Clear ( hints) => hints,
1239
+ Hints :: Blinded ( blinded_hints) => {
1240
+ for ( blinded_payinfo, blinded_path) in blinded_hints {
1241
+ route_hints. push ( RouteHint ( vec ! [ RouteHintHop {
1242
+ src_node_id: blinded_path. introduction_node_id,
1243
+ short_channel_id: BLINDED_PATH_SCID ,
1244
+ fees: RoutingFees {
1245
+ base_msat: blinded_payinfo. fee_base_msat,
1246
+ proportional_millionths: blinded_payinfo. fee_proportional_millionths,
1247
+ } ,
1248
+ cltv_expiry_delta: blinded_payinfo. cltv_expiry_delta,
1249
+ htlc_minimum_msat: Some ( blinded_payinfo. htlc_minimum_msat) ,
1250
+ htlc_maximum_msat: Some ( blinded_payinfo. htlc_maximum_msat) ,
1251
+ } ] ) ) ;
1252
+ }
1253
+ & route_hints
1254
+ }
1255
+ } ;
1256
+
1203
1257
// The main heap containing all candidate next-hops sorted by their score (max(fee,
1204
1258
// htlc_minimum)). Ideally this would be a heap which allowed cheap score reduction instead of
1205
1259
// adding duplicate entries when we find a better path to a given node.
@@ -1612,11 +1666,7 @@ where L::Target: Logger {
1612
1666
// If a caller provided us with last hops, add them to routing targets. Since this happens
1613
1667
// earlier than general path finding, they will be somewhat prioritized, although currently
1614
1668
// it matters only if the fees are exactly the same.
1615
- let route_hints = match & payment_params. route_hints {
1616
- Hints :: Clear ( hints) => hints,
1617
- _ => todo ! ( )
1618
- } ;
1619
- for route in route_hints. iter ( ) . filter ( |route| !route. 0 . is_empty ( ) ) {
1669
+ for route in route_hints_ref. iter ( ) . filter ( |route| !route. 0 . is_empty ( ) ) {
1620
1670
let first_hop_in_route = & ( route. 0 ) [ 0 ] ;
1621
1671
let have_hop_src_in_graph =
1622
1672
// Only add the hops in this route to our candidate set if either
@@ -2035,7 +2085,16 @@ where L::Target: Logger {
2035
2085
for results_vec in selected_paths {
2036
2086
let mut hops = Vec :: new ( ) ;
2037
2087
for res in results_vec { hops. push ( res?) ; }
2038
- paths. push ( Path { hops, blinded_tail : None } ) ;
2088
+ let mut blinded_tail = None ;
2089
+ if let Hints :: Blinded ( hints) = & payment_params. route_hints {
2090
+ blinded_tail = hints. iter ( )
2091
+ . find ( |( _, p) | {
2092
+ let intro_node_idx = if p. blinded_hops . len ( ) == 1 { hops. len ( ) - 1 } else { hops. len ( ) - 2 } ;
2093
+ p. introduction_node_id == hops[ intro_node_idx] . pubkey
2094
+ } )
2095
+ . map ( |( _, p) | p. clone ( ) ) ;
2096
+ }
2097
+ paths. push ( Path { hops, blinded_tail } ) ;
2039
2098
}
2040
2099
let route = Route {
2041
2100
paths,
@@ -2216,12 +2275,14 @@ mod tests {
2216
2275
use crate :: routing:: utxo:: UtxoResult ;
2217
2276
use crate :: routing:: router:: { get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features,
2218
2277
Path , PaymentParameters , Route , RouteHint , RouteHintHop , RouteHop , RoutingFees ,
2219
- DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA , MAX_PATH_LENGTH_ESTIMATE } ;
2278
+ BLINDED_PATH_SCID , DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA , MAX_PATH_LENGTH_ESTIMATE } ;
2220
2279
use crate :: routing:: scoring:: { ChannelUsage , FixedPenaltyScorer , Score , ProbabilisticScorer , ProbabilisticScoringParameters } ;
2221
2280
use crate :: routing:: test_utils:: { add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel} ;
2281
+ use crate :: blinded_path:: { BlindedHop , BlindedPath } ;
2222
2282
use crate :: chain:: transaction:: OutPoint ;
2223
2283
use crate :: chain:: keysinterface:: EntropySource ;
2224
- use crate :: ln:: features:: { ChannelFeatures , InitFeatures , NodeFeatures } ;
2284
+ use crate :: offers:: invoice:: BlindedPayInfo ;
2285
+ use crate :: ln:: features:: { BlindedHopFeatures , ChannelFeatures , InitFeatures , NodeFeatures } ;
2225
2286
use crate :: ln:: msgs:: { ErrorAction , LightningError , UnsignedChannelUpdate , MAX_VALUE_MSAT } ;
2226
2287
use crate :: ln:: channelmanager;
2227
2288
use crate :: util:: config:: UserConfig ;
@@ -5712,6 +5773,48 @@ mod tests {
5712
5773
let route = get_route ( & our_id, & payment_params, & network_graph. read_only ( ) , None , 100 , 42 , Arc :: clone ( & logger) , & scorer, & random_seed_bytes) ;
5713
5774
assert ! ( route. is_ok( ) ) ;
5714
5775
}
5776
+
5777
+ #[ test]
5778
+ fn simple_blinded_path_routing ( ) {
5779
+ // Check that we can generate a route to a blinded path with the expected hops.
5780
+ let ( secp_ctx, network, _, _, logger) = build_graph ( ) ;
5781
+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
5782
+ let network_graph = network. read_only ( ) ;
5783
+
5784
+ let scorer = ln_test_utils:: TestScorer :: new ( ) ;
5785
+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
5786
+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
5787
+
5788
+ let blinded_path = BlindedPath {
5789
+ introduction_node_id : nodes[ 2 ] ,
5790
+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
5791
+ blinded_hops : vec ! [
5792
+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 43 ) , encrypted_payload: vec![ 0 ; 43 ] } ,
5793
+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 44 ) , encrypted_payload: vec![ 0 ; 44 ] } ,
5794
+ ] ,
5795
+ } ;
5796
+ let blinded_payinfo = BlindedPayInfo {
5797
+ fee_base_msat : 100 ,
5798
+ fee_proportional_millionths : 500 ,
5799
+ htlc_minimum_msat : 1000 ,
5800
+ htlc_maximum_msat : 100_000_000 ,
5801
+ cltv_expiry_delta : 15 ,
5802
+ features : BlindedHopFeatures :: empty ( ) ,
5803
+ } ;
5804
+
5805
+ let payee_pubkey = ln_test_utils:: pubkey ( 45 ) ;
5806
+ let payment_params = PaymentParameters :: from_node_id ( payee_pubkey, 0 )
5807
+ . with_blinded_route_hints ( vec ! [ ( blinded_payinfo, blinded_path. clone( ) ) ] ) ;
5808
+ let route = get_route ( & our_id, & payment_params, & network_graph, None , 1001 , 0 ,
5809
+ Arc :: clone ( & logger) , & scorer, & random_seed_bytes) . unwrap ( ) ;
5810
+ assert_eq ! ( route. paths. len( ) , 1 ) ;
5811
+ assert_eq ! ( route. paths[ 0 ] . hops. len( ) , 3 ) ;
5812
+ assert_eq ! ( route. paths[ 0 ] . len( ) , 5 ) ;
5813
+ assert_eq ! ( route. paths[ 0 ] . hops[ 2 ] . pubkey, payee_pubkey) ;
5814
+ assert_eq ! ( route. paths[ 0 ] . hops[ 2 ] . short_channel_id, BLINDED_PATH_SCID ) ;
5815
+ assert_eq ! ( route. paths[ 0 ] . hops[ 1 ] . pubkey, nodes[ 2 ] ) ;
5816
+ assert_eq ! ( route. paths[ 0 ] . blinded_tail, Some ( blinded_path) ) ;
5817
+ }
5715
5818
}
5716
5819
5717
5820
#[ cfg( all( test, not( feature = "no-std" ) ) ) ]
0 commit comments