Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 159 additions & 44 deletions lightning/src/routing/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -374,11 +374,13 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
if final_value_msat == 0 {
return Err(LightningError{err: "Cannot send a payment of 0 msat".to_owned(), action: ErrorAction::IgnoreError});
}
let last_hops = last_hops.iter().map(|hops| hops.0.clone()).collect::<Vec<_>>();

let last_hops = last_hops.iter().filter_map(|hops| hops.0.last()).collect::<Vec<_>>();
for last_hop in last_hops.iter() {
if last_hop.src_node_id == *payee {
return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
for routes in last_hops.iter() {
for last_hop in routes.iter() {
if last_hop.src_node_id == *payee {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIU we already support multiple r-fields, what we don't support and this PR implements is multiple hops per-r-field ?

If so I think you could rename last_hops to r_fields, routes to the singular route as each r field constitutes a revealed forward route, and last_hop to hop only as it's not necessarily the latest link of the route anymore ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r_fields is pretty opaque, maybe paths_to_payee?

return Err(LightningError{err: "Last hop cannot have a payee as a source.".to_owned(), action: ErrorAction::IgnoreError});
}
}
}

Expand DownExpand Up@@ -406,7 +408,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
//
// We are not a faithful Dijkstra's implementation because we can change values which impact
// earlier nodes while processing later nodes. Specifically, if we reach a channel with a lower
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) then
// liquidity limit (via htlc_maximum_msat, on-chain capacity or assumed liquidity limits) than
// the value we are currently attempting to send over a path, we simply reduce the value being
// sent along the path for any hops after that channel. This may imply that later fees (which
// we've already tabulated) are lower because a smaller value is passing through the channels
Expand DownExpand Up@@ -784,7 +786,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// $fee_to_target_msat represents how much it costs to reach to this node from the payee,
// meaning how much will be paid in fees after this node (to the best of our knowledge).
// This data can later be helpful to optimize routing (pay lower fees).
macro_rules! add_entries_to_cheapest_to_target_node {
macro_rules! add_entries_to_cheapest_target_node {
( $node: expr, $node_id: expr, $fee_to_target_msat: expr, $next_hops_value_contribution: expr, $next_hops_path_htlc_minimum_msat: expr ) => {
let skip_node = if let Some(elem) = dist.get_mut($node_id) {
let was_processed = elem.was_processed;
Expand DownExpand Up@@ -868,47 +870,57 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
// If not, targets.pop() will not even let us enter the loop in step 2.
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, payee, 0, path_value_msat, 0);
add_entries_to_cheapest_target_node!(node, payee, 0, path_value_msat, 0);
},
}

// Step (1).
// 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.
for hop in last_hops.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {
// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
for route in last_hops.iter() {
//TODO:-
//The loop starts from the very last RouteHintHop and build a path from the payee along
//the RouteHintHop(s) for each route. This is done because for every step of the loop,
//we need the ```next_hops_path_htlc_minimum_msat``` and ```next_hops_fee_msat``` values
//to reflect the total minimum htlc msat and fees required to route to the payee from the current
//RouteHintHop node through the route. This also prevents adding any RouteHintHop which may be invalid.
//If a RouteHintHop is invalid, this means the preceeding RouteHintHops (in normal order) cannot be used
//however the next RouteHintHops and thereafter may be still viable.
for hop in route.iter() {
let have_hop_src_in_graph =
// Only add the last hop to our candidate set if either we have a direct channel or
// they are in the regular network graph.
first_hop_targets.get(&hop.src_node_id).is_some() ||
network.get_nodes().get(&hop.src_node_id).is_some();
if have_hop_src_in_graph {
// BOLT 11 doesn't allow inclusion of features for the last hop hints, which
// really sucks, cause we're gonna need that eventually.
let last_hop_htlc_minimum_msat: u64 = match hop.htlc_minimum_msat {
Some(htlc_minimum_msat) => htlc_minimum_msat,
None => 0
};
let directional_info = DummyDirectionalChannelInfo {
cltv_expiry_delta: hop.cltv_expiry_delta as u32,
htlc_minimum_msat: last_hop_htlc_minimum_msat,
htlc_maximum_msat: hop.htlc_maximum_msat,
fees: hop.fees,
};
// We assume that the recipient only included route hints for routes which had
// sufficient value to route `final_value_msat`. Note that in the case of "0-value"
// invoices where the invoice does not specify value this may not be the case, but
// better to include the hints than not.
if add_entry!(hop.short_channel_id, hop.src_node_id, payee, directional_info, Some((final_value_msat + 999) / 1000), &empty_channel_features, 0, path_value_msat, 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A multi-hop route hint isn't simply more route hints to get to the payee, but instead channels which eventually get to the payee, through multiple private channels.

Thus, this is no longer correct - anything but the last hop is no longer from hop.src_node_id to payee but instead from hop.src_node_id to the next hop in route (except the last entry, which is to payee).

// If this hop connects to a node with which we have a direct channel,
// ignore the network graph and, if the last hop was added, add our
// direct channel to the candidate set.
//
// Note that we *must* check if the last hop was added as `add_entry`
// always assumes that the third argument is a node to which we have a
// path.
if let Some(&(ref first_hop, ref features, ref outbound_capacity_msat, _)) = first_hop_targets.get(&hop.src_node_id) {
add_entry!(first_hop, *our_node_id , hop.src_node_id, dummy_directional_info, Some(outbound_capacity_msat / 1000), features, 0, path_value_msat, 0);
}
}
}
}
Expand DownExpand Up@@ -1036,7 +1048,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye

// If we found a path back to the payee, we shouldn't try to process it again. This is
// the equivalent of the `elem.was_processed` check in
// add_entries_to_cheapest_to_target_node!() (see comment there for more info).
// add_entries_to_cheapest_target_node!() (see comment there for more info).
if pubkey == *payee { continue 'path_construction; }

// Otherwise, since the current target node is not us,
Expand All@@ -1045,7 +1057,7 @@ pub fn get_route<L: Deref>(our_node_id: &PublicKey, network: &NetworkGraph, paye
match network.get_nodes().get(&pubkey) {
None => {},
Some(node) => {
add_entries_to_cheapest_to_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
add_entries_to_cheapest_target_node!(node, &pubkey, lowest_fee_to_node, value_contribution_msat, path_htlc_minimum_msat);
},
}
}
Expand DownExpand Up@@ -1343,7 +1355,7 @@ mod tests {
let logger = Arc::new(test_utils::TestLogger::new());
let chain_monitor = Arc::new(test_utils::TestChainSource::new(Network::Testnet));
let net_graph_msg_handler = NetGraphMsgHandler::new(genesis_block(Network::Testnet).header.block_hash(), None, Arc::clone(&logger));
// Build network from our_id to node7:
// Build network from our_id to node6:
//
// -1(1)2- node0 -1(3)2-
// / \
Expand DownExpand Up@@ -2125,12 +2137,45 @@ mod tests {
}])]
}

fn multiple_last_hops(nodes: &Vec<PublicKey>) -> Vec<RouteHint> {
let zero_fees = RoutingFees {
base_msat: 0,
proportional_millionths: 0,
};
vec![RouteHint(vec![RouteHintHop {
src_node_id: nodes[3].clone(),
short_channel_id: 8,
fees: zero_fees,
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}, RouteHintHop {
src_node_id: nodes[4].clone(),
short_channel_id: 9,
fees: RoutingFees {
base_msat: 1001,
proportional_millionths: 0,
},
cltv_expiry_delta: (9 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]), RouteHint(vec![RouteHintHop {
src_node_id: nodes[5].clone(),
short_channel_id: 10,
fees: zero_fees,
cltv_expiry_delta: (10 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}])]
}

#[test]
fn last_hops_test() {
fn last_hops_test_1() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 1 RouteHintHop per RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
Expand DownExpand Up@@ -2194,6 +2239,76 @@ mod tests {
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn last_hops_test_2() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
let (_, our_id, _, nodes) = get_nodes(&secp_ctx);

// Simple test across 2, 3, 5, and 4 via a last_hop channel
// Tests with just 2 RouteHintHops in the first RouteHint in the invoice

// First check that last hop can't have its source as the payee.
let invalid_last_hop = RouteHint(vec![RouteHintHop {
src_node_id: nodes[6],
short_channel_id: 8,
fees: RoutingFees {
base_msat: 1000,
proportional_millionths: 0,
},
cltv_expiry_delta: (8 << 8) | 1,
htlc_minimum_msat: None,
htlc_maximum_msat: None,
}]);

let mut invalid_last_hops = multiple_last_hops(&nodes);
invalid_last_hops.push(invalid_last_hop);
{
if let Err(LightningError{err, action: ErrorAction::IgnoreError}) = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &invalid_last_hops.iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)) {
assert_eq!(err, "Last hop cannot have a payee as a source.");
} else { panic!(); }
}

let route = get_route(&our_id, &net_graph_msg_handler.network_graph.read().unwrap(), &nodes[6], None, None, &last_hops(&nodes).iter().collect::<Vec<_>>(), 100, 42, Arc::clone(&logger)).unwrap();
assert_eq!(route.paths[0].len(), 5);

assert_eq!(route.paths[0][0].pubkey, nodes[1]);
assert_eq!(route.paths[0][0].short_channel_id, 2);
assert_eq!(route.paths[0][0].fee_msat, 100);
assert_eq!(route.paths[0][0].cltv_expiry_delta, (4 << 8) | 1);
assert_eq!(route.paths[0][0].node_features.le_flags(), &id_to_feature_flags(2));
assert_eq!(route.paths[0][0].channel_features.le_flags(), &id_to_feature_flags(2));

assert_eq!(route.paths[0][1].pubkey, nodes[2]);
assert_eq!(route.paths[0][1].short_channel_id, 4);
assert_eq!(route.paths[0][1].fee_msat, 0);
assert_eq!(route.paths[0][1].cltv_expiry_delta, (6 << 8) | 1);
assert_eq!(route.paths[0][1].node_features.le_flags(), &id_to_feature_flags(3));
assert_eq!(route.paths[0][1].channel_features.le_flags(), &id_to_feature_flags(4));

assert_eq!(route.paths[0][2].pubkey, nodes[4]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to properly test the route hints, you should disable some of the other paths to node 6, especially the one taken here over channel 6 and 11.

assert_eq!(route.paths[0][2].short_channel_id, 6);
assert_eq!(route.paths[0][2].fee_msat, 0);
assert_eq!(route.paths[0][2].cltv_expiry_delta, (11 << 8) | 1);
assert_eq!(route.paths[0][2].node_features.le_flags(), &id_to_feature_flags(5));
assert_eq!(route.paths[0][2].channel_features.le_flags(), &id_to_feature_flags(6));

assert_eq!(route.paths[0][3].pubkey, nodes[3]);
assert_eq!(route.paths[0][3].short_channel_id, 11);
assert_eq!(route.paths[0][3].fee_msat, 0);
assert_eq!(route.paths[0][3].cltv_expiry_delta, (8 << 8) | 1);
// If we have a peer in the node map, we'll use their features here since we don't have
// a way of figuring out their features from the invoice:
assert_eq!(route.paths[0][3].node_features.le_flags(), &id_to_feature_flags(4));
assert_eq!(route.paths[0][3].channel_features.le_flags(), &id_to_feature_flags(11));

assert_eq!(route.paths[0][4].pubkey, nodes[6]);
assert_eq!(route.paths[0][4].short_channel_id, 8);
assert_eq!(route.paths[0][4].fee_msat, 100);
assert_eq!(route.paths[0][4].cltv_expiry_delta, 42);
assert_eq!(route.paths[0][4].node_features.le_flags(), &Vec::<u8>::new()); // We dont pass flags in from invoices yet
assert_eq!(route.paths[0][4].channel_features.le_flags(), &Vec::<u8>::new()); // We can't learn any flags from invoices, sadly
}

#[test]
fn our_chans_last_hop_connect_test() {
let (secp_ctx, net_graph_msg_handler, _, logger) = build_graph();
Expand Down