Skip to content

Commit 690b10d

Browse files
Implement routing to blinded paths
But disallow sending payments to them, for now
1 parent db4f93a commit 690b10d

File tree

3 files changed

+201
-10
lines changed

3 files changed

+201
-10
lines changed

lightning/src/ln/outbound_payment.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,10 @@ impl OutboundPayments {
903903
path_errs.push(Err(APIError::InvalidRoute{err: "Path didn't go anywhere/had bogus size".to_owned()}));
904904
continue 'path_check;
905905
}
906+
if path.blinded_tail.is_some() {
907+
path_errs.push(Err(APIError::InvalidRoute{err: "Sending to blinded paths isn't supported yet".to_owned()}));
908+
continue 'path_check;
909+
}
906910
for (idx, hop) in path.iter().enumerate() {
907911
if idx != path.hops.len() - 1 && hop.pubkey == our_node_id {
908912
path_errs.push(Err(APIError::InvalidRoute{err: "Path went through us but wasn't a simple rebalance loop to us".to_owned()}));

lightning/src/routing/router.rs

Lines changed: 188 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,8 @@ impl Path {
267267

268268
/// The length of this path, including the blinded hops of its [`Path::blinded_tail`], if any.
269269
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))
271272
}
272273

273274
pub(crate) fn iter(&self) -> core::slice::Iter<RouteHop> {
@@ -451,6 +452,10 @@ const MEDIAN_HOP_CLTV_EXPIRY_DELTA: u32 = 40;
451452
// down from (1300-93) / 61 = 19.78... to arrive at a conservative estimate of 19.
452453
const MAX_PATH_LENGTH_ESTIMATE: u8 = 19;
453454

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+
454459
/// The recipient of a payment.
455460
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
456461
pub struct PaymentParameters {
@@ -605,6 +610,14 @@ impl PaymentParameters {
605610
Self { route_hints: Hints::Clear(route_hints), ..self }
606611
}
607612

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+
608621
/// Includes a payment expiration in seconds relative to the UNIX epoch.
609622
///
610623
/// (C-not exported) since bindings don't support move semantics
@@ -644,6 +657,15 @@ pub enum Hints {
644657
Clear(Vec<RouteHint>),
645658
}
646659

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+
647669
/// A list of hops along a payment path terminating with a channel to the recipient.
648670
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
649671
pub struct RouteHint(pub Vec<RouteHintHop>);
@@ -1103,7 +1125,18 @@ where L::Target: Logger {
11031125
}
11041126
}
11051127
},
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+
}
11071140
}
11081141
if payment_params.max_total_cltv_expiry_delta <= final_cltv_expiry_delta {
11091142
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 {
12161249
}
12171250
}
12181251

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+
12191274
// The main heap containing all candidate next-hops sorted by their score (max(fee,
12201275
// htlc_minimum)). Ideally this would be a heap which allowed cheap score reduction instead of
12211276
// adding duplicate entries when we find a better path to a given node.
@@ -1628,11 +1683,7 @@ where L::Target: Logger {
16281683
// If a caller provided us with last hops, add them to routing targets. Since this happens
16291684
// earlier than general path finding, they will be somewhat prioritized, although currently
16301685
// 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()) {
16361687
let first_hop_in_route = &(route.0)[0];
16371688
let have_hop_src_in_graph =
16381689
// Only add the hops in this route to our candidate set if either
@@ -2051,7 +2102,16 @@ where L::Target: Logger {
20512102
for results_vec in selected_paths {
20522103
let mut hops = Vec::new();
20532104
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 });
20552115
}
20562116
let route = Route {
20572117
paths,
@@ -2232,12 +2292,14 @@ mod tests {
22322292
use crate::routing::utxo::UtxoResult;
22332293
use crate::routing::router::{get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features,
22342294
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};
22362296
use crate::routing::scoring::{ChannelUsage, FixedPenaltyScorer, Score, ProbabilisticScorer, ProbabilisticScoringParameters};
22372297
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};
22382299
use crate::chain::transaction::OutPoint;
22392300
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};
22412303
use crate::ln::msgs::{ErrorAction, LightningError, UnsignedChannelUpdate, MAX_VALUE_MSAT};
22422304
use crate::ln::channelmanager;
22432305
use crate::util::config::UserConfig;
@@ -5728,6 +5790,122 @@ mod tests {
57285790
let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes);
57295791
assert!(route.is_ok());
57305792
}
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+
}
57315909
}
57325910

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

lightning/src/util/test_utils.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ use crate::chain::keysinterface::{InMemorySigner, Recipient, EntropySource, Node
6060
use std::time::{SystemTime, UNIX_EPOCH};
6161
use bitcoin::Sequence;
6262

63+
pub fn pubkey(byte: u8) -> PublicKey {
64+
let secp_ctx = Secp256k1::new();
65+
PublicKey::from_secret_key(&secp_ctx, &privkey(byte))
66+
}
67+
68+
pub fn privkey(byte: u8) -> SecretKey {
69+
SecretKey::from_slice(&[byte; 32]).unwrap()
70+
}
71+
6372
pub struct TestVecWriter(pub Vec<u8>);
6473
impl Writer for TestVecWriter {
6574
fn write_all(&mut self, buf: &[u8]) -> Result<(), io::Error> {

0 commit comments

Comments
 (0)