Skip to content

Commit 6d446a8

Browse files
Implement pathfinding for onion messages
We *could* reuse router::get_route, but it's better to start from scratch because get_route includes a lot of payment-specific computations that impact performance.
1 parent 8c68980 commit 6d446a8

File tree

2 files changed

+250
-0
lines changed

2 files changed

+250
-0
lines changed

lightning/src/onion_message/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323
mod blinded_route;
2424
mod messenger;
2525
mod packet;
26+
mod router;
2627
mod utils;
2728
#[cfg(test)]
2829
mod functional_tests;
2930

3031
// Re-export structs so they can be imported with just the `onion_message::` module prefix.
3132
pub use self::blinded_route::{BlindedRoute, BlindedHop};
3233
pub use self::messenger::{Destination, OnionMessenger, SendError, SimpleArcOnionMessenger, SimpleRefOnionMessenger};
34+
pub use self::router::find_path;
3335
pub(crate) use self::packet::Packet;

lightning/src/onion_message/router.rs

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
use bitcoin::secp256k1::PublicKey;
2+
3+
use ln::msgs::{ErrorAction, LightningError};
4+
use routing::gossip::{NetworkGraph, NodeId};
5+
use util::logger::{Level, Logger};
6+
7+
use alloc::collections::BinaryHeap;
8+
use core::hash::Hash;
9+
use core::ops::Deref;
10+
use prelude::*;
11+
12+
/// Find a path for sending an onion message.
13+
pub fn find_path<L: Deref, GL: Deref>(
14+
our_node_pubkey: &PublicKey, receiver_pubkey: &PublicKey, network_graph: &NetworkGraph<GL>, first_hops: Option<&[&PublicKey]>, logger: L
15+
) -> Result<Vec<PublicKey>, LightningError> where L::Target: Logger, GL::Target: Logger
16+
{
17+
let graph_lock = network_graph.read_only();
18+
let network_channels = graph_lock.channels();
19+
let network_nodes = graph_lock.nodes();
20+
let our_node_id = NodeId::from_pubkey(our_node_pubkey);
21+
22+
let mut for_each_successor = |node_id, callback: &mut FnMut(&NodeId, u64)| {
23+
// TODO: in this method, check if OM forwarding feature bit is supported
24+
if node_id == our_node_id && first_hops.is_some() {
25+
if let Some(first_hops) = first_hops {
26+
for hop in first_hops {
27+
callback(&NodeId::from_pubkey(hop), 1);
28+
}
29+
}
30+
} else if let Some(node_info) = network_nodes.get(&node_id) {
31+
for scid in &node_info.channels {
32+
if let Some(chan_info) = network_channels.get(&scid) {
33+
let successor_node_id = if chan_info.node_one == node_id {
34+
&chan_info.node_two
35+
} else {
36+
debug_assert!(chan_info.node_two == node_id);
37+
&chan_info.node_one
38+
};
39+
callback(successor_node_id, 1); // Use a fixed cost for each hop until scoring is added
40+
}
41+
}
42+
}
43+
};
44+
45+
let mut invalid_final_hop_pk = None;
46+
let mut convert_final_hop = |node_id: &NodeId| {
47+
match PublicKey::from_slice(node_id.as_slice()) {
48+
Ok(pk) => Ok(pk),
49+
Err(e) => {
50+
invalid_final_hop_pk = Some(*node_id);
51+
Err(())
52+
},
53+
}
54+
};
55+
56+
let receiver_node_id = NodeId::from_pubkey(receiver_pubkey);
57+
match dijkstra(our_node_id, &mut for_each_successor, |node_id| node_id == &receiver_node_id, &mut convert_final_hop) {
58+
Ok(p) => Ok(p),
59+
Err(Error::PathNotFound) => Err(LightningError {
60+
err: "Failed to find a path to the given destination".to_owned(),
61+
action: ErrorAction::IgnoreError,
62+
}),
63+
Err(Error::FinalHopConversion) => {
64+
debug_assert!(invalid_final_hop_pk.is_some());
65+
Err(LightningError {
66+
err: format!("Public key {:?} is invalid", invalid_final_hop_pk),
67+
action: ErrorAction::IgnoreAndLog(Level::Trace)
68+
})
69+
}
70+
}
71+
}
72+
73+
#[derive(Debug, PartialEq)]
74+
/// Errored running `dijkstra`.
75+
enum Error {
76+
/// No path exists to the destination.
77+
PathNotFound,
78+
/// Converting the processing hop type to the final hop type failed, see `dijkstra`'s
79+
/// `convert_final_hop` parameter.
80+
FinalHopConversion,
81+
}
82+
83+
// Heavily adapted from https://github.com/samueltardieu/pathfinding/blob/master/src/directed/dijkstra.rs
84+
// TODO: how2credit the repo (is that necessary?)?
85+
/// Run Dijkstra's from `start` until `found_target` indicates that we've found the destination.
86+
/// `successor_callback` must invoke the callback that it is provided on each of a given node's
87+
/// next-hop peers. `convert_final_hop` may be used to convert an intermediate processing hop type
88+
/// (`N`) to a final path hop type (`H`).
89+
fn dijkstra<N, H, FN, FS, FC>(start: N, successor_callback: &mut FN, found_target: FS,
90+
convert_final_hop: &mut FC) -> Result<Vec<H>, Error>
91+
where N: Eq + Hash + Copy + Ord,
92+
FN: FnMut(N, &mut FnMut(&N, u64)),
93+
FS: Fn(&N) -> bool,
94+
FC: FnMut(&N) -> Result<H, ()>,
95+
{
96+
let mut to_see = BinaryHeap::new();
97+
to_see.push((start, 0));
98+
let mut parents: HashMap<N, (N, u64)> = HashMap::new();
99+
parents.insert(start, (start, 0));
100+
101+
let mut target_reached = None;
102+
while let Some((node, cost)) = to_see.pop() {
103+
let &(_, c) = parents.get(&node).unwrap();
104+
if found_target(&node) {
105+
target_reached = Some(node);
106+
break;
107+
}
108+
// We may have inserted a node several times into the binary heap if we found a better way to
109+
// access it. Ensure that we are currently dealing with the best path and discard the others.
110+
if cost > c {
111+
continue;
112+
}
113+
successor_callback(node, &mut |successor, move_cost| {
114+
let new_cost = cost + move_cost;
115+
match parents.entry(*successor) {
116+
hash_map::Entry::Vacant(e) => {
117+
e.insert((node, new_cost));
118+
to_see.push((*successor, new_cost));
119+
}
120+
hash_map::Entry::Occupied(mut e) => {
121+
if e.get().1 > new_cost {
122+
e.insert((node, new_cost));
123+
to_see.push((e.get().0, new_cost));
124+
}
125+
}
126+
}
127+
});
128+
}
129+
130+
match target_reached {
131+
Some(t) => reverse_path(parents, t, convert_final_hop).map_err(|()| Error::FinalHopConversion),
132+
None => Err(Error::PathNotFound)
133+
}
134+
}
135+
136+
// Errors if `convert_path_hop` fails.
137+
fn reverse_path<N, H, FC>(parents: HashMap<N, (N, u64)>, start: N, convert_path_hop: &mut FC) -> Result<Vec<H>, ()>
138+
where N: Eq + Hash + Copy + Ord,
139+
FC: FnMut(&N) -> Result<H, ()>,
140+
{
141+
let mut path = vec![convert_path_hop(&start)?];
142+
let mut curr = start;
143+
loop {
144+
if let Some((parent_node_id, _)) = parents.get(&curr) {
145+
if parent_node_id != &curr {
146+
path.push(convert_path_hop(parent_node_id)?);
147+
curr = *parent_node_id;
148+
} else { break; }
149+
} else { break; }
150+
}
151+
path.reverse();
152+
path.remove(0);
153+
Ok(path)
154+
}
155+
156+
#[cfg(test)]
157+
mod tests {
158+
use routing::test_utils;
159+
use super::dijkstra;
160+
use super::Error;
161+
162+
use sync::Arc;
163+
164+
fn expected(target: u8) -> Result<Vec<u8>, Error> {
165+
match target {
166+
0 => Ok(vec![0]),
167+
1 => Ok(vec![]),
168+
2 => Ok(vec![6, 2]),
169+
3 => Ok(vec![0, 3]),
170+
4 => Ok(vec![6, 4]),
171+
5 => Ok(vec![6, 5]),
172+
6 => Ok(vec![6]),
173+
7 => Ok(vec![0, 3, 7]),
174+
8 => Err(Error::PathNotFound),
175+
_ => panic!("no such node"),
176+
}
177+
}
178+
179+
#[test]
180+
fn dijkstra_ok() {
181+
let successors_lookup : Vec<Vec<(u8, usize)>> = vec![
182+
vec![(1, 7), (2, 7), (3, 6)],
183+
vec![(0, 8), (6, 7)],
184+
vec![(5, 7)],
185+
vec![(7, 7)],
186+
vec![(4, 2)],
187+
vec![(1, 1)],
188+
vec![(2, 5), (4, 5), (5, 2)],
189+
vec![(5, 8)],
190+
vec![],
191+
];
192+
let mut successors = |node, callback: &mut FnMut(&u8, u64)| {
193+
for successor in &successors_lookup[node as usize] {
194+
callback(&successor.0, 1);
195+
}
196+
};
197+
for target in 0..9 {
198+
assert_eq!(
199+
dijkstra(1, &mut successors, |&node| node == target, &mut |&node| Ok(node)),
200+
expected(target)
201+
);
202+
}
203+
}
204+
205+
#[test]
206+
fn one_hop() {
207+
let (secp_ctx, network_graph, _, _, logger) = test_utils::build_graph();
208+
let (_, our_id, _, node_pks) = test_utils::get_nodes(&secp_ctx);
209+
210+
let path = super::find_path(&our_id, &node_pks[0], &network_graph, None, Arc::clone(&logger)).unwrap();
211+
assert_eq!(path.len(), 1);
212+
assert!(path[0] == node_pks[0]);
213+
}
214+
215+
#[test]
216+
fn two_hops() {
217+
let (secp_ctx, network_graph, _, _, logger) = test_utils::build_graph();
218+
let (_, our_id, _, node_pks) = test_utils::get_nodes(&secp_ctx);
219+
220+
let path = super::find_path(&our_id, &node_pks[2], &network_graph, None, Arc::clone(&logger)).unwrap();
221+
assert_eq!(path.len(), 2);
222+
// See test_utils::build_graph ASCII graph, the first hop can be any of these
223+
assert!(path[0] == node_pks[1] || path[0] == node_pks[7] || path[0] == node_pks[0]);
224+
assert_eq!(path[1], node_pks[2]);
225+
}
226+
227+
#[test]
228+
fn three_hops() {
229+
let (secp_ctx, network_graph, _, _, logger) = test_utils::build_graph();
230+
let (_, our_id, _, node_pks) = test_utils::get_nodes(&secp_ctx);
231+
232+
let mut path = super::find_path(&our_id, &node_pks[5], &network_graph, None, Arc::clone(&logger)).unwrap();
233+
assert_eq!(path.len(), 3);
234+
assert!(path[0] == node_pks[1] || path[0] == node_pks[7] || path[0] == node_pks[0]);
235+
path.remove(0);
236+
assert_eq!(path, vec![node_pks[2], node_pks[5]]);
237+
}
238+
239+
#[test]
240+
fn long_path() {
241+
let (secp_ctx, network, _, _, logger) = test_utils::build_line_graph();
242+
let (_, our_id, _, node_pks) = test_utils::get_nodes(&secp_ctx);
243+
let network_graph = network.read_only();
244+
245+
let path = super::find_path(&our_id, &node_pks[18], &network, None, Arc::clone(&logger)).unwrap();
246+
assert_eq!(path.len(), 19);
247+
}
248+
}

0 commit comments

Comments
 (0)