Skip to content

Commit c0c4a84

Browse files
Implement routing to blinded paths
But disallow sending payments to them, for now
1 parent c5a02c0 commit c0c4a84

File tree

3 files changed

+125
-9
lines changed

3 files changed

+125
-9
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: 112 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,14 @@ 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+
#[cfg(test)] // TODO: make this public when we allow sending to blinded recipients
600+
pub fn with_blinded_route_hints(self, blinded_route_hints: Vec<(BlindedPayInfo, BlindedPath)>) -> Self {
601+
Self { route_hints: Hints::Blinded(blinded_route_hints), ..self }
602+
}
603+
592604
/// Includes a payment expiration in seconds relative to the UNIX epoch.
593605
///
594606
/// (C-not exported) since bindings don't support move semantics
@@ -628,6 +640,15 @@ pub enum Hints {
628640
Clear(Vec<RouteHint>),
629641
}
630642

643+
impl Hints {
644+
fn blinded_len(&self) -> usize {
645+
match self {
646+
Self::Blinded(hints) => hints.len(),
647+
Self::Clear(_) => 0,
648+
}
649+
}
650+
}
651+
631652
/// A list of hops along a payment path terminating with a channel to the recipient.
632653
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
633654
pub struct RouteHint(pub Vec<RouteHintHop>);
@@ -1087,7 +1108,18 @@ where L::Target: Logger {
10871108
}
10881109
}
10891110
},
1090-
_ => todo!()
1111+
Hints::Blinded(hints) => {
1112+
for (_, blinded_path) in hints.iter() {
1113+
let intro_node_is_payee = blinded_path.introduction_node_id == payment_params.payee_pubkey;
1114+
if blinded_path.blinded_hops.len() > 1 && intro_node_is_payee {
1115+
return Err(LightningError{err: "Blinded path cannot have the payee as the source.".to_owned(), action: ErrorAction::IgnoreError});
1116+
} else if !intro_node_is_payee && blinded_path.blinded_hops.len() == 1 {
1117+
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});
1118+
} else if blinded_path.blinded_hops.len() == 0 {
1119+
return Err(LightningError{err: "0-hop blinded path provided".to_owned(), action: ErrorAction::IgnoreError});
1120+
}
1121+
}
1122+
}
10911123
}
10921124
if payment_params.max_total_cltv_expiry_delta <= final_cltv_expiry_delta {
10931125
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});
@@ -1200,6 +1232,28 @@ where L::Target: Logger {
12001232
}
12011233
}
12021234

