Skip to content

Limit route hints to three channels #2044

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

freddiekrugerrand
Copy link
Contributor

@freddiekrugerrand freddiekrugerrand commented Feb 22, 2023

This PR makes the following changes to hint selection:

  • Only include the top 3 channels from this sorted list
  • Select up to 3 hints from distinct nodes for phantom invoices.
  • Sort eligible channels by available inbound capacity: Rationale: Inbound capacity seems like a rational value to sort by, and it makes selection deterministic for easier testing. See discussion.

Fixes #1782, replaces #1919.

@freddiekrugerrand
Copy link
Contributor Author

This PR does not limit the number of hints for phantom invoices to one.
New to rust/LDK, so I'm still figuring out how to do it without breaking tests.

@TheBlueMatt
Copy link
Collaborator

New to rust/LDK, so I'm still figuring out how to do it without #1919 (comment).

Those tests are super prescriptive, which is okay, but it probably means any change will result in the tests breaking, which means you'll just need to update them :).

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

LGTM, but, yea, would be cool to limit the phantom hints as well.

@freddiekrugerrand
Copy link
Contributor Author

Would be cool to limit the phantom hints as well.

Working on adding this + updating phantom tests!

@freddiekrugerrand freddiekrugerrand force-pushed the 1782-limitchannelhints branch 2 times, most recently from 8e3c58b to 3647c26 Compare February 24, 2023 20:05
@codecov-commenter
Copy link

codecov-commenter commented Feb 24, 2023

Codecov Report

Patch coverage: 98.66% and project coverage change: -0.03 ⚠️

Comparison is base (0e28bcb) 91.38% compared to head (1af848f) 91.36%.

❗ Current head 1af848f differs from pull request most recent head 905cd8b. Consider uploading reports for the commit 905cd8b to get more accurate results

📣 This organization is not using Codecov’s GitHub App Integration. We recommend you install it so Codecov can continue to function properly for your repositories. Learn more

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2044      +/-   ##
==========================================
- Coverage   91.38%   91.36%   -0.03%     
==========================================
  Files         102      102              
  Lines       49767    50878    +1111     
  Branches    49767    50878    +1111     
==========================================
+ Hits        45482    46487    +1005     
- Misses       4285     4391     +106     
Impacted Files Coverage Δ
lightning-invoice/src/utils.rs 97.41% <98.66%> (+0.35%) ⬆️

... and 45 files with indirect coverage changes

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

@dunxen dunxen self-requested a review February 26, 2023 20:04
@benthecarman
Copy link
Contributor

This PR makes the following changes to hint selection:

* Sort eligible channels by available inbound capacity

* Only include the top 3 channels from this sorted list

This is a pretty huge privacy leak, this will reveal your largest channels and can be a fingerprint for wallets. Would be better to add some randomness imo.

@jkczyz
Copy link
Contributor

jkczyz commented Feb 28, 2023

This is a pretty huge privacy leak, this will reveal your largest channels and can be a fingerprint for wallets. Would be better to add some randomness imo.

We can probably get away without sorting given the eligible channels should have enough capacity unless none do. Then it's just a matter of probability of success vs privacy tradeoff.

@freddiekrugerrand freddiekrugerrand changed the title Sort route hints by inbound and limit to three channels Limit route hints to three channels Mar 1, 2023
// hints across our nodes we add one hint from each node in turn until no node has any hints
// left (if one node has more hints than any other, these will accumulate at the end of the
// vector).
let mut invoice_hints :Vec<RouteHint> = Vec::new();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suspect there is a smarter way to do this with iterators, but couldn't find an elegant one without using itertools interleave (not currently a dependency?), and even that doesn't work well for > 2 sets. New to rust, so certainly open to better suggestions!

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if there is an easy way to do this without writing a custom Interleave iterator.

Regardless, we shouldn't clone anything we don't ultimately take. So at very least we should push references into a Vec<&RouteHint> and later do invoice_hints.into_iter().take(3).cloned().collect().

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that seems like a fair solution right now.

@freddiekrugerrand
Copy link
Contributor Author

