Skip to content

Introduce Realistic constraints for Dummy Hops#4501

Open
shaavan wants to merge 8 commits into
lightningdevkit:mainfrom
shaavan:dummy-follow
Open

Introduce Realistic constraints for Dummy Hops#4501
shaavan wants to merge 8 commits into
lightningdevkit:mainfrom
shaavan:dummy-follow

Conversation

@shaavan

@shaavan shaavan commented Mar 21, 2026

Copy link
Copy Markdown
Member

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.

@ldk-reviews-bot

ldk-reviews-bot commented Mar 21, 2026

Copy link
Copy Markdown

👋 Thanks for assigning @TheBlueMatt 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.

Comment thread lightning/src/ln/msgs.rs Outdated
}, {
(0, blinding_point, option),
(65537, skimmed_fee_msat, option),
(65539, dummy_hops_skimmed_fee_msat, option),

Copy link
Copy Markdown
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 thread lightning/src/ln/channelmanager.rs Outdated
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
Copy Markdown
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 thread lightning/src/ln/onion_utils.rs Outdated
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
Copy Markdown
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 thread lightning/src/ln/channel.rs Outdated
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
Copy Markdown
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.

Comment thread lightning/src/ln/channel.rs Outdated
state: OutboundHTLCState,
source: HTLCSource,
blinding_point: Option<PublicKey>,
dummy_hops_skimmed_fee_msat: Option<u64>,

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

ldk-claude-review-bot commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator

No new issues found.

The PR has been substantially refactored since my prior review. All five issues I flagged previously are now resolved:

  • Wire-injection (msgs.rs / channelmanager.rs:5195 / onion_utils.rs:2579) — Resolved. dummy_skimmed_fee no longer exists on the UpdateAddHTLC wire format. The fee is now computed strictly from local dummy-hop peeling and carried in the internal PendingDecodeAddHTLC struct and the peel_payment_onion_inner accumulator. A malicious peer can no longer inject a value.
  • channel.rs:521 / channel.rs:16009 — Resolved. The current approach does not add a field to OutboundHTLCOutput; channel.rs is untouched by this diff, so the missing-field and indentation concerns no longer apply.

I traced the full new flow and found it sound:

  • Dummy fee accumulation is bounded by the original HTLC amount and guarded against overflow (channelmanager.rs:7529, onion_payment.rs:571).
  • Serialization correctly moves decode_update_add_htlcs from even TLV 14 to even TLV 16, writing None to 14 and converting legacy blobs; new field takes precedence on read.
  • compute_payinfo reorder (payment.rs:1148) is equivalent to prior behavior when no dummy hops exist, and the per-dummy amt_to_forward reduction of htlc_maximum is internally consistent with the intermediate-node logic.
  • Event TLVs use odd numbers (17 for PaymentClaimable, 13 for PaymentClaimed) for backward-compatible skip; internal structs use even TLVs with default values.
  • ceil_bucket rounds fees up, so advertised blinded fees are always ≥ real fees (no underfunding); buckets terminate in u32::MAX so the .expect() calls cannot panic.

Non-blocking design observation (not posted inline): DummyTlvs::from_forward_tlvs caps params to min(forward, DEFAULT), which pins dummy CLTV delta to 40 (the smallest bucket) whenever the forward hop is ≥ 40. This slightly cuts against the PR's stated "indistinguishable from real relays" goal, though path blinding hides per-hop parameters so it is not a concrete information leak.

@codecov

codecov Bot commented Mar 21, 2026

Copy link
Copy Markdown

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.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@wpaulino wpaulino removed their request for review March 23, 2026 15:58
@ldk-reviews-bot

Copy link
Copy Markdown

✅ Added second reviewer: @wpaulino

@wpaulino wpaulino requested review from jkczyz and removed request for wpaulino March 23, 2026 15:59
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 1st Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@shaavan

shaavan commented Mar 27, 2026

Copy link
Copy Markdown
Member Author

Updated .01 → .02

Changes:

  • Keeps dummy-hop skimmed fees as trusted local receive-side state instead of serializing them into update_add_htlc, while still surfacing them on claim events.
  • Adds compatibility handling for older serialized/on-wire data by manually decoding the legacy HTLC TLV format.
  • Refactors dummy-hop minimum-HTLC calculation into a shared helper and applies it consistently when computing blinded payinfo with realistic dummy-hop defaults.
  • Tightens router-side blinded path admission to reject paths whose amount falls below the minimum implied by the dummy-hop tail, with updated test coverage around the new minimum handling.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 2nd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 3rd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Comment thread lightning/src/ln/channel.rs Outdated
Comment thread lightning/src/blinded_path/payment.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

🔔 4th Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 5th Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot

Copy link
Copy Markdown

🔔 6th Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMatt TheBlueMatt removed the request for review from jkczyz April 6, 2026 20:02
shaavan added 4 commits June 22, 2026 13:27
Dummy hops reuse blinded forwarding validation, but an amount or CLTV
underflow at this stage occurs while processing a locally peeled dummy hop.
Report that context instead of describing the failure as a generic blinded
forward so logs identify the failing layer accurately.
Exact forwarding fees in blinded payment paths can reveal unusually specific
local policy. Round base and proportional fees upward into common network
buckets while retaining the existing CLTV buckets and rejecting unsupported
CLTV deltas.

Rounding upward preserves sufficient forwarding fees without exposing precise
settings. Update the asynchronous payment test to use a bucket-aligned fee so
its deterministic route ordering and fee expectations remain stable.
Receive-side accounting needs to distinguish value retained by locally peeled
dummy hops from the amount delivered to the final payment payload. Add
`dummy_skimmed_fees_msat` to `PaymentClaimable` and `PaymentClaimed`, then
thread the value through pending receive state and claimed HTLC bookkeeping.

Persist the new fields as optional TLVs with zero defaults so old serialized
events and payment state remain readable. Later commits populate the value
while peeling dummy hops.
Inbound HTLCs may peel a dummy hop and then defer decoding the rewritten
`update_add_htlc` through `ChannelManager`. Storing only the rewritten message
loses the fee retained by each dummy hop before the final receive payload is
decoded.

Store accumulated dummy-hop fees alongside pending update-add state and carry
them into receive routing. Use a new serialized field for the extended state
while converting the legacy representation with no accumulated fee, preserving
restart compatibility.
shaavan and others added 4 commits June 22, 2026 13:27
Onion decoding may peel multiple local dummy hops recursively without passing
through deferred `ChannelManager` state. Track the amount removed at each peel
and attach the accumulated value when the final receive payload is reached.

Use checked accumulation and fail with an onion-blinding error on overflow so
receive-side events report the full locally retained fee without weakening
validation.
Payment claim helpers previously returned only the amount delivered to the
final receive payload. Include `dummy_skimmed_fees_msat` from `PaymentClaimed`
so route tests compare the sender's forwarded amount against both the delivered
value and fees retained by local dummy hops.
Zero-fee dummy hops are distinguishable from realistic forwarding hops. Derive
their fee and CLTV requirements from a nearby forwarding TLV when available,
then cap each value at common network defaults to avoid reproducing outlier
policies too precisely. Use the same nonzero defaults when no nearby forwarding
hop is available.

The advertised HTLC maximum bounds the amount entering the payee, including
fees retained by its dummy hops. Apply the payee maximum before processing
dummy hops and subtract each dummy-hop fee so `compute_payinfo` exposes the
largest final amount that can traverse the path.

Update asynchronous, blinded-payment, and offer tests for the nonzero dummy-hop
fee and CLTV defaults. Account explicitly for conservative fee aggregation and
the resulting sender overpayment in claim assertions.

AI-assisted: Error Findings

Co-Authored-By: OpenAI Codex <codex@openai.com>
Callers building blinded payment paths may need to supply explicit dummy-hop
relay parameters for path-length privacy. Make
`BlindedPaymentPath::new_with_dummy_hops` public now that fee aggregation, CLTV
deltas, and HTLC bounds are covered by the preceding implementation and tests.

Remove the stale TODOs for adding dummy hops and validating their end-to-end
behavior.
@shaavan

shaavan commented Jun 22, 2026

Copy link
Copy Markdown
Member Author

Updated .02 → .03

Thanks, @TheBlueMattChanges:

  • Introduce ForwardTlvs fee bucketing.
  • Introduce DummyTlvs fee selection logic so dummy hops are not trivially distinguishable from other ForwardTlvs.

Note:
As this version diverged significantly from the original PR in both content and commit structure, I rebased and squashed the changes into a single commit to keep the history easier to follow.

@shaavan shaavan requested a review from TheBlueMatt June 22, 2026 08:45
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

4 participants