@@ -435,6 +435,10 @@ const MEDIAN_HOP_CLTV_EXPIRY_DELTA: u32 = 40;
435435// down from (1300-93) / 61 = 19.78... to arrive at a conservative estimate of 19.
436436const MAX_PATH_LENGTH_ESTIMATE : u8 = 19 ;
437437
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+
438442/// The recipient of a payment.
439443#[ derive( Clone , Debug , Hash , PartialEq , Eq ) ]
440444pub struct PaymentParameters {
@@ -589,6 +593,13 @@ impl PaymentParameters {
589593 Self { route_hints : Hints :: Clear ( route_hints) , ..self }
590594 }
591595
596+ /// Includes blinded hints for routing to the payee.
597+ ///
598+ /// (C-not exported) since bindings don't support move semantics
599+ pub fn with_blinded_route_hints ( self , blinded_route_hints : Vec < ( BlindedPayInfo , BlindedPath ) > ) -> Self {
600+ Self { route_hints : Hints :: Blinded ( blinded_route_hints) , ..self }
601+ }
602+
592603 /// Includes a payment expiration in seconds relative to the UNIX epoch.
593604 ///
594605 /// (C-not exported) since bindings don't support move semantics
@@ -628,6 +639,15 @@ pub enum Hints {
628639 Clear ( Vec < RouteHint > ) ,
629640}
630641
642+ impl Hints {
643+ fn blinded_len ( & self ) -> usize {
644+ match self {
645+ Self :: Blinded ( hints) => hints. len ( ) ,
646+ Self :: Clear ( _) => 0 ,
647+ }
648+ }
649+ }
650+
631651/// A list of hops along a payment path terminating with a channel to the recipient.
632652#[ derive( Clone , Debug , Hash , Eq , PartialEq ) ]
633653pub struct RouteHint ( pub Vec < RouteHintHop > ) ;
@@ -1087,7 +1107,18 @@ where L::Target: Logger {
10871107 }
10881108 }
10891109 } ,
1090- _ => todo ! ( )
1110+ Hints :: Blinded ( hints) => {
1111+ for ( _, blinded_path) in hints. iter ( ) {
1112+ let intro_node_is_payee = blinded_path. introduction_node_id == payment_params. payee_pubkey ;
1113+ if blinded_path. blinded_hops . len ( ) > 1 && intro_node_is_payee {
1114+ return Err ( LightningError { err : "Blinded path cannot have the payee as the source." . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1115+ } else if !intro_node_is_payee && blinded_path. blinded_hops . len ( ) == 1 {
1116+ 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 } ) ;
1117+ } else if blinded_path. blinded_hops . len ( ) == 0 {
1118+ return Err ( LightningError { err : "0-hop blinded path provided" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1119+ }
1120+ }
1121+ }
10911122 }
10921123 if payment_params. max_total_cltv_expiry_delta <= final_cltv_expiry_delta {
10931124 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 } ) ;
@@ -1575,6 +1606,26 @@ where L::Target: Logger {
15751606
15761607 let mut payment_paths = Vec :: < PaymentPath > :: new ( ) ;
15771608
1609+ let mut route_hints = Vec :: with_capacity ( payment_params. route_hints . blinded_len ( ) ) ;
1610+ let route_hints_ref = match & payment_params. route_hints {
1611+ Hints :: Clear ( hints) => hints,
1612+ Hints :: Blinded ( blinded_hints) => {
1613+ for ( blinded_payinfo, blinded_path) in blinded_hints {
1614+ route_hints. push ( RouteHint ( vec ! [ RouteHintHop {
1615+ src_node_id: blinded_path. introduction_node_id,
1616+ short_channel_id: BLINDED_PATH_SCID ,
1617+ fees: RoutingFees {
1618+ base_msat: blinded_payinfo. fee_base_msat,
1619+ proportional_millionths: blinded_payinfo. fee_proportional_millionths,
1620+ } ,
1621+ cltv_expiry_delta: blinded_payinfo. cltv_expiry_delta,
1622+ htlc_minimum_msat: Some ( blinded_payinfo. htlc_minimum_msat) ,
1623+ htlc_maximum_msat: Some ( blinded_payinfo. htlc_maximum_msat) ,
1624+ } ] ) ) ;
1625+ }
1626+ & route_hints
1627+ }
1628+ } ;
15781629 // TODO: diversify by nodes (so that all paths aren't doomed if one node is offline).
15791630 ' paths_collection: loop {
15801631 // For every new path, start from scratch, except for used_channel_liquidities, which
@@ -1612,11 +1663,7 @@ where L::Target: Logger {
16121663 // If a caller provided us with last hops, add them to routing targets. Since this happens
16131664 // earlier than general path finding, they will be somewhat prioritized, although currently
16141665 // 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 ( ) ) {
1666+ for route in route_hints_ref. iter ( ) . filter ( |route| !route. 0 . is_empty ( ) ) {
16201667 let first_hop_in_route = & ( route. 0 ) [ 0 ] ;
16211668 let have_hop_src_in_graph =
16221669 // Only add the hops in this route to our candidate set if either
@@ -2035,7 +2082,16 @@ where L::Target: Logger {
20352082 for results_vec in selected_paths {
20362083 let mut hops = Vec :: new ( ) ;
20372084 for res in results_vec { hops. push ( res?) ; }
2038- paths. push ( Path { hops, blinded_tail : None } ) ;
2085+ let mut blinded_tail = None ;
2086+ if let Hints :: Blinded ( hints) = & payment_params. route_hints {
2087+ blinded_tail = hints. iter ( )
2088+ . find ( |( _, p) | {
2089+ let intro_node_idx = if p. blinded_hops . len ( ) == 1 { hops. len ( ) - 1 } else { hops. len ( ) - 2 } ;
2090+ p. introduction_node_id == hops[ intro_node_idx] . pubkey
2091+ } )
2092+ . map ( |( _, p) | p. clone ( ) ) ;
2093+ }
2094+ paths. push ( Path { hops, blinded_tail } ) ;
20392095 }
20402096 let route = Route {
20412097 paths,
@@ -2216,12 +2272,14 @@ mod tests {
22162272 use crate :: routing:: utxo:: UtxoResult ;
22172273 use crate :: routing:: router:: { get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features,
22182274 Path , PaymentParameters , Route , RouteHint , RouteHintHop , RouteHop , RoutingFees ,
2219- DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA , MAX_PATH_LENGTH_ESTIMATE } ;
2275+ BLINDED_PATH_SCID , DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA , MAX_PATH_LENGTH_ESTIMATE } ;
22202276 use crate :: routing:: scoring:: { ChannelUsage , FixedPenaltyScorer , Score , ProbabilisticScorer , ProbabilisticScoringParameters } ;
22212277 use crate :: routing:: test_utils:: { add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel} ;
2278+ use crate :: blinded_path:: { BlindedHop , BlindedPath } ;
22222279 use crate :: chain:: transaction:: OutPoint ;
22232280 use crate :: chain:: keysinterface:: EntropySource ;
2224- use crate :: ln:: features:: { ChannelFeatures , InitFeatures , NodeFeatures } ;
2281+ use crate :: offers:: invoice:: BlindedPayInfo ;
2282+ use crate :: ln:: features:: { BlindedHopFeatures , ChannelFeatures , InitFeatures , NodeFeatures } ;
22252283 use crate :: ln:: msgs:: { ErrorAction , LightningError , UnsignedChannelUpdate , MAX_VALUE_MSAT } ;
22262284 use crate :: ln:: channelmanager;
22272285 use crate :: util:: config:: UserConfig ;
@@ -5712,6 +5770,48 @@ mod tests {
57125770 let route = get_route ( & our_id, & payment_params, & network_graph. read_only ( ) , None , 100 , 42 , Arc :: clone ( & logger) , & scorer, & random_seed_bytes) ;
57135771 assert ! ( route. is_ok( ) ) ;
57145772 }
5773+
5774+ #[ test]
5775+ fn simple_blinded_path_routing ( ) {
5776+ // Check that we can generate a route to a blinded path with the expected hops.
5777+ let ( secp_ctx, network, _, _, logger) = build_graph ( ) ;
5778+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
5779+ let network_graph = network. read_only ( ) ;
5780+
5781+ let scorer = ln_test_utils:: TestScorer :: new ( ) ;
5782+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
5783+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
5784+
5785+ let blinded_path = BlindedPath {
5786+ introduction_node_id : nodes[ 2 ] ,
5787+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
5788+ blinded_hops : vec ! [
5789+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 43 ) , encrypted_payload: vec![ 0 ; 43 ] } ,
5790+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 44 ) , encrypted_payload: vec![ 0 ; 44 ] } ,
5791+ ] ,
5792+ } ;
5793+ let blinded_payinfo = BlindedPayInfo {
5794+ fee_base_msat : 100 ,
5795+ fee_proportional_millionths : 500 ,
5796+ htlc_minimum_msat : 1000 ,
5797+ htlc_maximum_msat : 100_000_000 ,
5798+ cltv_expiry_delta : 15 ,
5799+ features : BlindedHopFeatures :: empty ( ) ,
5800+ } ;
5801+
5802+ let payee_pubkey = ln_test_utils:: pubkey ( 45 ) ;
5803+ let payment_params = PaymentParameters :: from_node_id ( payee_pubkey, 0 )
5804+ . with_blinded_route_hints ( vec ! [ ( blinded_payinfo, blinded_path. clone( ) ) ] ) ;
5805+ let route = get_route ( & our_id, & payment_params, & network_graph, None , 1001 , 0 ,
5806+ Arc :: clone ( & logger) , & scorer, & random_seed_bytes) . unwrap ( ) ;
5807+ assert_eq ! ( route. paths. len( ) , 1 ) ;
5808+ assert_eq ! ( route. paths[ 0 ] . hops. len( ) , 3 ) ;
5809+ assert_eq ! ( route. paths[ 0 ] . len( ) , 5 ) ;
5810+ assert_eq ! ( route. paths[ 0 ] . hops[ 2 ] . pubkey, payee_pubkey) ;
5811+ assert_eq ! ( route. paths[ 0 ] . hops[ 2 ] . short_channel_id, BLINDED_PATH_SCID ) ;
5812+ assert_eq ! ( route. paths[ 0 ] . hops[ 1 ] . pubkey, nodes[ 2 ] ) ;
5813+ assert_eq ! ( route. paths[ 0 ] . blinded_tail, Some ( blinded_path) ) ;
5814+ }
57155815}
57165816
57175817#[ cfg( all( test, not( feature = "no-std" ) ) ) ]
0 commit comments