From d5c978c4a1b3d2c7b1cf4c59d221c9fc2e255802 Mon Sep 17 00:00:00 2001 From: Valentine Wallace Date: Tue, 21 Mar 2023 19:59:27 -0400 Subject: [PATCH] Implement routing to blinded paths But disallow sending payments to them, for now --- lightning/src/ln/outbound_payment.rs | 4 + lightning/src/routing/router.rs | 186 +++++++++++++++++++++++++-- lightning/src/util/test_utils.rs | 9 ++ 3 files changed, 190 insertions(+), 9 deletions(-) diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 8f636a53769..ebea8f76ed4 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -903,6 +903,10 @@ impl OutboundPayments { path_errs.push(Err(APIError::InvalidRoute{err: "Path didn't go anywhere/had bogus size".to_owned()})); continue 'path_check; } + if path.blinded_tail.is_some() { + path_errs.push(Err(APIError::InvalidRoute{err: "Sending to blinded paths isn't supported yet".to_owned()})); + continue 'path_check; + } for (idx, hop) in path.iter().enumerate() { if idx != path.hops.len() - 1 && hop.pubkey == our_node_id { path_errs.push(Err(APIError::InvalidRoute{err: "Path went through us but wasn't a simple rebalance loop to us".to_owned()})); diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index c08cd56c160..558bfad4fa9 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -435,6 +435,10 @@ const MEDIAN_HOP_CLTV_EXPIRY_DELTA: u32 = 40; // down from (1300-93) / 61 = 19.78... to arrive at a conservative estimate of 19. const MAX_PATH_LENGTH_ESTIMATE: u8 = 19; +/// We need to create RouteHintHops for blinded pathfinding, but we don't have an scid, so use a +/// dummy value. +const BLINDED_PATH_SCID: u64 = 0; + /// The recipient of a payment. #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct PaymentParameters { @@ -589,6 +593,14 @@ impl PaymentParameters { Self { route_hints: Hints::Clear(route_hints), ..self } } + /// Includes blinded hints for routing to the payee. + /// + /// (C-not exported) since bindings don't support move semantics + #[cfg(test)] // TODO: make this public when we allow sending to blinded recipients + pub fn with_blinded_route_hints(self, blinded_route_hints: Vec<(BlindedPayInfo, BlindedPath)>) -> Self { + Self { route_hints: Hints::Blinded(blinded_route_hints), ..self } + } + /// Includes a payment expiration in seconds relative to the UNIX epoch. /// /// (C-not exported) since bindings don't support move semantics @@ -628,6 +640,15 @@ pub enum Hints { Clear(Vec), } +impl Hints { + fn blinded_len(&self) -> usize { + match self { + Self::Blinded(hints) => hints.len(), + Self::Clear(_) => 0, + } + } +} + /// A list of hops along a payment path terminating with a channel to the recipient. #[derive(Clone, Debug, Hash, Eq, PartialEq)] pub struct RouteHint(pub Vec); @@ -1087,7 +1108,18 @@ where L::Target: Logger { } } }, - _ => todo!() + Hints::Blinded(hints) => { + for (_, blinded_path) in hints.iter() { + let intro_node_is_payee = blinded_path.introduction_node_id == payment_params.payee_pubkey; + if blinded_path.blinded_hops.len() > 1 && intro_node_is_payee { + return Err(LightningError{err: "Blinded path cannot have the payee as the source".to_owned(), action: ErrorAction::IgnoreError}); + } else if !intro_node_is_payee && blinded_path.blinded_hops.len() == 1 { + 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}); + } else if blinded_path.blinded_hops.len() == 0 { + return Err(LightningError{err: "0-hop blinded path provided".to_owned(), action: ErrorAction::IgnoreError}); + } + } + } } if payment_params.max_total_cltv_expiry_delta <= final_cltv_expiry_delta { 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 { } } + // Marshall route hints + let mut route_hints = Vec::with_capacity(payment_params.route_hints.blinded_len()); + let route_hints_ref = match &payment_params.route_hints { + Hints::Clear(hints) => hints, + Hints::Blinded(blinded_hints) => { + for (blinded_payinfo, blinded_path) in blinded_hints { + route_hints.push(RouteHint(vec![RouteHintHop { + src_node_id: blinded_path.introduction_node_id, + short_channel_id: BLINDED_PATH_SCID, + fees: RoutingFees { + base_msat: blinded_payinfo.fee_base_msat, + proportional_millionths: blinded_payinfo.fee_proportional_millionths, + }, + cltv_expiry_delta: blinded_payinfo.cltv_expiry_delta, + htlc_minimum_msat: Some(blinded_payinfo.htlc_minimum_msat), + htlc_maximum_msat: Some(blinded_payinfo.htlc_maximum_msat), + }])); + } + &route_hints + } + }; + // The main heap containing all candidate next-hops sorted by their score (max(fee, // htlc_minimum)). Ideally this would be a heap which allowed cheap score reduction instead of // adding duplicate entries when we find a better path to a given node. @@ -1612,11 +1666,7 @@ where L::Target: Logger { // If a caller provided us with last hops, add them to routing targets. Since this happens // earlier than general path finding, they will be somewhat prioritized, although currently // it matters only if the fees are exactly the same. - let route_hints = match &payment_params.route_hints { - Hints::Clear(hints) => hints, - _ => todo!() - }; - for route in route_hints.iter().filter(|route| !route.0.is_empty()) { + for route in route_hints_ref.iter().filter(|route| !route.0.is_empty()) { let first_hop_in_route = &(route.0)[0]; let have_hop_src_in_graph = // Only add the hops in this route to our candidate set if either @@ -2035,7 +2085,16 @@ where L::Target: Logger { for results_vec in selected_paths { let mut hops = Vec::new(); for res in results_vec { hops.push(res?); } - paths.push(Path { hops, blinded_tail: None }); + let mut blinded_tail = None; + if let Hints::Blinded(hints) = &payment_params.route_hints { + blinded_tail = hints.iter() + .find(|(_, p)| { + let intro_node_idx = if p.blinded_hops.len() == 1 { hops.len() - 1 } else { hops.len() - 2 }; + p.introduction_node_id == hops[intro_node_idx].pubkey + }) + .map(|(_, p)| p.clone()); + } + paths.push(Path { hops, blinded_tail }); } let route = Route { paths, @@ -2216,12 +2275,14 @@ mod tests { use crate::routing::utxo::UtxoResult; use crate::routing::router::{get_route, build_route_from_hops_internal, add_random_cltv_offset, default_node_features, Path, PaymentParameters, Route, RouteHint, RouteHintHop, RouteHop, RoutingFees, - DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE}; + BLINDED_PATH_SCID, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE}; use crate::routing::scoring::{ChannelUsage, FixedPenaltyScorer, Score, ProbabilisticScorer, ProbabilisticScoringParameters}; use crate::routing::test_utils::{add_channel, add_or_update_node, build_graph, build_line_graph, id_to_feature_flags, get_nodes, update_channel}; + use crate::blinded_path::{BlindedHop, BlindedPath}; use crate::chain::transaction::OutPoint; use crate::chain::keysinterface::EntropySource; - use crate::ln::features::{ChannelFeatures, InitFeatures, NodeFeatures}; + use crate::offers::invoice::BlindedPayInfo; + use crate::ln::features::{BlindedHopFeatures, ChannelFeatures, InitFeatures, NodeFeatures}; use crate::ln::msgs::{ErrorAction, LightningError, UnsignedChannelUpdate, MAX_VALUE_MSAT}; use crate::ln::channelmanager; use crate::util::config::UserConfig; @@ -5712,6 +5773,113 @@ mod tests { let route = get_route(&our_id, &payment_params, &network_graph.read_only(), None, 100, 42, Arc::clone(&logger), &scorer, &random_seed_bytes); assert!(route.is_ok()); } + + #[test] + fn simple_blinded_path_routing() { + // Check that we can generate a route to a blinded path with the expected hops. + let (secp_ctx, network, _, _, logger) = build_graph(); + let (_, our_id, _, nodes) = get_nodes(&secp_ctx); + let network_graph = network.read_only(); + + let scorer = ln_test_utils::TestScorer::new(); + let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + + let blinded_path = BlindedPath { + introduction_node_id: nodes[2], + blinding_point: ln_test_utils::pubkey(42), + blinded_hops: vec![ + BlindedHop { blinded_node_id: ln_test_utils::pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: ln_test_utils::pubkey(44), encrypted_payload: vec![0; 44] }, + ], + }; + let blinded_payinfo = BlindedPayInfo { + fee_base_msat: 100, + fee_proportional_millionths: 500, + htlc_minimum_msat: 1000, + htlc_maximum_msat: 100_000_000, + cltv_expiry_delta: 15, + features: BlindedHopFeatures::empty(), + }; + + let payee_pubkey = ln_test_utils::pubkey(45); + let payment_params = PaymentParameters::from_node_id(payee_pubkey, 0) + .with_blinded_route_hints(vec![(blinded_payinfo, blinded_path.clone())]); + let route = get_route(&our_id, &payment_params, &network_graph, None, 1001, 0, + Arc::clone(&logger), &scorer, &random_seed_bytes).unwrap(); + assert_eq!(route.paths.len(), 1); + assert_eq!(route.paths[0].hops.len(), 3); + assert_eq!(route.paths[0].len(), 5); + assert_eq!(route.paths[0].hops[2].pubkey, payee_pubkey); + assert_eq!(route.paths[0].hops[2].short_channel_id, BLINDED_PATH_SCID); + assert_eq!(route.paths[0].hops[1].pubkey, nodes[2]); + assert_eq!(route.paths[0].blinded_tail, Some(blinded_path)); + } + + #[test] + fn blinded_path_routing_errors() { + // Check that we can generate a route to a blinded path with the expected hops. + let (secp_ctx, network, _, _, logger) = build_graph(); + let (_, our_id, _, nodes) = get_nodes(&secp_ctx); + let network_graph = network.read_only(); + + let scorer = ln_test_utils::TestScorer::new(); + let keys_manager = ln_test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); + let random_seed_bytes = keys_manager.get_secure_random_bytes(); + + let mut invalid_blinded_path = BlindedPath { + introduction_node_id: nodes[2], + blinding_point: ln_test_utils::pubkey(42), + blinded_hops: vec![ + BlindedHop { blinded_node_id: ln_test_utils::pubkey(43), encrypted_payload: vec![0; 43] }, + ], + }; + let blinded_payinfo = BlindedPayInfo { + fee_base_msat: 100, + fee_proportional_millionths: 500, + htlc_minimum_msat: 1000, + htlc_maximum_msat: 100_000_000, + cltv_expiry_delta: 15, + features: BlindedHopFeatures::empty(), + }; + + let payee_pubkey = ln_test_utils::pubkey(45); + let payment_params = PaymentParameters::from_node_id(payee_pubkey, 0) + .with_blinded_route_hints(vec![(blinded_payinfo.clone(), invalid_blinded_path.clone())]); + match get_route(&our_id, &payment_params, &network_graph, None, 1001, 0, + Arc::clone(&logger), &scorer, &random_seed_bytes) + { + Err(LightningError { err, .. }) => { + assert_eq!(err, format!("1-hop blinded path introduction node id {} did not match payee {}", nodes[2], payee_pubkey)); + }, + _ => panic!("Expected error") + } + + invalid_blinded_path.introduction_node_id = payee_pubkey; + invalid_blinded_path.blinded_hops.push(BlindedHop { blinded_node_id: ln_test_utils::pubkey(43), encrypted_payload: vec![0; 44] }); + let payment_params = PaymentParameters::from_node_id(payee_pubkey, 0) + .with_blinded_route_hints(vec![(blinded_payinfo.clone(), invalid_blinded_path.clone())]); + match get_route(&our_id, &payment_params, &network_graph, None, 1001, 0, + Arc::clone(&logger), &scorer, &random_seed_bytes) + { + Err(LightningError { err, .. }) => { + assert_eq!(err, "Blinded path cannot have the payee as the source"); + }, + _ => panic!("Expected error") + } + + invalid_blinded_path.blinded_hops.clear(); + let payment_params = PaymentParameters::from_node_id(payee_pubkey, 0) + .with_blinded_route_hints(vec![(blinded_payinfo, invalid_blinded_path)]); + match get_route(&our_id, &payment_params, &network_graph, None, 1001, 0, + Arc::clone(&logger), &scorer, &random_seed_bytes) + { + Err(LightningError { err, .. }) => { + assert_eq!(err, "0-hop blinded path provided"); + }, + _ => panic!("Expected error") + } + } } #[cfg(all(test, not(feature = "no-std")))] diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index c71230795ac..98fa06b35fe 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -60,6 +60,15 @@ use crate::chain::keysinterface::{InMemorySigner, Recipient, EntropySource, Node use std::time::{SystemTime, UNIX_EPOCH}; use bitcoin::Sequence; +pub fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) +} + +pub fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() +} + pub struct TestVecWriter(pub Vec); impl Writer for TestVecWriter { fn write_all(&mut self, buf: &[u8]) -> Result<(), io::Error> {