Skip to content

Correct blinded path forwarding CLTV expiry check#4555

Open
TheBlueMatt wants to merge 1 commit intolightningdevkit:mainfrom
TheBlueMatt:2026-04-bp-cltv-check
Open

Correct blinded path forwarding CLTV expiry check#4555
TheBlueMatt wants to merge 1 commit intolightningdevkit:mainfrom
TheBlueMatt:2026-04-bp-cltv-check

Conversation

@TheBlueMatt
Copy link
Copy Markdown
Collaborator

The PaymentConstraints::max_cltv_expiry field exists to ensure a blinded path expires across the entire path at once - once the path is expired it will be rejected by the introduction node rather than traversing the entire path and failing at the destination.

This was broken by the fact that we were checking the outgoing CLTV value rather than the incoming one, which admittedly isn't clear in the spec but is somewhat implied. Here we fix this, updating a test which was actually (kinda) exploiting this privacy loss rather than allowing the HTLC to fail at the introduction node.

This, of course, does not risk funds loss as our own CLTV policy is still enforced on top. The only impact it could have is a recipient which was relying on blinded path expiry to avoid some cost (e.g. LSPS5 node wakeup cost) involved in receiving an HTLC they ultimately fail, though I'm not aware of any practical deployment where that is a concern.

Reported by Jordan Mecom of Block's Security Team

The `PaymentConstraints::max_cltv_expiry` field exists to ensure
a blinded path expires across the entire path at once - once the
path is expired it will be rejected by the introduction node rather
than traversing the entire path and failing at the destination.

This was broken by the fact that we were checking the outgoing CLTV
value rather than the incoming one, which admittedly isn't clear in
the spec but is somewhat implied. Here we fix this, updating a test
which was actually (kinda) exploiting this privacy loss rather than
allowing the HTLC to fail at the introduction node.

This, of course, does not risk funds loss as our own CLTV policy is
still enforced on top. The only impact it could have is a recipient
which was relying on blinded path expiry to avoid some cost (e.g.
LSPS5 node wakeup cost) involved in receiving an HTLC they
ultimately fail, though I'm not aware of any practical deployment
where that is a concern.

Reported by Jordan Mecom of Block's Security Team
@ldk-reviews-bot
Copy link
Copy Markdown

ldk-reviews-bot commented Apr 13, 2026

I've assigned @jkczyz as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@ldk-claude-review-bot
Copy link
Copy Markdown
Collaborator

The PR is clean. Here's my analysis:

Core fix (onion_payment.rs:69): The change from outgoing_cltv_value to inbound_cltv_expiry is correct. The forwarding hop's max_cltv_expiry is constructed in the router (router.rs:180-183) as receiver_max_cltv.saturating_add(cltv_expiry_delta), designed to be checked against the inbound CLTV. The old code checked against inbound - delta, effectively requiring inbound > receiver_max + 2*delta before rejecting, which was too lenient.

Test changes: Properly updated to expect failure at the introduction node (HTLCHandlingFailureType::InvalidOnion at nodes[1]) instead of at the receiver, matching the corrected behavior.

Consistency: All other call sites of check_blinded_payment_constraints (receive at line 302 and trampoline receive at line 338) already use the inbound CLTV expiry (cltv_expiry), so they're unaffected and correct.

No issues found.

@ldk-reviews-bot ldk-reviews-bot requested a review from jkczyz April 13, 2026 01:35
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