-
Notifications
You must be signed in to change notification settings - Fork 369
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
Add random 'shadow route' CLTV delta offsets to improve privacy. #1286
Conversation
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 for the delay on this one! A few comments, don't worry about the failing tests for now, I can help out with those if needed.
87fc7d4
to
ea0ad9d
Compare
Addressed comments and rebased off a1fedea. |
251374f
to
f6793ab
Compare
lightning/src/routing/router.rs
Outdated
if let Some(random_index) = usize::from_be_bytes(random_path_bytes).checked_rem(cur_node.channels.len()) { | ||
if let Some(random_channel_id) = cur_node.channels.get(random_index) { | ||
if let Some(random_channel) = network_channels.get(random_channel_id) { | ||
if random_channel.node_one == cur_node_id { |
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.
One improvement, I think if the randomly selected next hop is actually part of the extended payment path you could pick up another hop to pursue the random walk. What we would like to avoid is the random walk rolling back on the actually drawn out path. Let's say you have A -> B -> C -> D and you sort the random walk D -> C -> B -> A thus a shadow offset partially overlapping with the real cltv deltas. I would say it's a low-probability phenomena but the last publicly known node might not have numerous public channels opened for routing.
That said, it might not be worthy the effort at that point.
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.
Thank you very much for having a look at this! Could you elaborate why we wouldn't want the shadow route to overlap the actual path? I see this only as a problem if we assume an on-path adversary that can say for sure that it wasn't part of the actual payment path and hence might be able to reduce the number of candidate routes. However, if we exclude overlapping paths from the beginning, we might reduce the randomness from the getgo, making any adversary's life easier?
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'm not sure if its worth the effort to implement this, but I see a few concerns with overlapping paths. eg if you're an attacker and the CLTV deltas only line up with one path, and it overlaps/backtraces, you know for sure that that is shadow route, because the normal routing algorithm won't exhibit that behavior.
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.
Hum, I think it may not be worth to rule out any overlap, since this could indeed reduce the variety of potential random walks quite a bit. But maybe it would be enough to ensure that we do not immediately loop back on the actual path. And in the case this excludes the only public edge we could fall back to a 'synthetic' random offset, just as in the case for fully private paths.
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.
Yea, I'll leave it up to you. I don't care too much because so many nodes use the same CLTV delta that its probably all the same in practice, but if its easy to avoid the same-node-backtrack then feel free to add it.
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 I'm leaning on the side of leaving this as-is for now and potentially having another look at it in a potential follow-up PR. We might want to have a follow-up anyways, for example in order to introduce some kind of value obfuscation, as noted in #158.
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 really up to you, fine to leave it as a follow-up.
Looks like this needs rebase, sorry about that. |
3794bf6
to
803348f
Compare
Rebased. |
803348f
to
4e9cb21
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.
I'm basically happy with this. There's some compilation errors due to new tests that weren't updated on rebase and the benchmark-only code needs updating, I believe. I'm happy to help resolve any test failures left after that.
ea06afb
to
28cebe1
Compare
To make the router tests pass, maybe it'd be best to split |
78e9dcd
to
a20cd5c
Compare
Good idea, especially given that For the time being I left |
Codecov Report
@@ Coverage Diff @@
## main #1286 +/- ##
==========================================
- Coverage 90.75% 90.60% -0.15%
==========================================
Files 72 72
Lines 42254 40324 -1930
==========================================
- Hits 38347 36536 -1811
+ Misses 3907 3788 -119
Continue to review full report at Codecov.
|
Feel free to squash the commits down to one or two, note that we'd ideally like to ensure all code compiles and all tests pass after each commit. |
c0657fd
to
67137c6
Compare
Sure sure, just wanted to avoid squashing too early. Rebased and squashed with 67137c6. |
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.
Some style nits and minor test comments.
lightning-invoice/src/utils.rs
Outdated
) -> Result<Route, LightningError> { | ||
let mut locked_random_seed_bytes = self.random_seed_bytes.lock().unwrap(); | ||
*locked_random_seed_bytes = sha256::Hash::hash(&*locked_random_seed_bytes).into_inner(); | ||
find_route(payer, params, &*self.network_graph, first_hops, &*self.logger, scorer, &locked_random_seed_bytes.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.
As is the lock is held through find_route
which we maybe don't want. Instead, we'll need an explicit {}
scope to tell the compiler where to drop the locked_random_seed_bytes
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.
Should we consider parameterizing DefaultRouter
by K: Deref where K::Target: KeysInterface
and call get_secure_random_bytes
each time? I suppose the interface is too broad, but I wonder if a narrower Randomness
interface is warranted. No strong opinion here, but it would avoid the mutex.
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 now went the scoping route with 386a0b5. However, I agree that introducing a Randomness
interface (not only here) would be a nicer abstraction than pushing around arrays with random_seed_bytes
. Possibly a good follow-up PR?
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 suppose we could, yea, agree we'd want to do it in a followup, though. I'm a little hesitant to replace a trivial parameter with a full interface, though, as it just seems like overkill, but we can discuss that later.
lightning-invoice/src/utils.rs
Outdated
) -> Result<Route, LightningError> { | ||
let mut locked_random_seed_bytes = self.random_seed_bytes.lock().unwrap(); | ||
*locked_random_seed_bytes = sha256::Hash::hash(&*locked_random_seed_bytes).into_inner(); | ||
find_route(payer, params, &*self.network_graph, first_hops, &*self.logger, scorer, &locked_random_seed_bytes.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.
Should we consider parameterizing DefaultRouter
by K: Deref where K::Target: KeysInterface
and call get_secure_random_bytes
each time? I suppose the interface is too broad, but I wonder if a narrower Randomness
interface is warranted. No strong opinion here, but it would avoid the mutex.
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.
Good with the new approach of only offsetting the final hop.
(Thought the old one had the benefit to also mask the sender topology as the intermediate node couldn't deduce from their CLTV deltas the distance from sender, I believe)
386a0b5
to
01bccc4
Compare
Thanks for all the great feedback, I'll take it into account going forward! |
22b4eeb
to
ee58764
Compare
lightning/src/routing/router.rs
Outdated
// Limit the offset so we never exceed the max_total_cltv_expiry_delta | ||
let max_path_offset = payment_params.max_total_cltv_expiry_delta | ||
.checked_sub(path.iter().map(|h| h.cltv_expiry_delta).sum()) | ||
.unwrap_or(shadow_ctlv_expiry_delta_offset); |
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.
Shouldn't this be unwrap_or(0)
? If our path expiry is more than the max already we probably shouldn't add an offset at all? Also, were we planning on limiting the total CLTV during the get_route
run to give us a little headroom here?
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.
Well my thinking here was that since we limit the route overall with max_total_cltv_expiry_delta
, this subtraction should never fail. If however something unexpected were to happen and it would fail, we might be better off to default to provide some privacy (capped at 3*144
), instead of none. But it can of course also default to 0
, I have no strong preference here.
As for the considering the offset in get_route
: after looking at the data I found that there were basically no cases in which there was not ample room to add the offset and hence thought it would be cleaner not to interfere with pathfinding and keep the two methods entirely separate. However, if we still want to take some of the offset into account, I can of course go back to, for example, subtracting 2*40
or 2*144
from the limit during path finding.
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.
Well my thinking here was that since we limit the route overall with max_total_cltv_expiry_delta, this subtraction should never fail.
Right, sorry, indeed.
If however something unexpected were to happen and it would fail, we might be better off to default to provide some privacy (capped at 3*144), instead of none.
🤷♂️ I guess we shouldn't ever violate the user-provided value ever?
As for the considering the offset in get_route: #1286 (comment) I found that there were basically no cases in which there was not ample room to add the offset and hence thought it would be cleaner not to interfere with pathfinding and keep the two methods entirely separate.
Hmm, I feel like that's a better reason to interfere than to not interfere.
I'm still a bit confused by this hunk here - why do we do the subtraction at max_path_offset.checked_sub(max_path_offset.wrapping_rem(40))
?
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 guess we shouldn't ever violate the user-provided value ever?
...
Hmm, I feel like that's a better reason to interfere than to not interfere.
Alright, both addressed by f7be9da.
I'm still a bit confused by this hunk here - why do we do the subtraction at
max_path_offset.checked_sub(max_path_offset.wrapping_rem(40))
?
The difference of the path total to max_total_cltv_expiry_delta
may be a pretty uncommon value, which could result in an implausible shadow route offset. The thinking here therefore was to choose the path offset to be the closest multiple of 40 to increase plausibility. I now had another look at this part and added a comment to make this more transparent in f7be9da.
f7be9da
to
773e76b
Compare
09c262f
to
a6f241c
Compare
Rebased and squashed once more. |
lightning/src/routing/router.rs
Outdated
logger: L, scorer: &S | ||
our_node_pubkey: &PublicKey, payment_params: &PaymentParameters, network_graph: &ReadOnlyNetworkGraph, | ||
first_hops: Option<&[&ChannelDetails]>, final_value_msat: u64, final_cltv_expiry_delta: u32, | ||
logger: L, scorer: &S, random_seed_bytes: &[u8; 32] |
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.
random_seed_bytes
is not needed here since it is only used in add_random_cltv_offset
.
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 correct. If however possible, I’d like to leave it in get_route
for now and use it to implement the 'real random shuffle' TODO in a follow-up PR. I now prefixed it with an underscore to disable the warning, but let me know if you'd rather keep it out of get_route
altogether in this PR.
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.
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.
a6f241c
to
f5b1b46
Compare
lightning/src/routing/router.rs
Outdated
max_path_offset = max_path_offset | ||
.wrapping_sub(max_path_offset.wrapping_rem(MEDIAN_HOP_CLTV_EXPIRY_DELTA)) | ||
.max(max_path_offset.wrapping_rem(MEDIAN_HOP_CLTV_EXPIRY_DELTA)); | ||
shadow_ctlv_expiry_delta_offset = cmp::min(shadow_ctlv_expiry_delta_offset, max_path_offset); |
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.
If the subtraction wraps, shadow_ctlv_expiry_delta_offset
would not likely change, IIUC. Could it then possibly exceed payment_params.max_total_cltv_expiry_delta
for small values?
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.
Hum, I'm not entirely sure if I'm understanding you correctly. The wrapping_sub
should actually never wrap due to x >= x mod y
. I made this a bit more explicit by using saturating_sub
in 0b777cb.
Moreover, the line prior ensures that max_path_offset
is never larger than max_total_cltv_expiry_delta
and all changes from there only ever would reduce it further.
0b777cb
to
5731df3
Compare
5731df3
to
e2efb0c
Compare
e2efb0c
to
e92b5a7
Compare
Rebased. |
This PR is an attempt to address #158.
In order to generate a plausible CLTV delta offset, a three-hop random walk starting from the payee is conducted and
cltv_expiry_delta
values are aggregated. This offset is then added to all but the final hop of the selected paths (since the last hop knows anyways that it is the recipient).Tests are currently failing since they of course do not expect the randomly added offset. I'll fix them when someone had a look at the implementation itself.