Updated PR:

  • Fix tabs + typos
  • Remove sort by inbound (+ update tests to just do length assertions)
  • Limit phantom invoice hint selection to 3 hints (selecting one per phantom node until we reach 3)

Two refactor commits can probably be squashed into one.

Comment on lines 271 to 288
loop{
let mut remaining_hints = false;

for hints in phantom_hints.iter(){
if hint_idx < hints.len(){
invoice_hints.push(hints[hint_idx].clone());
remaining_hints = true;
}
}

if !remaining_hints{
break
}

hint_idx +=1;
}

invoice_hints.iter().take(3).map(|x| x.clone()).collect()
Copy link
Contributor

Choose a reason for hiding this comment

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

No need for the map and clone if you use into_iter instead of iter. The difference is that it consumes the Vec so you get values instead of references.

Comment on lines 264 to 269
// We have one vector per phantom node involved in creating the invoice. To distribute the
// hints across our nodes we add one hint from each node in turn until no node has any hints
// left (if one node has more hints than any other, these will accumulate at the end of the
// vector).
Copy link
Contributor

Choose a reason for hiding this comment

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

This is not quite accurate. There is only one phantom node but many real nodes, each with its own PhantomRouteHints. Essentially, the channels represent channels to real_node_pubkey, which has a fake channel with phantom_scid to the phantom node.

// hints across our nodes we add one hint from each node in turn until no node has any hints
// left (if one node has more hints than any other, these will accumulate at the end of the
// vector).
let mut invoice_hints :Vec<RouteHint> = Vec::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if there is an easy way to do this without writing a custom Interleave iterator.

Regardless, we shouldn't clone anything we don't ultimately take. So at very least we should push references into a Vec<&RouteHint> and later do invoice_hints.into_iter().take(3).cloned().collect().

@freddiekrugerrand freddiekrugerrand force-pushed the 1782-limitchannelhints branch 3 times, most recently from 8364dcd to c4c78b2 Compare March 2, 2023 19:41
@@ -200,6 +202,37 @@ where
invoice = invoice.amount_milli_satoshis(amt);
}

for route_hint in select_phantom_hints(amt_msat, phantom_route_hints, logger) {
invoice = invoice.private_route(route_hint.clone());
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we can just let route_hint move here and don't need to clone.

Suggested change
invoice = invoice.private_route(route_hint.clone());
invoice = invoice.private_route(route_hint);

@freddiekrugerrand freddiekrugerrand force-pushed the 1782-limitchannelhints branch 4 times, most recently from 37f8fb6 to d70cfd4 Compare March 2, 2023 19:57
@dunxen
Copy link
Contributor

dunxen commented Mar 2, 2023

This is looking good to me now besides a few minor formatting nits. I'll give it another pass once reviewers with more context have checked again.

@@ -629,6 +677,7 @@ fn filter_channels<L: Deref>(

include_channel
})
.take(3)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe this is picking three nodes at random (i.e. based on the order they appear in the hash map), I'm not sure we want to do that, ideally we should have some kind of sorting logic instead imo.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Originally sorted by inbound but removed that due to privacy concerns raised.

I think sorting by some factor that makes a channel more desirable for hints makes sense -maybe just channel capacity?

If we sort by something unrelated to the ability to route, then maybe some randomness is just as good?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think channel capacity is a decent privacy vs utility trade-off actually, otherwise yeah I'd say it would probably just be fine to have it random.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry missed that comment, left a top level comment with my thinking.

dunxen
dunxen previously approved these changes Apr 4, 2023
Copy link
Contributor

@dunxen dunxen left a comment

Choose a reason for hiding this comment

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

Nice. I've read through the comments on some functions and picked up some nits. Feel free to address them in the impl Iterator follow-up PR.

