Skip to content

Commit d249732

Browse files
Implement routing to blinded payment paths
Sending to them is still disallowed, for now
1 parent 215ea05 commit d249732

File tree

1 file changed

+232
-13
lines changed

1 file changed

+232
-13
lines changed

lightning/src/routing/router.rs

Lines changed: 232 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,18 @@ impl PaymentParameters {
617617
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")
618618
}
619619

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+
620632
/// Includes the payee's features. Errors if the parameters were initialized with blinded payment
621633
/// paths.
622634
///
@@ -722,6 +734,19 @@ impl Payee {
722734
_ => None,
723735
}
724736
}
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+
}
725750
}
726751

727752
/// 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>(
11571182
_random_seed_bytes: &[u8; 32]
11581183
) -> Result<Route, LightningError>
11591184
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);
11641189
let our_node_id = NodeId::from_pubkey(&our_node_pubkey);
11651190

11661191
if payee_node_id.map_or(false, |payee| payee == our_node_id) {
@@ -1185,8 +1210,22 @@ where L::Target: Logger {
11851210
}
11861211
}
11871212
},
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+
}
11901229
}
11911230
let final_cltv_expiry_delta = payment_params.payee.final_cltv_expiry_delta().unwrap_or(0);
11921231
if payment_params.max_total_cltv_expiry_delta <= final_cltv_expiry_delta {
@@ -1295,6 +1334,27 @@ where L::Target: Logger {
12951334
return Err(LightningError{err: "Cannot route when there are no outbound routes away from us".to_owned(), action: ErrorAction::IgnoreError});
12961335
}
12971336
}
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+
}
12981358

12991359
// The main heap containing all candidate next-hops sorted by their score (max(fee,
13001360
// htlc_minimum)). Ideally this would be a heap which allowed cheap score reduction instead of
@@ -1710,11 +1770,20 @@ where L::Target: Logger {
17101770
// If a caller provided us with last hops, add them to routing targets. Since this happens
17111771
// earlier than general path finding, they will be somewhat prioritized, although currently
17121772
// 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()) {
17181787
let first_hop_in_route = &(route.0)[0];
17191788
let have_hop_src_in_graph =
17201789
// Only add the hops in this route to our candidate set if either
@@ -2133,7 +2202,31 @@ where L::Target: Logger {
21332202
for results_vec in selected_paths {
21342203
let mut hops = Vec::with_capacity(results_vec.len());
21352204
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 });
21372230
}
21382231
let route = Route {
21392232
paths,
@@ -2323,9 +2416,10 @@ mod tests {
23232416
use crate::routing::test_utils::{add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel};
23242417
use crate::chain::transaction::OutPoint;
23252418
use crate::chain::keysinterface::EntropySource;
2326-
use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures};
2419+
use crate::ln::features::{BlindedHopFeatures, ChannelFeatures, InitFeatures, NodeFeatures};
23272420
use crate::ln::msgs::{ErrorAction, LightningError, UnsignedChannelUpdate, MAX_VALUE_MSAT};
23282421
use crate::ln::channelmanager;
2422+
use crate::offers::invoice::BlindedPayInfo;
23292423
use crate::util::config::UserConfig;
23302424
use crate::util::test_utils as ln_test_utils;
23312425
use crate::util::chacha20::ChaCha20;
@@ -5961,6 +6055,131 @@ mod tests {
59616055
assert_eq!(route.paths[0].blinded_tail.as_ref().unwrap().excess_final_cltv_expiry_delta, 40);
59626056
assert_eq!(route.paths[0].hops.last().unwrap().cltv_expiry_delta, 40);
59636057
}
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+
}
59646183
}
59656184

59666185
#[cfg(all(test, not(feature = "no-std")))]

0 commit comments

Comments
 (0)