Skip to content

Introduce Realistic constraints for Dummy Hops#4501

Open
shaavan wants to merge 6 commits intolightningdevkit:mainfrom
shaavan:dummy-follow
Open

Introduce Realistic constraints for Dummy Hops#4501
shaavan wants to merge 6 commits intolightningdevkit:mainfrom
shaavan:dummy-follow

Conversation

@shaavan
Copy link
Member

@shaavan shaavan commented Mar 21, 2026

Resolves #4326. Follow-up to #4152.

Overview

This PR makes dummy hops in blinded payment paths behave like real forwarding relays across construction, routing, receive handling, and testing.

It introduces realistic relay parameters, propagates dummy-hop fees end to end, and ensures that hidden-hop constraints are enforced consistently. It also exposes a public constructor for building blinded paths with dummy hops, backed by end-to-end test coverage.

This change is driven by a core requirement: dummy hops only provide privacy if they are indistinguishable from real relays.

Previously, dummy hops used zeroed parameters and incomplete fee handling, which could make them stand out and lead to inconsistencies between advertised payinfo and actual enforcement. This PR removes those gaps and aligns dummy-hop behavior with real routing semantics.

Key Changes

  • Align dummy-hop behavior with real relay semantics: Introduce realistic relay parameters (fees, CLTV delta, HTLC minimum), enforce hidden-hop constraints during receive handling, and return errors for accurate failure signaling.

  • Ensure end-to-end correctness and observability: Propagate dummy-hop fees through receive handling and expose them via PaymentClaimable and PaymentClaimed, update test helpers and add end-to-end coverage (payinfo, underpayment, HTLC minimum), and make BlindedPaymentPath::new_with_dummy_hops public with validated behavior.

shaavan and others added 6 commits March 21, 2026 18:48
When amount or CLTV validation underflows on a dummy hop, return a
dummy-hop-specific error. This avoids misleading logs and makes
blinded-path failures easier to debug.
Propagate dummy-hop skimmed fees through HTLC peeling, channel
persistence, and receive-side payment tracking.

Expose the accumulated value on `PaymentClaimable` and
`PaymentClaimed`.

This keeps `amount_msat` focused on the amount delivered to the final
receive TLVs, while still surfacing the additional revenue from dummy
hops. Receivers can account for these fees without changing the meaning
of existing skimmed-fee fields.
Update functional test helpers and their callers to derive per-path
claim amounts and expected forwarded values when dummy hops are present,
including cumulative fees across multiple dummy hops.

Without this, tests claiming blinded payments with dummy hops treat the
payee's `amount_msat` as the amount forwarded through the route, which
no longer matches the wire amounts once dummy-hop fees are applied.
Replace zeroed dummy-hop relay parameters with non-trivial default
fee, CLTV, and HTLC minimum values, and propagate these defaults into
blinded payinfo construction and routing tests.

Dummy hops only preserve their privacy value if they behave like
plausible forwarding hops. Realistic defaults ensure hidden hops affect
fees and CLTV like real relays, instead of standing out as zero-cost
padding.
Expand dummy-hop blinded payment tests to cover advertised payinfo
behavior end to end.

Introduce an underpayment case that strips dummy-hop relay fees from
the sender's payinfo, and assert that receive-path construction exposes
the expected aggregated base fee and CLTV delta for non-default dummy
hops.

Also verify that under-advertising the blinded path HTLC minimum causes
the receiver to reject the payment while processing hidden dummy hops.

Co-Authored-By: OpenAI Codex <codex@openai.com>
Allow external callers to construct blinded receive paths with dummy
hops via `new_with_dummy_hops`.
@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Mar 21, 2026

