Skip to content

Commit 6e6f76e

Browse files
Implement routing to blinded paths
1 parent 85fe2cb commit 6e6f76e

File tree

2 files changed

+118
-9
lines changed

2 files changed

+118
-9
lines changed

lightning/src/routing/router.rs

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
436436
const 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)]
440444
pub 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)]
633653
pub 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")))]

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)