1235+
// Marshall route hints
1236+
let mut route_hints = Vec::with_capacity(payment_params.route_hints.blinded_len());
1237+
let route_hints_ref = match &payment_params.route_hints {
1238+
Hints::Clear(hints) => hints,
1239+
Hints::Blinded(blinded_hints) => {
1240+
for (blinded_payinfo, blinded_path) in blinded_hints {
1241+
route_hints.push(RouteHint(vec![RouteHintHop {
1242+
src_node_id: blinded_path.introduction_node_id,
1243+
short_channel_id: BLINDED_PATH_SCID,
1244+
fees: RoutingFees {
1245+
base_msat: blinded_payinfo.fee_base_msat,
1246+
proportional_millionths: blinded_payinfo.fee_proportional_millionths,
1247+
},
1248+
cltv_expiry_delta: blinded_payinfo.cltv_expiry_delta,
1249+
htlc_minimum_msat: Some(blinded_payinfo.htlc_minimum_msat),
1250+
htlc_maximum_msat: Some(blinded_payinfo.htlc_maximum_msat),
1251+
}]));
1252+
}
1253+
&route_hints
1254+
}
1255+
};
1256+
12031257
// The main heap containing all candidate next-hops sorted by their score (max(fee,
12041258
// htlc_minimum)). Ideally this would be a heap which allowed cheap score reduction instead of
12051259
// adding duplicate entries when we find a better path to a given node.
@@ -1612,11 +1666,7 @@ where L::Target: Logger {
16121666
// If a caller provided us with last hops, add them to routing targets. Since this happens
16131667
// earlier than general path finding, they will be somewhat prioritized, although currently
16141668
// 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()) {
1669+
for route in route_hints_ref.iter().filter(|route| !route.0.is_empty()) {
16201670
let first_hop_in_route = &(route.0)[0];
16211671
let have_hop_src_in_graph =
16221672
// Only add the hops in this route to our candidate set if either
@@ -2035,7 +2085,16 @@ where L::Target: Logger {
20352085
for results_vec in selected_paths {
20362086
let mut hops = Vec::new();
20372087
for res in results_vec { hops.push(res?); }
2038-
paths.push(Path { hops, blinded_tail: None });
2088+
let mut blinded_tail = None;
2089+
if let Hints::Blinded(hints) = &payment_params.route_hints {
2090+
blinded_tail = hints.iter()
2091+
.find(|(_, p)| {
2092+
let intro_node_idx = if p.blinded_hops.len() == 1 { hops.len() - 1 } else { hops.len() - 2 };
2093+
p.introduction_node_id == hops[intro_node_idx].pubkey
2094+
})
2095+
.map(|(_, p)| p.clone());
2096+
}
2097+
paths.push(Path { hops, blinded_tail });
20392098
}
20402099
let route = Route {
20412100
paths,
@@ -2216,12 +2275,14 @@ mod tests {
22162275
use crate::routing::utxo::UtxoResult;
22172276
use crate::routing::router::{get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features,
22182277
Path, PaymentParameters, Route, RouteHint, RouteHintHop, RouteHop, RoutingFees,
2219-
DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE};
2278+
BLINDED_PATH_SCID, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE};
22202279
use crate::routing::scoring::{ChannelUsage, FixedPenaltyScorer, Score, ProbabilisticScorer, ProbabilisticScoringParameters};
22212280
use crate::routing::test_utils::{add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel};
2281+
use crate::blinded_path::{BlindedHop, BlindedPath};
22222282
use crate::chain::transaction::OutPoint;
22232283
use crate::chain::keysinterface::EntropySource;
2224-
use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures};
2284+
use crate::offers::invoice::BlindedPayInfo;
2285+
use crate::ln::features::{BlindedHopFeatures, ChannelFeatures, InitFeatures, NodeFeatures};
22252286
use crate::ln::msgs::{ErrorAction, LightningError, UnsignedChannelUpdate, MAX_VALUE_MSAT};
22262287
use crate::ln::channelmanager;
22272288
use crate::util::config::UserConfig;
@@ -5712,6 +5773,48 @@ mod tests {
57125773
let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes);
57135774
assert!(route.is_ok());
57145775
}
5776+
5777+
#[test]
5778+
fn simple_blinded_path_routing() {
5779+
// Check that we can generate a route to a blinded path with the expected hops.
5780+
let (secp_ctx, network, _, _, logger) = build_graph();
5781+
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);
5782+
let network_graph = network.read_only();
5783+
5784+
let scorer = ln_test_utils::TestScorer::new();
5785+
let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet);
5786+
let random_seed_bytes = keys_manager.get_secure_random_bytes();
5787+
5788+
let blinded_path = BlindedPath {
5789+
introduction_node_id: nodes[2],
5790+
blinding_point: ln_test_utils::pubkey(42),
5791+
blinded_hops: vec![
5792+
BlindedHop { blinded_node_id: ln_test_utils::pubkey(43), encrypted_payload: vec![0; 43] },
5793+
BlindedHop { blinded_node_id: ln_test_utils::pubkey(44), encrypted_payload: vec![0; 44] },
5794+
],
5795+
};
5796+
let blinded_payinfo = BlindedPayInfo {
5797+
fee_base_msat: 100,
5798+
fee_proportional_millionths: 500,
5799+
htlc_minimum_msat: 1000,
5800+
htlc_maximum_msat: 100_000_000,
5801+
cltv_expiry_delta: 15,
5802+
features: BlindedHopFeatures::empty(),
5803+
};
5804+
5805+
let payee_pubkey = ln_test_utils::pubkey(45);
5806+
let payment_params = PaymentParameters::from_node_id(payee_pubkey, 0)
5807+
.with_blinded_route_hints(vec![(blinded_payinfo, blinded_path.clone())]);
5808+
let route = get_route(&our_id, &payment_params, &network_graph, None, 1001, 0,
5809+
Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap();
5810+
assert_eq!(route.paths.len(), 1);
5811+
assert_eq!(route.paths[0].hops.len(), 3);
5812+
assert_eq!(route.paths[0].len(), 5);
5813+
assert_eq!(route.paths[0].hops[2].pubkey, payee_pubkey);
5814+
assert_eq!(route.paths[0].hops[2].short_channel_id, BLINDED_PATH_SCID);
5815+
assert_eq!(route.paths[0].hops[1].pubkey, nodes[2]);
5816+
assert_eq!(route.paths[0].blinded_tail, Some(blinded_path));
5817+
}
57155818
}
57165819

57175820
#[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)