-
Notifications
You must be signed in to change notification settings - Fork 403
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
Limit route hints to three channels #2044
Conversation
This PR does not limit the number of hints for phantom invoices to one. |
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 :). |
There was a problem hiding this 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.
Working on adding this + updating phantom tests! |
8e3c58b
to
3647c26
Compare
Codecov ReportPatch coverage:
📣 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
... 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. |
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. |
3647c26
to
c3f2029
Compare
lightning-invoice/src/utils.rs
Outdated
// 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(); |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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.
c3f2029
to
90f5e95
Compare
Updated PR:
Two refactor commits can probably be squashed into one. |
lightning-invoice/src/utils.rs
Outdated
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() |
There was a problem hiding this comment.
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.
lightning-invoice/src/utils.rs
Outdated
// 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). |
There was a problem hiding this comment.
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.
lightning-invoice/src/utils.rs
Outdated
// 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(); |
There was a problem hiding this comment.
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()
.
8364dcd
to
c4c78b2
Compare
lightning-invoice/src/utils.rs
Outdated
@@ -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()); |
There was a problem hiding this comment.
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.
invoice = invoice.private_route(route_hint.clone()); | |
invoice = invoice.private_route(route_hint); |
37f8fb6
to
d70cfd4
Compare
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. |
lightning-invoice/src/utils.rs
Outdated
@@ -629,6 +677,7 @@ fn filter_channels<L: Deref>( | |||
|
|||
include_channel | |||
}) | |||
.take(3) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
lightning-invoice/src/utils.rs
Outdated
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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: that -> than
lightning-invoice/src/utils.rs
Outdated
/// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: chose -> chosen
Oops, there's a dead doc link:
|
fd6627b
to
14c8a60
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly just nits.
lightning-invoice/src/utils.rs
Outdated
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/announce/include
lightning-invoice/src/utils.rs
Outdated
@@ -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 |
There was a problem hiding this comment.
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.
lightning-invoice/src/utils.rs
Outdated
/// 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"). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Re-wrap
lightning-invoice/src/utils.rs
Outdated
} | ||
|
||
let scaled_min_inbound = min_inbound_capacity_msat.unwrap() * 110; | ||
let current_sufficient = current_channel * 100 >= scaled_min_inbound; |
There was a problem hiding this comment.
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
lightning-invoice/src/utils.rs
Outdated
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); | ||
} |
There was a problem hiding this comment.
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.
lightning-invoice/src/utils.rs
Outdated
where | ||
L::Target: Logger, | ||
{ | ||
let mut phantom_hints : Vec<RouteHint> = Vec::new(); |
There was a problem hiding this comment.
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.
14c8a60
to
53b5f0b
Compare
|
||
for hints in phantom_hints.iter() { | ||
if hint_idx < hints.len() { | ||
invoice_hints.push(hints[hint_idx].clone()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
c1c37bc
to
1af848f
Compare
Looks good for squash. |
LGTM, feel free to squash the fixup, sorry for the delay. |
1af848f
to
905cd8b
Compare
Squashed. Thanks for all the patient review! |
There was a problem hiding this 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
This PR makes the following changes to hint selection:
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.