let new_higher_capacity = channel.is_public == entry.get().is_public &&
channel.inbound_capacity_msat > current_max_capacity;
if new_now_public || new_higher_capacity {
// `new_now_public), and this channel has more desirable inbound that the incumbent,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: that -> than

/// prefer_current_channel chooses a channel to use for route hints between a currently selected and candidate
/// channel based on the inbound capacity of each channel and the minimum inbound capacity requested for the hints,
/// returning true if the current channel should be preferred over the candidate channel.
/// * If no minimum amount is requested, the channel with the most inbound is chose to maximize the chances that a
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: chose -> chosen

@TheBlueMatt
Copy link
Collaborator

Oops, there's a dead doc link:

error: unresolved link to `PhantomKeysManager`
   --> lightning-invoice/src/utils.rs:222:11
    |
222 | /// See [`PhantomKeysManager`] for more information on phantom node payments.
    |           ^^^^^^^^^^^^^^^^^^ no item named `PhantomKeysManager` in scope
    |

@freddiekrugerrand freddiekrugerrand force-pushed the 1782-limitchannelhints branch 3 times, most recently from fd6627b to 14c8a60 Compare April 5, 2023 12:28
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Mostly just nits.

channel.inbound_capacity_msat > current_max_capacity;
if new_now_public || new_higher_capacity {
// `new_now_public), and this channel has more desirable inbound than the incumbent,
// prefer to announce this channel.
Copy link
Contributor

Choose a reason for hiding this comment

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

s/announce/include

@@ -570,12 +574,17 @@ fn filter_channels<L: Deref>(
// If this channel is public and the previous channel is not, ensure we replace the
// previous channel to avoid announcing non-public channels.
let new_now_public = channel.is_public && !entry.get().is_public;
// Decide whether we prefer the currently selected channel with the node to the new one,
// based on their inbound capacity. A scaling factor of 10% to pad our minimum inbound
Copy link
Contributor

Choose a reason for hiding this comment

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

s/A scaling factor/Add a scaling factor

Though arguably this detail doesn't need to be mentioned here.

Comment on lines 676 to 678
/// percentage)
/// then we choose the lowest inbound channel with above this amount. If we have sufficient inbound channels, we don't
/// want to deplete our larger channels with small payments (the off-chain version of "grinding our change").
Copy link
Contributor

Choose a reason for hiding this comment

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

Re-wrap

}

let scaled_min_inbound = min_inbound_capacity_msat.unwrap() * 110;
let current_sufficient = current_channel * 100 >= scaled_min_inbound;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove double spaces in three places

Comment on lines 744 to 746
for test_case in test_cases {
assert_eq!(crate::utils::prefer_current_channel(test_case.0, test_case.1, test_case.2), test_case.3);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer that we don't use a loop here. Otherwise, it isn't clear which case has failed.

where
L::Target: Logger,
{
let mut phantom_hints : Vec<RouteHint> = Vec::new();
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: no space before colon

This commit updates the way that we choose our preferred channel per
counterparty when selecting route hints. Previously, we would choose
the largest usable channel above our requested minimum.

This change updates selection to prefer the smallest channel above the
minimum amount (if provided, plus a scaling factor of 10% to allow
some margin for error). This is the off chain version of not "grinding
our change" - we want to supply route hints for channels that have
enough inbound to facilitate the receive, but not overload our high
inbound channels with smaller payments (since we may need this large
chunk of inbound for larger payment later on).

If there is no minimum amount provided, we err on the side of caution
and just select our highest inbound channels so that payments of any
size have a chance of succeeding. Likewise, if we have no channels above
the minimum, we select the largest channel to maximize our changes of
receiving the payment in multiple parts.
dunxen
dunxen previously approved these changes Apr 6, 2023

for hints in phantom_hints.iter() {
if hint_idx < hints.len() {
invoice_hints.push(hints[hint_idx].clone());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Won't this add all 4 hints if we have two nodes each with two channels? We'll walk the outer loop twice and the inner loop will push both channels both times.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah, would also include too many if we have > 3 phantom nodes (would include the first hint for each node then exit). Thanks for the catch!

Added a fixup moving the len check up to address + test coverage for the two scenarios where this previously wouldn't work.

@dunxen
Copy link
Contributor

dunxen commented Apr 19, 2023

Looks good for squash.

@TheBlueMatt
Copy link
Collaborator

LGTM, feel free to squash the fixup, sorry for the delay.

@freddiekrugerrand
Copy link
Contributor Author

Squashed. Thanks for all the patient review!

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Thanks! Opened an issue for moving to iterators #2240

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Limit number of route hints while creating an invoice
7 participants