-
Notifications
You must be signed in to change notification settings - Fork 428
Introduce Dummy Hop support for Blinded Payment Path #4152
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
base: main
Are you sure you want to change the base?
Conversation
|
👋 Thanks for assigning @jkczyz as a reviewer! |
|
The implementation is still in progress. The parsing logic that mitigates timing-based attacks is being refined. Once that’s settled, this PR will be ready for review. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4152 +/- ##
==========================================
- Coverage 89.33% 86.52% -2.82%
==========================================
Files 180 158 -22
Lines 139042 102009 -37033
Branches 139042 102009 -37033
==========================================
- Hits 124219 88260 -35959
+ Misses 12196 11327 -869
+ Partials 2627 2422 -205
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
TheBlueMatt
left a comment
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.
Yay!
| let tlvs = intermediate_nodes | ||
| .iter() | ||
| .map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs)) | ||
| .chain((0..dummy_count).map(|_| BlindedPaymentTlvsRef::Dummy(&PaymentDummyTlv))) |
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.
Can we add a debug-assert that the forward hops and the dummy hops (ie all hops except the last) end up with the same length after padding?
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 call, I’ll add that in the next update. Thanks for pointing it out!
|
Updated: .01 → .02 SummaryThis iteration introduces dummy hops before the actual receiver in a blinded path. The goal is to defend against potential timing side-channels at the receiver.
Conceptually this works, but it leads to two concrete issues, surfaced by failing tests. Test Failures1.
|
|
Updated: .02 → .03 ChangesAccount for an existing fee-handling trade-off in LDK that leads to a small, intentional overpayment when the payer is the introduction node of a blinded path. This restores the DetailsLDK already has a known quirk in blinded-path handling:
In the classic two-hop case where (payer as introduction node → payee), this overpayment used to be manually avoided in tests (see router.rs L1754–1756), because the path length was unambiguous and the introduction node could safely treat the effective fee as zero. With the introduction of dummy hops, that assumption no longer holds:
Rather than change LDK’s fee semantics in this PR, I’ve chosen to make this overpayment explicit in the test framework and document the reasoning clearly. Once the underlying fee trade-off is revisited in LDK, this temporary adjustment can be removed with minimal changes. |
|
Rebased: .03 → .04 |
This shouldn't be an issue? We should be failing all blinded path receives with Re: the fee issues, yea, its something we'll have to handle, but in practice what should happen is that we pick a route with a non-zero fee, then we go to pay it and when we start unwrapping the onion we notice that the fee is for us, allowing us to reclaim the funds. Interestingly, this is a bit of a privacy leak - if the sender and the intro node are the same they can intuit that the remaining hops are blinded. Dunno if its worth doing anything about but we could add fees for the blinded hops. |
|
Updated: .04 → .05 Changes:
Thanks a lot Matt, that comment really helped clear things up. I had been assuming the existing test behaviour should stay the same even after introducing dummy hops, but your note made me rethink that. In Once dummy hops are in the mix though, the failure no longer originates at the intro node. It surfaces from a deeper Updated the test accordingly. Thanks again! |
|
Updated: .05 → .06 Changes:Cleaned up the commits for clarity and flow. NoteWith this update, the first full version of Payment Dummy Hops for Blinded Payment Path receive is finally in place. The dummy-hop support for TrampolineBlindedReceive will follow in a separate PR. |
|
Updated: .06 → .07 Addressed @TheBlueMatt’s comment — updates:
|
|
🔔 1st Reminder Hey @TheBlueMatt @tankyleo! This PR has been waiting for your review. |
1 similar comment
|
🔔 1st Reminder Hey @TheBlueMatt @tankyleo! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @TheBlueMatt @tankyleo! This PR has been waiting for your review. |
1 similar comment
|
🔔 2nd Reminder Hey @TheBlueMatt @tankyleo! This PR has been waiting for your review. |
|
🔔 3rd Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
dcaa1e3 to
f9c165c
Compare
|
🔔 1st Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
1 similar comment
|
🔔 1st Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
|
🔔 2nd Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
1 similar comment
|
🔔 2nd Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
|
🔔 3rd Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
1 similar comment
|
🔔 3rd Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
|
🔔 4th Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
1 similar comment
|
🔔 4th Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
|
🔔 5th Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
1 similar comment
|
🔔 5th Reminder Hey @TheBlueMatt @jkczyz! This PR has been waiting for your review. |
|
🔔 6th Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
|
🔔 7th Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
|
🔔 8th Reminder Hey @TheBlueMatt! This PR has been waiting for your review. |
| payment_context: context, | ||
| })) | ||
| }, | ||
| (None, None, None, None, None, None, None, Some(())) => Ok(BlindedPaymentTlvs::Dummy), |
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 actually feel like we should include a payment_relay in dummy payment hops. The biggest issue with padded payment hops is that the sender can (likely) identify the path actually taken based on feerate and CLTV-delta information. The use of payment_relay allows us to request that nodes require/take additional fee/CLTV delta on top of their published feerates/CLTV deltas as a way to add additional privacy. But, if we're gonna ask the payer for additional fees and CLTV, why are we "giving it" to other nodes? We should just require it on the dummy hops (where we get it, though I think this also addresses some off-by-ones in the fee enforcement logic which may be a privacy leak as well).
This requires enforcing the fees when decoding the onions as well, in addition to reporting it somehow in PaymentClaimable/PaymentClaimed (maybe just as a part of the total amount, though might be nice to break it out). I'm certainly fine with pushing that enforcement to another PR given this one is getting somewhat long in the tooth, but we should at least support deserializing the payment_relay field 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.
Thanks for the pointer. I agree that introducing payment_relay for dummy hops makes sense, to prevent any feerates based path analysis.
I’ve added support for dummy-hop's payment_relay in .15, including parsing and enforcement during onion decoding.
For now, I’ve intentionally deferred exposing this via the public API or wiring it into DefaultRouter, as doing so would require more substantial changes to the existing testing framework. I’m planning to take that up in a follow-up, along with reflecting the dummy-hop balance in PaymentClaimable / PaymentClaimed.
| }) | ||
| }, | ||
| onion_utils::Hop::Dummy { .. } => { | ||
| debug_assert!(false, "Dummy hop should have been peeled earlier"); |
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.
But this comment was in create_recv_pending_htlc_info (peel_payment_onion calls create_recv_pending_htlc_info if the onion_utils::Hop is Dummy)? Also the error message returned is wrong.
That said, maybe in peel_payment_onion we should handle the Dummy case by hand, peeling iteratively until we have something useful? That would avoid having to define a (serialized) PendingHTLCRouting::Dummy, which is pretty annoying. It would mean those using peel_payment_onion wouldn't be able to (easily) do iterative sleeps to simulate forwards in terms of timing which we do in ChannelManager, but that seems worth it to me?
| /// Same as [`BlindedPaymentPath::new`], but allows specifying a number of dummy hops. | ||
| /// | ||
| /// Note: | ||
| /// At most [`MAX_DUMMY_HOPS_COUNT`] dummy hops can be added to the blinded path. |
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.
Accordingly we'll need to allow specifying the fees/CLTV delta for each dummy hop here, though probably that should wait until a PR where we start enforcing 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.
Makes sense. I’ve restructured the blinded path constructor in .15 so that introducing dummy_tlvs in the public API should be a minimal change.
I’ll expand the public API once we’re able to properly exercise and test dummy-hop fees in the test framework.
Dummy BlindedPaymentTlvs is an empty TLV inserted immediately before the actual ReceiveTlvs in a blinded path. Receivers treat these dummy hops as real hops, which prevents timing-based attacks. Allowing arbitrary dummy hops before the final ReceiveTlvs obscures the recipient's true position in the route and makes it harder for an onlooker to infer the destination, strengthening recipient privacy.
Adds a new constructor for blinded paths that allows specifying the number of dummy hops. This enables users to insert arbitrary hops before the real destination, enhancing privacy by making it harder to infer the sender–receiver distance or identify the final destination. Lays the groundwork for future use of dummy hops in blinded path construction.
Upcoming commits will need the ability to specify whether a blinded path contains dummy hops. This change adds that support to the testing framework ahead of time, so later tests can express dummy-hop scenarios explicitly.
|
Updated: .14 → .15 Thanks, @TheBlueMatt. Updates:
DetailsThis update introduces the following changes related to
The following items are intentionally deferred to follow-up work:
Introducing finite |
TheBlueMatt
left a comment
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.
A few minor things and then I'm definitely for sure happy with this! Thanks!
| pub(super) fn compute_payinfo( | ||
| intermediate_nodes: &[PaymentForwardNode], payee_tlvs: &ReceiveTlvs, | ||
| payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, | ||
| intermediate_nodes: &[PaymentForwardNode], dummy_count: usize, dummy_tlvs: &DummyTlvs, |
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.
In a followup where we move to picking dummies in routing, we'll definitely want to replace the count + single-DummyTlvs with a slice of DummyTlvs so that we can have different ones.
| // before the real HTLC. Each call to `process_pending_htlc_forwards` | ||
| // strips exactly one dummy layer, so we call it N times. | ||
| for _ in 0..dummy_hop_override.unwrap_or(DEFAULT_PAYMENT_DUMMY_HOPS) { | ||
| node.node.process_pending_htlc_forwards(); |
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.
Please also
| node.node.process_pending_htlc_forwards(); | |
| assert!(node.node.needs_pending_htlc_processing()); | |
| node.node.process_pending_htlc_forwards(); |
| } | ||
|
|
||
| #[rustfmt::skip] | ||
| fn check_dummy_forward( |
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.
Please reuse check_blinded_forward (with BlindedHopFeatures::empty()) rather than copying it.
| /// packet which must be processed again. This process repeats until a non-dummy | ||
| /// routing decision is reached, which is guaranteed to be | ||
| /// [`PendingHTLCRouting::Receive`]. | ||
| Dummy { |
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.
We can drop this now. We only had it for the onion_payment utils but we removed that.
|
Also would be nice to test that an HTLC is rejected if it attempts to under-pay the blinded path requirements for dummy hops, but can come in a followup when we expose the fees on those hops. |
Builds on #4126
This PR adds Dummy Hop support for Blinded Payment Paths, paralleling the dummy-hop feature introduced for Blinded Message Paths in #3726.
By allowing arbitrary dummy hops before the real ReceiveTlvs, the length of a Blinded Payment Path can be increased to create a larger search space for an attacker trying to locate the true recipient. This reduces the risk of timing and position based deanonymization and improves user privacy.