Skip to content

Add idempotency check to prevent duplicate InvoiceReceived events #3658

New issue

Have a question about this project? No Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “No 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? No Sign in to your account

Merged
merged 2 commits into from
Apr 28, 2025

Conversation

shaavan
Copy link
Member

@shaavan shaavan commented Mar 10, 2025

Solves #3653

When manually_handle_bolt12_invoice is enabled, we shouldn’t emit multiple InvoiceReceived events for the same invoice — it can be confusing and misleading for consumers of the event stream.

This PR adds a simple idempotency check to make sure we only emit the event once per invoice.

Also added a test to make sure this behavior stays in place going forward.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Mar 10, 2025

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

@wpaulino wpaulino requested a review from jkczyz March 10, 2025 18:20
Copy link
Member Author

Choose a reason for hiding this comment

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

@benthecarman

it seems send_payment_for_bolt12_invoice is not idempotent either so this would effect everyone using manually_handle_bolt12_invoices

Hi Ben! AFAIU, send_payment_for_bolt12_invoice does seem idempotent, as in, it won’t allow paying the same invoice twice (see this link).

Let me know if I’m misunderstanding the issue or if there’s still a potential problem here that I might be missing. Thanks!

@ldk-reviews-bot
Copy link

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

@shaavan
Copy link
Member Author

shaavan commented Mar 12, 2025

Updated from pr3658.01 to pr3658.02 (diff):
Addressed @jkczyz (offline) comments

Changes:

  1. Update the approach, such that we update the PendingOutboundPayment entry the moment we receive the invoice to ensure event generation idempotency.

@jkczyz jkczyz requested a review from TheBlueMatt March 12, 2025 20:40
@jkczyz
Copy link
Contributor

jkczyz commented Mar 12, 2025

@TheBlueMatt Is the following line still correct after this change?

PendingOutboundPayment::InvoiceReceived { .. } |

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.

Yes, I believe that specific logic is still fine.

@shaavan
Copy link
Member Author

shaavan commented Mar 13, 2025

Updated from pr3658.02 to pr3658.03 (diff):
Addressed @jkczyz comments

Changes:

  1. Restructure functions, to have better logical seperation.
  2. Encapsulate handle_message code to improve code readability.

Comment on lines 871 to 873
if !with_manual_handling { self.mark_invoice_received(payment_id, payment_hash)? }

let (retry_strategy, params_config) = self.received_invoice_details(payment_id)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we avoid taking the lock twice here when with_manual_handling is false?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for the pointer, Jeff!
I’ve updated the mark_invoice_received function to also return the retry and route_params, so we can avoid taking the lock twice in pr3658.04.
Let me know if this looks good — thanks again!

Comment on lines 1803 to 1818
fn received_invoice_details(
&self, payment_id: PaymentId,
) -> Result<(Retry, RouteParametersConfig), Bolt12PaymentError> {
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
hash_map::Entry::Occupied(entry) => match entry.get() {
PendingOutboundPayment::InvoiceReceived {
retry_strategy, route_params_config, ..
} => {
return Ok((*retry_strategy, *route_params_config))
},
_ => return Err(Bolt12PaymentError::DuplicateInvoice),
},
hash_map::Entry::Vacant(_) => return Err(Bolt12PaymentError::UnexpectedInvoice),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably can re-inline this to avoid taking the lock again.

@shaavan
Copy link
Member Author

shaavan commented Mar 15, 2025

Updated from pr3658.03 to pr3658.04 (diff):
Addressed @jkczyz comments

Changes:

  1. Update function signatures, and code to avoid taking lock twice.
  2. Minor nit fixes

Copy link

codecov bot commented Mar 15, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 91.47%. Comparing base (5bc9ffa) to head (d57b153).
Report is 277 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3658      +/-   ##
==========================================
+ Coverage   89.21%   91.47%   +2.26%     
==========================================
  Files         155      157       +2     
  Lines      118966   145966   +27000     
  Branches   118966   145966   +27000     
==========================================
+ Hits       106133   133525   +27392     
+ Misses      10253     9985     -268     
+ Partials     2580     2456     -124     

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

Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Looks good. Just some small changes.

@shaavan
Copy link
Member Author

shaavan commented Mar 18, 2025

Updated from pr3658.04 to pr3658.05 (diff):
Addressed @jkczyz comments

Changes:

  1. Introduce new wrapper function mark_invoice_received for ChannelManager usage.
  2. Update function signature to take invoice as input, and output invoice.payment_hash()
  3. Update docs

Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

LGTM

@shaavan
Copy link
Member Author

shaavan commented Mar 19, 2025

Updated from pr3658.05 to pr3658.06 (diff):

Changes:

  1. Squash commits.

@jkczyz jkczyz self-requested a review March 19, 2025 15:06
jkczyz
jkczyz previously approved these changes Mar 19, 2025
@shaavan
Copy link
Member Author

shaavan commented Apr 4, 2025

Updated from pr3658.06 to pr3658.07 (diff):
Addressed @TheBlueMatt comments

Changes:

  1. Remove the manual_handling parameter, and updated logic to upgrade-if-necessay-and-get-details.
  2. Updated documentation to reflect that InvoiceReceived may now be written to the disk.

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.

Changes LGTM!

shaavan added 2 commits April 19, 2025 00:16
Ensures `InvoiceReceived` events are not generated multiple times
when `manually_handle_bolt12_invoice` is enabled.
Duplicate events for the same invoice could cause confusion—this
change introduces an idempotency check to prevent that.
@shaavan
Copy link
Member Author

shaavan commented Apr 18, 2025

Updated from pr3658.07 to pr3658.08 (diff):
Addressed @TheBlueMatt comments

Changes:

  1. Squash commits
  2. Renamed function and variable for improved clarity and better alignment with their actual purpose.

@TheBlueMatt TheBlueMatt merged commit 233aa39 into lightningdevkit:main Apr 28, 2025
28 checks passed
@TheBlueMatt
Copy link
Collaborator

Backported in #3757

TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this pull request Apr 30, 2025
v0.1.3 - Apr 30, 2025 - "Routing Unicode in 2025"

Bug Fixes
=========

 * `Event::InvoiceReceived` is now only generated once for each `Bolt12Invoice`
   received matching a pending outbound payment. Previously it would be provided
   each time we received an invoice, which may happen many times if the sender
   sends redundant messages to improve success rates (lightningdevkit#3658).
 * LDK's router now more fully saturates paths which are subject to HTLC
   maximum restrictions after the first hop. In some rare cases this can result
   in finding paths when it would previously spuriously decide it cannot find
   enough diverse paths (lightningdevkit#3707, lightningdevkit#3755).

Security
========

0.1.3 fixes a denial-of-service vulnerability which cause a crash of an
LDK-based node if an attacker has access to a valid `Bolt12Offer` which the
LDK-based node created.
 * A malicious payer which requests a BOLT 12 Invoice from an LDK-based node
   (via the `Bolt12InvoiceRequest` message) can cause the panic of the
   LDK-based node due to the way `String::truncate` handles UTF-8 codepoints.
   The codepath can only be reached once the received `Botlt12InvoiceRequest`
   has been authenticated to be based on a valid `Bolt12Offer` which the same
   LDK-based node issued (lightningdevkit#3747, lightningdevkit#3750).
No Sign up for free to join this conversation on GitHub. Already have an account? No Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants