Skip to content

Commit

Permalink
Implement routing to blinded paths
Browse files Browse the repository at this point in the history
But disallow sending payments to them, for now
  • Loading branch information
valentinewallace committed Mar 22, 2023
1 parent c5a02c0 commit c0c4a84
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 9 deletions.
4 changes: 4 additions & 0 deletions lightning/src/ln/outbound_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()}));
Expand Down
121 changes: 112 additions & 9 deletions lightning/src/routing/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -628,6 +640,15 @@ pub enum Hints {
Clear(Vec<RouteHint>),
}

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<RouteHintHop>);
Expand Down Expand Up @@ -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});
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -5712,6 +5773,48 @@ 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));
}
}

#[cfg(all(test, not(feature = "no-std")))]
Expand Down
9 changes: 9 additions & 0 deletions lightning/src/util/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>);
impl Writer for TestVecWriter {
fn write_all(&mut self, buf: &[u8]) -> Result<(), io::Error> {
Expand Down

0 comments on commit c0c4a84

Please sign in to comment.