Skip to content
New issue

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

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

Add idempotency check to prevent duplicate InvoiceReceived events #3658

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

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.

// current one is handled by the user.
match self.pending_outbound_payments.lock().unwrap().entry(payment_id) {
hash_map::Entry::Occupied(entry) => match entry.get() {
PendingOutboundPayment::InvoiceReceived {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this race-y? If a user (for some reason, they shouldn't, but still) calls send_payment_for_bolt12_invoice twice in two separate threads we could end up actually sending the payment twice.

Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't the second lock acquisition happening in send_payment_for_bolt12_invoice_internal prevent that? We'd still attempt to find a path twice but would fail because the first attempt transitions the state to Retryable causing the second attempt to return DuplicateInvoice.

@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 1826
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.11%. Comparing base (5bc9ffa) to head (4ec8d9f).
Report is 85 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3658      +/-   ##
==========================================
+ Coverage   89.21%   91.11%   +1.90%     
==========================================
  Files         155      156       +1     
  Lines      118966   135669   +16703     
  Branches   118966   135669   +16703     
==========================================
+ Hits       106133   123613   +17480     
+ Misses      10253     9606     -647     
+ Partials     2580     2450     -130     

☔ 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 added 2 commits March 19, 2025 17:11
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 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
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.

4 participants