Skip to content

Conversation

@doitian
Copy link
Member

@doitian doitian commented Dec 24, 2025

Refactor hold TLC settlement logic to improve payment handling:

  • Add invoice status validation (Expired, Cancelled, Paid states properly handled)
  • Ensure RemoveTlc is called for invalid TLCs
  • Reject overpaid TLCs for MPP invoices
  • For non-MPP invoices, accept only one fulfilled TLC and reject all others

Key Changes

  1. New SettleTlcSetCommand module: Encapsulates TLC settlement logic with clear verification steps and settlement actions
  2. Separated command handling: Split SettleTlcSet into two distinct commands:
    • SettleTlcSet(payment_hash, tlc_ids) - Settle TLCs by given list of (channel_id, tlc_id)
    • SettleHoldTlcSet(payment_hash) - Settle hold TLC set saved for a payment hash
  3. Improved channel actor: Removed is_mpp field from PendingNotifySettleTlc and simplified the settlement flow
  4. Comprehensive test coverage: Added unit tests for various scenarios including invoice status validation, MPP handling, and edge cases

Behavior Notes

This refactoring ensures that, in the presence of multiple payments to the same invoice, at most one payment succeeds. However, due to the lack of database transaction support, there remains a small window where the total accepted amount may exceed the invoice amount.

Additionally, in the case of two concurrent MPP payments, it's possible to partially accept TLCs from both without either payment fully succeeding. This is an inherent limitation of MPP and cannot be avoided on the recipient side.

Related Issues

Closes #965

@doitian doitian force-pushed the fix-over-paid-invoice branch 4 times, most recently from 08c3ea7 to f9525ed Compare December 27, 2025 17:47
@@ -0,0 +1,262 @@
use crate::{
Copy link
Collaborator

Choose a reason for hiding this comment

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

tile file name is not following our current naming style, suggest something like settle_tlcs.

let expiry_duration =
Duration::from_millis(hold_expire_at.saturating_sub(now_timestamp_as_millis_u64()));
if !expiry_duration.is_zero() {
if expiry_duration.is_zero() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

it's better to use hold_expire_at.is_zero(), then

 let expiry_duration =
          Duration::from_millis(hold_expire_at.saturating_sub(now_timestamp_as_millis_u64()));
self.network.send_after(expiry_duration, timeout_command);

in latter code.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think even we add a new field for is_hold in PendingNotifySettleTlc seems more readable.

}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TlcInfoWithChannelId {
Copy link
Member

@quake quake Dec 30, 2025

Choose a reason for hiding this comment

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

We probably don’t need the TlcInfoWithChannelId structure. I’ve looked through the related code before, and from a logical perspective, only the shared_secret and channel_id are actually required to construct the error message.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean using a struct like TlcErrorReplyContext?

Copy link
Member

Choose a reason for hiding this comment

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

Do you mean using a struct like TlcErrorReplyContext?

yes

@doitian doitian force-pushed the fix-over-paid-invoice branch from f9525ed to 2e9bd7a Compare January 6, 2026 05:23
- Add invoice status validation
- Ensure `RemoveTlc` is called for invalid TLCs
- Reject overpaid TLCs for MPP invoices
- For non-MPP invoices, accept only one fulfilled TLC and reject all others

This refactoring ensures that, in the presence of multiple payments to the same invoice, at most one payment succeeds. However, due to the lack of database transaction support, there remains a small window where the total accepted amount may exceed the invoice amount.

Additionally, in the case of two concurrent MPP payments, it's possible to partially accept TLCs from both without either payment fully succeeding. This is an inherent limitation of MPP and cannot be avoided on the recipient side.

Closes nervosnetwork#965
@doitian doitian force-pushed the fix-over-paid-invoice branch from 2e9bd7a to ff00467 Compare January 6, 2026 08:40
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.

[settle_invoice] two nodes can spend the same invoice.

3 participants