@@ -617,6 +617,18 @@ impl PaymentParameters {
617
617
Self :: from_node_id ( payee_pubkey, final_cltv_expiry_delta) . with_bolt11_features ( InvoiceFeatures :: for_keysend ( ) ) . expect ( "PaymentParameters::from_node_id should always initialize the payee as unblinded" )
618
618
}
619
619
620
+ /// Creates parameters for paying to a blinded payee.
621
+ pub fn blinded ( blinded_route_hints : Vec < ( BlindedPayInfo , BlindedPath ) > ) -> Self {
622
+ Self {
623
+ payee : Payee :: Blinded ( blinded_route_hints) ,
624
+ expiry_time : None ,
625
+ max_total_cltv_expiry_delta : DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA ,
626
+ max_path_count : DEFAULT_MAX_PATH_COUNT ,
627
+ max_channel_saturation_power_of_half : 2 ,
628
+ previously_failed_channels : Vec :: new ( ) ,
629
+ }
630
+ }
631
+
620
632
/// Includes the payee's features. Errors if the parameters were initialized with blinded payment
621
633
/// paths.
622
634
///
@@ -722,6 +734,19 @@ impl Payee {
722
734
_ => None ,
723
735
}
724
736
}
737
+ fn blinded_route_hints ( & self ) -> & [ ( BlindedPayInfo , BlindedPath ) ] {
738
+ match self {
739
+ Self :: Blinded ( hints) => & hints[ ..] ,
740
+ Self :: Clear { .. } => & [ ]
741
+ }
742
+ }
743
+
744
+ fn clear_route_hints ( & self ) -> & [ RouteHint ] {
745
+ match self {
746
+ Self :: Blinded ( _) => & [ ] ,
747
+ Self :: Clear { route_hints, .. } => & route_hints[ ..]
748
+ }
749
+ }
725
750
}
726
751
727
752
/// A list of hops along a payment path terminating with a channel to the recipient.
@@ -1157,10 +1182,10 @@ pub(crate) fn get_route<L: Deref, S: Score>(
1157
1182
_random_seed_bytes : & [ u8 ; 32 ]
1158
1183
) -> Result < Route , LightningError >
1159
1184
where L :: Target : Logger {
1160
- let payee_node_id = payment_params. payee . node_id ( ) . map ( |pk| NodeId :: from_pubkey ( & pk) ) ;
1161
- const DUMMY_BLINDED_PAYEE_ID : [ u8 ; 33 ] = [ 42u8 ; 33 ] ;
1162
- let target_pubkey = payment_params. payee . node_id ( ) . unwrap_or_else ( || PublicKey :: from_slice ( & DUMMY_BLINDED_PAYEE_ID ) . unwrap ( ) ) ;
1163
- let target_node_id = NodeId :: from_pubkey ( & target_pubkey) ;
1185
+ let mut payee_node_id = payment_params. payee . node_id ( ) . map ( |pk| NodeId :: from_pubkey ( & pk) ) ;
1186
+ const DUMMY_BLINDED_PAYEE_ID : [ u8 ; 33 ] = [ 02 ; 33 ] ;
1187
+ let mut target_pubkey = payment_params. payee . node_id ( ) . unwrap_or_else ( || PublicKey :: from_slice ( & DUMMY_BLINDED_PAYEE_ID ) . unwrap ( ) ) ;
1188
+ let mut target_node_id = NodeId :: from_pubkey ( & target_pubkey) ;
1164
1189
let our_node_id = NodeId :: from_pubkey ( & our_node_pubkey) ;
1165
1190
1166
1191
if payee_node_id. map_or ( false , |payee| payee == our_node_id) {
@@ -1185,8 +1210,22 @@ where L::Target: Logger {
1185
1210
}
1186
1211
}
1187
1212
} ,
1188
- _ => return Err ( LightningError { err : "Routing to blinded paths isn't supported yet" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ,
1189
-
1213
+ Payee :: Blinded ( hints) => {
1214
+ for ( _, blinded_path) in hints. iter ( ) {
1215
+ if blinded_path. blinded_hops . len ( ) == 0 {
1216
+ return Err ( LightningError { err : "0-hop blinded path provided" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1217
+ } else if & blinded_path. introduction_node_id == our_node_pubkey {
1218
+ return Err ( LightningError { err : "Cannot generate a route to blinded path with ourselves as the intro node id" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1219
+ } else if blinded_path. blinded_hops . len ( ) == 1 {
1220
+ if payee_node_id. is_some ( ) && target_pubkey != blinded_path. introduction_node_id {
1221
+ return Err ( LightningError { err : format ! ( "1-hop blinded paths must all have matching introduction node ids. Had node id {}, got node id {}" , payee_node_id. unwrap( ) , blinded_path. introduction_node_id) , action : ErrorAction :: IgnoreError } ) ;
1222
+ }
1223
+ payee_node_id = Some ( NodeId :: from_pubkey ( & blinded_path. introduction_node_id ) ) ;
1224
+ target_pubkey = blinded_path. introduction_node_id ;
1225
+ target_node_id = NodeId :: from_pubkey ( & target_pubkey) ;
1226
+ }
1227
+ }
1228
+ }
1190
1229
}
1191
1230
let final_cltv_expiry_delta = payment_params. payee . final_cltv_expiry_delta ( ) . unwrap_or ( 0 ) ;
1192
1231
if payment_params. max_total_cltv_expiry_delta <= final_cltv_expiry_delta {
@@ -1295,6 +1334,27 @@ where L::Target: Logger {
1295
1334
return Err ( LightningError { err : "Cannot route when there are no outbound routes away from us" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ;
1296
1335
}
1297
1336
}
1337
+ // Marshall blinded route hints
1338
+ let mut blinded_route_hints = Vec :: with_capacity ( payment_params. payee . blinded_route_hints ( ) . len ( ) ) ;
1339
+ const DUMMY_BLINDED_SCID : u64 = 0 ;
1340
+ for ( blinded_payinfo, blinded_path) in payment_params. payee . blinded_route_hints ( ) . iter ( ) {
1341
+ if blinded_path. blinded_hops . len ( ) == 1 {
1342
+ // If the introduction node is the destination, this hint is for a public node and we can just
1343
+ // use the public network graph
1344
+ continue
1345
+ }
1346
+ blinded_route_hints. push ( RouteHintHop {
1347
+ src_node_id : blinded_path. introduction_node_id ,
1348
+ short_channel_id : DUMMY_BLINDED_SCID ,
1349
+ fees : RoutingFees {
1350
+ base_msat : blinded_payinfo. fee_base_msat ,
1351
+ proportional_millionths : blinded_payinfo. fee_proportional_millionths ,
1352
+ } ,
1353
+ cltv_expiry_delta : blinded_payinfo. cltv_expiry_delta ,
1354
+ htlc_minimum_msat : Some ( blinded_payinfo. htlc_minimum_msat ) ,
1355
+ htlc_maximum_msat : Some ( blinded_payinfo. htlc_maximum_msat ) ,
1356
+ } ) ;
1357
+ }
1298
1358
1299
1359
// The main heap containing all candidate next-hops sorted by their score (max(fee,
1300
1360
// htlc_minimum)). Ideally this would be a heap which allowed cheap score reduction instead of
@@ -1710,11 +1770,20 @@ where L::Target: Logger {
1710
1770
// If a caller provided us with last hops, add them to routing targets. Since this happens
1711
1771
// earlier than general path finding, they will be somewhat prioritized, although currently
1712
1772
// it matters only if the fees are exactly the same.
1713
- let route_hints = match & payment_params. payee {
1714
- Payee :: Clear { route_hints, .. } => route_hints,
1715
- _ => return Err ( LightningError { err : "Routing to blinded paths isn't supported yet" . to_owned ( ) , action : ErrorAction :: IgnoreError } ) ,
1716
- } ;
1717
- for route in route_hints. iter ( ) . filter ( |route| !route. 0 . is_empty ( ) ) {
1773
+ for hint in blinded_route_hints. iter ( ) {
1774
+ let have_hop_src_in_graph =
1775
+ // Only add the hops in this route to our candidate set if either
1776
+ // we have a direct channel to the first hop or the first hop is
1777
+ // in the regular network graph.
1778
+ first_hop_targets. get ( & NodeId :: from_pubkey ( & hint. src_node_id ) ) . is_some ( ) ||
1779
+ network_nodes. get ( & NodeId :: from_pubkey ( & hint. src_node_id ) ) . is_some ( ) ;
1780
+ if have_hop_src_in_graph {
1781
+ add_entry ! ( CandidateRouteHop :: PrivateHop { hint } ,
1782
+ NodeId :: from_pubkey( & hint. src_node_id) ,
1783
+ target_node_id, 0 , path_value_msat, 0 , 0_u64 , 0 , 0 ) ;
1784
+ }
1785
+ }
1786
+ for route in payment_params. payee . clear_route_hints ( ) . iter ( ) . filter ( |route| !route. 0 . is_empty ( ) ) {
1718
1787
let first_hop_in_route = & ( route. 0 ) [ 0 ] ;
1719
1788
let have_hop_src_in_graph =
1720
1789
// Only add the hops in this route to our candidate set if either
@@ -2133,7 +2202,31 @@ where L::Target: Logger {
2133
2202
for results_vec in selected_paths {
2134
2203
let mut hops = Vec :: with_capacity ( results_vec. len ( ) ) ;
2135
2204
for res in results_vec { hops. push ( res?) ; }
2136
- paths. push ( Path { hops, blinded_tail : None } ) ;
2205
+ let blinded_path = payment_params. payee . blinded_route_hints ( ) . iter ( )
2206
+ . find ( |( _, p) | {
2207
+ let intro_node_idx = if p. blinded_hops . len ( ) == 1 { hops. len ( ) - 1 }
2208
+ else { hops. len ( ) . saturating_sub ( 2 ) } ;
2209
+ p. introduction_node_id == hops[ intro_node_idx] . pubkey
2210
+ } ) . map ( |( _, p) | p. clone ( ) ) ;
2211
+ let blinded_tail = if let Some ( BlindedPath { blinded_hops, blinding_point, .. } ) = blinded_path {
2212
+ let num_blinded_hops = blinded_hops. len ( ) ;
2213
+ Some ( BlindedTail {
2214
+ hops : blinded_hops,
2215
+ blinding_point,
2216
+ excess_final_cltv_expiry_delta : 0 ,
2217
+ final_value_msat : {
2218
+ if num_blinded_hops > 1 {
2219
+ hops. pop ( ) . unwrap ( ) . fee_msat
2220
+ } else {
2221
+ let final_amt_msat = hops. last ( ) . unwrap ( ) . fee_msat ;
2222
+ hops. last_mut ( ) . unwrap ( ) . fee_msat = 0 ;
2223
+ debug_assert_eq ! ( hops. last( ) . unwrap( ) . cltv_expiry_delta, 0 ) ;
2224
+ final_amt_msat
2225
+ }
2226
+ }
2227
+ } )
2228
+ } else { None } ;
2229
+ paths. push ( Path { hops, blinded_tail } ) ;
2137
2230
}
2138
2231
let route = Route {
2139
2232
paths,
@@ -2323,9 +2416,10 @@ mod tests {
2323
2416
use crate :: routing:: test_utils:: { add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel} ;
2324
2417
use crate :: chain:: transaction:: OutPoint ;
2325
2418
use crate :: chain:: keysinterface:: EntropySource ;
2326
- use crate :: ln:: features:: { ChannelFeatures , InitFeatures , NodeFeatures } ;
2419
+ use crate :: ln:: features:: { BlindedHopFeatures , ChannelFeatures , InitFeatures , NodeFeatures } ;
2327
2420
use crate :: ln:: msgs:: { ErrorAction , LightningError , UnsignedChannelUpdate , MAX_VALUE_MSAT } ;
2328
2421
use crate :: ln:: channelmanager;
2422
+ use crate :: offers:: invoice:: BlindedPayInfo ;
2329
2423
use crate :: util:: config:: UserConfig ;
2330
2424
use crate :: util:: test_utils as ln_test_utils;
2331
2425
use crate :: util:: chacha20:: ChaCha20 ;
@@ -5961,6 +6055,131 @@ mod tests {
5961
6055
assert_eq ! ( route. paths[ 0 ] . blinded_tail. as_ref( ) . unwrap( ) . excess_final_cltv_expiry_delta, 40 ) ;
5962
6056
assert_eq ! ( route. paths[ 0 ] . hops. last( ) . unwrap( ) . cltv_expiry_delta, 40 ) ;
5963
6057
}
6058
+
6059
+ #[ test]
6060
+ fn simple_blinded_route_hints ( ) {
6061
+ do_simple_route_hints ( 1 ) ;
6062
+ do_simple_route_hints ( 2 ) ;
6063
+ do_simple_route_hints ( 3 ) ;
6064
+ }
6065
+
6066
+ fn do_simple_route_hints ( num_blinded_hops : usize ) {
6067
+ // Check that we can generate a route to a blinded path with the expected hops.
6068
+ let ( secp_ctx, network, _, _, logger) = build_graph ( ) ;
6069
+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
6070
+ let network_graph = network. read_only ( ) ;
6071
+
6072
+ let scorer = ln_test_utils:: TestScorer :: new ( ) ;
6073
+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
6074
+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
6075
+
6076
+ let mut blinded_path = BlindedPath {
6077
+ introduction_node_id : nodes[ 2 ] ,
6078
+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
6079
+ blinded_hops : Vec :: with_capacity ( num_blinded_hops) ,
6080
+ } ;
6081
+ for i in 0 ..num_blinded_hops {
6082
+ blinded_path. blinded_hops . push (
6083
+ BlindedHop { blinded_node_id : ln_test_utils:: pubkey ( 42 + i as u8 ) , encrypted_payload : vec ! [ 0 ; 32 ] } ,
6084
+ ) ;
6085
+ }
6086
+ let blinded_payinfo = BlindedPayInfo {
6087
+ fee_base_msat : 100 ,
6088
+ fee_proportional_millionths : 500 ,
6089
+ htlc_minimum_msat : 1000 ,
6090
+ htlc_maximum_msat : 100_000_000 ,
6091
+ cltv_expiry_delta : 15 ,
6092
+ features : BlindedHopFeatures :: empty ( ) ,
6093
+ } ;
6094
+
6095
+ let final_amt_msat = 1001 ;
6096
+ let payment_params = PaymentParameters :: blinded ( vec ! [ ( blinded_payinfo. clone( ) , blinded_path. clone( ) ) ] ) ;
6097
+ let route = get_route ( & our_id, & payment_params, & network_graph, None , final_amt_msat , Arc :: clone ( & logger) ,
6098
+ & scorer, & random_seed_bytes) . unwrap ( ) ;
6099
+ assert_eq ! ( route. paths. len( ) , 1 ) ;
6100
+ assert_eq ! ( route. paths[ 0 ] . hops. len( ) , 2 ) ;
6101
+
6102
+ let tail = route. paths [ 0 ] . blinded_tail . as_ref ( ) . unwrap ( ) ;
6103
+ assert_eq ! ( tail. hops, blinded_path. blinded_hops) ;
6104
+ assert_eq ! ( tail. excess_final_cltv_expiry_delta, 0 ) ;
6105
+ assert_eq ! ( tail. final_value_msat, 1001 ) ;
6106
+
6107
+ let final_hop = route. paths [ 0 ] . hops . last ( ) . unwrap ( ) ;
6108
+ assert_eq ! ( final_hop. pubkey, blinded_path. introduction_node_id) ;
6109
+ if tail. hops . len ( ) > 1 {
6110
+ assert_eq ! ( final_hop. fee_msat,
6111
+ blinded_payinfo. fee_base_msat as u64 + blinded_payinfo. fee_proportional_millionths as u64 * tail. final_value_msat / 1000000 ) ;
6112
+ assert_eq ! ( final_hop. cltv_expiry_delta, blinded_payinfo. cltv_expiry_delta as u32 ) ;
6113
+ } else {
6114
+ assert_eq ! ( final_hop. fee_msat, 0 ) ;
6115
+ assert_eq ! ( final_hop. cltv_expiry_delta, 0 ) ;
6116
+ }
6117
+ }
6118
+
6119
+ #[ test]
6120
+ fn blinded_path_routing_errors ( ) {
6121
+ // Check that we can generate a route to a blinded path with the expected hops.
6122
+ let ( secp_ctx, network, _, _, logger) = build_graph ( ) ;
6123
+ let ( _, our_id, _, nodes) = get_nodes ( & secp_ctx) ;
6124
+ let network_graph = network. read_only ( ) ;
6125
+
6126
+ let scorer = ln_test_utils:: TestScorer :: new ( ) ;
6127
+ let keys_manager = ln_test_utils:: TestKeysInterface :: new ( & [ 0u8 ; 32 ] , Network :: Testnet ) ;
6128
+ let random_seed_bytes = keys_manager. get_secure_random_bytes ( ) ;
6129
+
6130
+ let mut invalid_blinded_path = BlindedPath {
6131
+ introduction_node_id : nodes[ 2 ] ,
6132
+ blinding_point : ln_test_utils:: pubkey ( 42 ) ,
6133
+ blinded_hops : vec ! [
6134
+ BlindedHop { blinded_node_id: ln_test_utils:: pubkey( 43 ) , encrypted_payload: vec![ 0 ; 43 ] } ,
6135
+ ] ,
6136
+ } ;
6137
+ let blinded_payinfo = BlindedPayInfo {
6138
+ fee_base_msat : 100 ,
6139
+ fee_proportional_millionths : 500 ,
6140
+ htlc_minimum_msat : 1000 ,
6141
+ htlc_maximum_msat : 100_000_000 ,
6142
+ cltv_expiry_delta : 15 ,
6143
+ features : BlindedHopFeatures :: empty ( ) ,
6144
+ } ;
6145
+
6146
+ // let payee_pubkey = ln_test_utils::pubkey(45);
6147
+ let mut invalid_blinded_path_2 = invalid_blinded_path. clone ( ) ;
6148
+ invalid_blinded_path_2. introduction_node_id = ln_test_utils:: pubkey ( 45 ) ;
6149
+ let payment_params = PaymentParameters :: blinded ( vec ! [
6150
+ ( blinded_payinfo. clone( ) , invalid_blinded_path. clone( ) ) ,
6151
+ ( blinded_payinfo. clone( ) , invalid_blinded_path_2) ] ) ;
6152
+ match get_route ( & our_id, & payment_params, & network_graph, None , 1001 , Arc :: clone ( & logger) ,
6153
+ & scorer, & random_seed_bytes)
6154
+ {
6155
+ Err ( LightningError { err, .. } ) => {
6156
+ assert_eq ! ( err, format!( "1-hop blinded paths must all have matching introduction node ids. Had node id {}, got node id {}" , nodes[ 2 ] , ln_test_utils:: pubkey( 45 ) ) ) ;
6157
+ } ,
6158
+ _ => panic ! ( "Expected error" )
6159
+ }
6160
+
6161
+ invalid_blinded_path. introduction_node_id = our_id;
6162
+ let payment_params = PaymentParameters :: blinded ( vec ! [ ( blinded_payinfo. clone( ) , invalid_blinded_path. clone( ) ) ] ) ;
6163
+ match get_route ( & our_id, & payment_params, & network_graph, None , 1001 , Arc :: clone ( & logger) ,
6164
+ & scorer, & random_seed_bytes)
6165
+ {
6166
+ Err ( LightningError { err, .. } ) => {
6167
+ assert_eq ! ( err, "Cannot generate a route to blinded path with ourselves as the intro node id" ) ;
6168
+ } ,
6169
+ _ => panic ! ( "Expected error" )
6170
+ }
6171
+
6172
+ invalid_blinded_path. blinded_hops . clear ( ) ;
6173
+ let payment_params = PaymentParameters :: blinded ( vec ! [ ( blinded_payinfo, invalid_blinded_path) ] ) ;
6174
+ match get_route ( & our_id, & payment_params, & network_graph, None , 1001 , Arc :: clone ( & logger) ,
6175
+ & scorer, & random_seed_bytes)
6176
+ {
6177
+ Err ( LightningError { err, .. } ) => {
6178
+ assert_eq ! ( err, "0-hop blinded path provided" ) ;
6179
+ } ,
6180
+ _ => panic ! ( "Expected error" )
6181
+ }
6182
+ }
5964
6183
}
5965
6184
5966
6185
#[ cfg( all( test, not( feature = "no-std" ) ) ) ]
0 commit comments