Skip to content

Conversation

@shaavan
Copy link
Member

@shaavan shaavan commented Oct 9, 2025

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.

Extends the work started in
[PR#3917](lightningdevkit#3917)
by adding ReceiveAuthKey-based verification for Blinded Payment Paths.

This reduces space previously taken by individual ReceiveTlvs and
aligns the verification logic with that used for Blinded Message Paths.
Now that we have introduced an alternate mechanism for authentication
in the codebase, we can safely remove the now redundant (hmac, nonce)
fields from the Payment ReceiveTlvs's while maintaining the security
of the onion messages.
@ldk-reviews-bot
Copy link

👋 Hi! I see this is a draft PR.
I'll wait to assign reviewers until you mark it as ready for review.
Just convert it out of draft status when you're ready for review!

@shaavan
Copy link
Member Author

shaavan commented Oct 9, 2025

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
Copy link

codecov bot commented Oct 9, 2025

Codecov Report

❌ Patch coverage is 84.57711% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.73%. Comparing base (9514637) to head (61f0ead).
⚠️ Report is 60 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/blinded_path/payment.rs 83.78% 9 Missing and 3 partials ⚠️
lightning/src/ln/msgs.rs 66.66% 7 Missing and 1 partial ⚠️
lightning/src/ln/onion_payment.rs 65.21% 6 Missing and 2 partials ⚠️
lightning/src/routing/router.rs 70.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4152      +/-   ##
==========================================
+ Coverage   88.57%   88.73%   +0.16%     
==========================================
  Files         179      180       +1     
  Lines      134374   135518    +1144     
  Branches   134374   135518    +1144     
==========================================
+ Hits       119016   120258    +1242     
+ Misses      12604    12497     -107     
- Partials     2754     2763       +9     
Flag Coverage Δ
fuzzing 21.71% <1.92%> (-0.09%) ⬇️
tests 88.58% <84.57%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

PaymentDummyTlv 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.
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.

Yay!

let tlvs = intermediate_nodes
.iter()
.map(|node| BlindedPaymentTlvsRef::Forward(&node.tlvs))
.chain((0..dummy_count).map(|_| BlindedPaymentTlvsRef::Dummy(&PaymentDummyTlv)))
Copy link
Collaborator

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?

Copy link
Member Author

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!

@shaavan
Copy link
Member Author

shaavan commented Nov 14, 2025

Updated: .01 → .02

Summary

This iteration introduces dummy hops before the actual receiver in a blinded path.
The current structure being evaluated is:

forward nodes -> dummy hops -> final receiver

The goal is to defend against potential timing side-channels at the receiver.
The mechanism works like this:

  • When the final receiver gets what appears to be the first dummy hop,
    it decodes and stores a new update_add_htlc.
  • This stored HTLC is decrypted only in the next background processing cycle,
    preventing immediate timing correlation attacks.

Conceptually this works, but it leads to two concrete issues, surfaced by failing tests.


Test Failures

1. rejects_keysend_to_non_static_invoice_path

Scenario:

Alice (Payer) --ss1--> Dummy01 --ss2--> Dummy02 --ss3--> Bob (Payee)

What happens:

  • Alice constructs the onion as usual.
    She cannot know which hops are dummy, so she generates a shared secret (ss) for each hop.
  • On receiving an invalid keysend, Bob tries to fail the HTLC backwards.
  • As expected, Bob encrypts the failure with ss3.

The problem:
Dummy hops are not part of an actual HTLC chain.
They are parsed and consumed in-place, so there is no backward path.

So Bob tries to send an encrypted failure using ss3… but there's nowhere for it to go.
For this to work, Bob would need to remember ss1 (the introduction hop's secret) so he could encrypt the error correctly, but with the current design, that state doesn’t exist anywhere without invasive changes to internal flow structures.

The receiver cannot fail an HTLC backward when dummy hops are present.


2. creates_offer_with_blinded_path_using_unannounced_introduction_node

This test explores an Offer scenario with an unannounced introduction node.

Background (pre-dummy-hops):

  • Alice creates an Offer.

  • Bob tries to pay it.

  • Alice and Bob share an unannounced channel.

  • Alice constructs a two-hop blinded path to herself, using Bob as the introduction node.

  • Bob recognizes himself as the introduction node and categorizes the remainder of blinded path as:

    CandidateRouteHop::OneHopBlinded { ... }
    

    (ref: router.rs L3346–3349

  • As per spec logic, this leads Bob to treat the fee as 0:

    (ref: router.rs L1750–1752)

All good.

After dummy hops are introduced before the final receiver:

  • Bob no longer sees himself as the sole real hop.
  • From his point of view, he looks like an intermediate hop in a multi-hop blinded path.
  • Because the hops are blinded, he cannot tell that the extra hops are dummy.
  • Consequently, he charges non-zero fees, leading to overpayment, and the test fails.

Also Alice cannot manually set the fee to zero in blinded_payinfo, because any node might be the payer.

This breaks the “one-hop-blinded” assumption used for Offers payable through unannounced nodes.


Conclusion

Both failures point to the same root issue:

Inserting dummy hops changes the semantic meaning of the blinded path for both the sender and the introduction node.

  • The payee loses the ability to fail an HTLC cleanly.
  • The introduction node loses its ability to detect a one-hop blinded path.
  • Neither side can special-case dummy hops without undermining the privacy properties dummy hops were meant to introduce.

I'm documenting this here as part of evaluating whether the dummy-hop approach (in this form) is viable, and to surface the structural implications before iterating further.

Feedback and alternative approaches welcome.

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.

3 participants