I've assigned @wpaulino 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.

}, {
(0, blinding_point, option),
(65537, skimmed_fee_msat, option),
(65539, dummy_hops_skimmed_fee_msat, option),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: dummy_hops_skimmed_fee_msat is serialized in the wire update_add_htlc message, but it should be an internal-only field. The field is only meaningful for the receiver's local dummy-hop peeling, never transmitted between peers.

Because it appears in the wire format, a malicious channel peer can inject an arbitrary value. That injected value then flows — unsanitized — into PaymentClaimable and PaymentClaimed events (via channelmanager.rs:5194 and onion_utils.rs:2578), misleading wallet software about dummy-hop fee earnings.

The receiver should either:

  1. Strip/ignore dummy_hops_skimmed_fee_msat from the deserialized wire message before processing (e.g., force it to None), or
  2. Remove it from the wire serialization entirely and only carry it in an internal struct used during local dummy-hop peeling.

As-is, any value a peer sets in TLV 65539 gets added to the real accumulated dummy-hop fees.

Comment on lines 5192 to 5195
create_recv_pending_htlc_info(decoded_hop, shared_secret, msg.payment_hash,
msg.amount_msat, msg.cltv_expiry, None, allow_underpay, msg.skimmed_fee_msat,
msg.amount_msat, msg.cltv_expiry, None, allow_underpay,
msg.dummy_hops_skimmed_fee_msat, msg.skimmed_fee_msat,
msg.accountable.unwrap_or(false), current_height)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: msg.dummy_hops_skimmed_fee_msat comes directly from the wire-deserialized UpdateAddHTLC sent by our channel peer. A malicious peer can set this to any value, and it will propagate into the PendingHTLCInfo, then into the ClaimableHTLC, and finally into the PaymentClaimable / PaymentClaimed events — corrupting the reported dummy-hop fee for the application layer.

This should be forced to None here (or earlier, at deserialization) since the real value is only determined by local dummy-hop peeling in process_pending_update_add_htlcs.

Comment on lines +2577 to +2579
let this_dummy_fee = msg.amount_msat.saturating_sub(outgoing_amt_msat);
let dummy_hops_skimmed_fee_msat =
msg.dummy_hops_skimmed_fee_msat.unwrap_or(0).saturating_add(this_dummy_fee);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the wire-injection issue: msg.dummy_hops_skimmed_fee_msat originates from the deserialized wire message. If a malicious peer injects a large value, it gets accumulated here with the real dummy-hop fee. The initial value should always be 0, not the untrusted wire value.

Consider changing line 2578 to always start accumulation from 0 for the first dummy hop, or ensure the field is stripped from the wire message before this function is called.

Comment on lines +16004 to +16009
payment_id: PaymentId([42; 32]),
bolt12_invoice: None,
},
dummy_hops_skimmed_fee_msat: None,
skimmed_fee_msat: None,
blinding_point: None,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broken indentation: payment_id and bolt12_invoice (fields of the inner HTLCSource::OutboundRoute) are under-indented by one tab level, and dummy_hops_skimmed_fee_msat, skimmed_fee_msat, blinding_point (fields of the outer OutboundHTLCOutput) are also under-indented. While this compiles, it makes the struct nesting misleading. Running cargo fmt should fix this.

state: OutboundHTLCState,
source: HTLCSource,
blinding_point: Option<PublicKey>,
dummy_hops_skimmed_fee_msat: Option<u64>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compilation error: This new required field is missing from many OutboundHTLCOutput construction sites in the BOLT spec test vectors later in this file (e.g., lines ~16869, ~16885, ~17301, ~17556, ~17700, ~17935, and others). There are at least 12 construction sites that don't include dummy_hops_skimmed_fee_msat, which will cause a compilation failure.

Each of these sites needs dummy_hops_skimmed_fee_msat: None, added.

@ldk-claude-review-bot
Copy link
Collaborator

@codecov
Copy link

codecov bot commented Mar 21, 2026

Codecov Report

❌ Patch coverage is 95.81152% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.20%. Comparing base (56c813f) to head (0badc51).
⚠️ Report is 128 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 80.00% 5 Missing and 1 partial ⚠️
lightning/src/ln/channelmanager.rs 95.45% 1 Missing ⚠️
lightning/src/ln/functional_test_utils.rs 98.90% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4501      +/-   ##
==========================================
+ Coverage   86.05%   86.20%   +0.14%     
==========================================
  Files         159      160       +1     
  Lines      105759   107721    +1962     
  Branches   105759   107721    +1962     
==========================================
+ Hits        91013    92857    +1844     
- Misses      12227    12237      +10     
- Partials     2519     2627     +108     
Flag Coverage Δ
tests 86.20% <95.81%> (+0.14%) ⬆️

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.

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.

Dummy blinded payment path followups

3 participants