Repository navigation
invoices: contain modifier errors and bypass interception for replays - #11161
Conversation
18a5107 to
bee2865
Compare
🔴 PR Severity: CRITICAL
🔴 Critical (1 file)
🟠 High (2 files)
🟢 Low (7 files)
AnalysisThe highest-severity file touched is To override, add a |
GeorgeTsagk
left a comment
There was a problem hiding this comment.
Thanks for the cleanup, the core change is sound and well covered by unit tests. Verified locally: the new unit tests pass, full invoices and contractcourt suites pass, and htlcswitch passes with -tags dev. Requesting changes for the rpcFailureResolution gap, the rest is informational. (Supersedes my earlier dismissed review, same content as inline threads.)
|
|
||
| // ResultInvoiceLookupError is returned when an unexpected error occurs | ||
| // while looking up an invoice. | ||
| ResultInvoiceLookupError |
There was a problem hiding this comment.
Blocking: these two new outcomes also need handling in rpcFailureResolution (lnrpc/routerrpc/subscribe_events.go). That switch's default returns unknown fail resolution, and the fail path emits a LinkFailEvent carrying the outcome as FailureDetail (link.go sendHTLCError -> NotifyLinkFailEvent), so any open SubscribeHtlcEvents stream terminates with that error once such an event fires (router_server.go returns the error from rpcHtlcEvent). Confirmed with a quick test calling rpcFailureResolution(ResultInvoiceLookupError): unknown fail resolution: invoice lookup failed.
There was a problem hiding this comment.
Fixed in the rewritten first commit (e7111c4f77). I added INVOICE_LOOKUP_ERROR and INVOICE_INTERCEPTOR_ERROR to the router RPC FailureDetail enum, mapped both outcomes in rpcFailureResolution, regenerated the protobuf/Swagger outputs, and added focused mapping tests. make rpc, go test ./lnrpc/routerrpc, and make lint pass.
|
|
||
| // The malformed payment should only fail the HTLC, leaving the channel | ||
| // active on both sides. | ||
| ht.AssertChannelActive(alice, chanPoint) |
There was a problem hiding this comment.
Note this doesn't exercise the new code path: wrong payment address fails via ResultAddressMismatch inside UpdateInvoice, which was already an HTLC-level fail before this PR. Covering the new paths in an itest would require injecting DB or interceptor faults, which the itest framework can't easily do, so unit-test coverage is the practical option. The added assertions still have value as hardening of a related path.
There was a problem hiding this comment.
Agreed. This assertion hardens the related ResultAddressMismatch path; it does not exercise either new pre-update processing failure. Direct itest coverage would require DB/interceptor fault injection that the harness does not currently expose, so the invoice registry, link, and contractcourt unit tests are the intended coverage for the new paths. I will keep this assertion as related channel-stability hardening in its separate itest commit.
| log.Errorf("Unable to look up invoice %v for circuit %v: %v", | ||
| invoiceRef, ctx.circuitKey, err) | ||
|
|
||
| return ctx.failRes(ResultInvoiceLookupError), nil, nil |
There was a problem hiding this comment.
Discussion / non-blocking: this changes semantics on transient errors, not just link stability. A transient lookup error now definitively fails the payment (FailIncorrectDetails), whereas before the HTLC replayed after link re-establishment and could still settle once the DB recovered. Arguably the lesser evil given the old path also sent a BOLT-1 Error to the peer, but senders now see a terminal-looking failure for a possibly transient local problem.
There was a problem hiding this comment.
Agreed. This is an intentional tradeoff. Previously the error tore down the link and left the HTLC eligible for replay after reconnection; now the link stays active, but FailIncorrectDetails makes the sender stop the current payment attempt even when the local fault was transient. We prefer containing an unexpected processing fault to the affected HTLC rather than disconnecting the peer. The PR description calls out this before/after behavior explicitly, so I do not plan a code change for this thread.
| case invoices.ResultInvoiceLookupError, | ||
| invoices.ResultInvoiceInterceptorError: | ||
|
|
||
| return nil, fmt.Errorf("invoice processing "+ |
There was a problem hiding this comment.
Minor: the returned error drops the underlying DB/interceptor error and only carries the outcome. It's logged at Error level in the registry with invoice ref and circuit key, so it's recoverable, but wrapping it here would make contractcourt logs self-contained.
There was a problem hiding this comment.
Agreed that the contractcourt error is less self-contained. The registry logs the original cause together with the invoice reference and circuit key before returning the resolution. Carrying that cause further would require extending HtlcFailResolution (which currently contains only semantic resolution metadata) or introducing another resolution type. Since the cause is already retained in the daemon logs and this does not affect resolver correctness, I would avoid that extra plumbing in this PR and treat self-contained contractcourt errors as possible follow-up hardening.
bee2865 to
934988d
Compare
| log.Errorf("Unable to look up invoice %v for circuit %v: %v", | ||
| invoiceRef, ctx.circuitKey, err) | ||
|
|
||
| return ctx.failRes(ResultInvoiceLookupError), nil, nil |
There was a problem hiding this comment.
i think this needs to distinguish a new HTLC from a replay before returning a fail resolution.
- the first pass may already have recorded this circuit as accepted or settled while the channel response is not locked in yet.
- these lookup/interceptor branches return before
resolveReplayedHtlc, so the replay can get a different outcome. - in the settled case, the invoice can remain
SETTLEDwhile the payer receivesFailIncorrectDetailsand gets the HTLC back.
can we resolve a known replay first, and keep or retry the HTLC when lookup fails and we cannot read its recorded state?
|
|
||
| // TestInvoiceRegistryPreMutationErrors verifies that errors before the invoice | ||
| // update fail only the incoming HTLC and don't mutate the invoice database. | ||
| func TestInvoiceRegistryPreMutationErrors(t *testing.T) { |
There was a problem hiding this comment.
can we run this through the existing invoice DB matrix instead of creating a KV-only factory here?
TestInvoiceRegistryalready owns the KV, sqlite, and postgres setup.- these cases should also cover accepted and settled replays, since that is where returning an immediate failure becomes unsafe.
that would keep the regression tied to the actual affected backends.
934988d to
ff7f12e
Compare
|
@ziggie1984, remember to re-request review from reviewers when ready |
|
I am proposing closing this cleanup, it just is too less of a code improvement which justifies this code change. |
a213438 to
6300207
Compare
|
To help decide whether this PR is worth keeping, these are the concrete behavior changes relative to the existing implementation. This concerns the invoice settlement modifier (
For example, restarting the modifier application while it is handling a payment currently risks disabling the link. With this PR, that new HTLC fails while the channel stays usable. Avoiding a channel-level error also avoids giving the peer that particular reason to force-close; a peer force-close is a possible consequence of the old behavior, not a guaranteed one. The replay distinction is important: the existing software already preserves recorded outcomes once it reaches the replay check. This PR moves that check ahead of the external modifier. A previously settled HTLC can therefore return its recorded preimage without depending on another modifier response. Some behavior is unchanged:
The early replay check and on-chain exception are necessary safeguards for converting modifier errors into HTLC failures. An already-settled replay must not be failed, and an on-chain processing error must not be treated as a terminal invoice decision. Tradeoff: the payer receives generic Assessment: this is worthwhile, narrowly scoped reliability hardening for deployments using the modifier. Its justification is limiting the consequences of a local application failure. We have not established its production frequency, so the case for keeping it should rest on whether that failure isolation and reduced replay dependency justify the added branches and tests. Regression checks found no introduced production regression. The registry race suite passed on KV, SQLite, and PostgreSQL, including canceled/AMP replays and preservation of an accepted MPP shard when another shard encounters a modifier error. The strengthened processing-error tests also passed against the pre-cleanup helper-based implementation. The remaining validation gap is an end-to-end modifier-disconnect test; the wrong-payment-address integration test does not exercise that scenario. |
|
The old code returned a Go error, which propagated up and tore down the channel link. The new code instead returns a fail resolution with a new outcome, But "whenever the modifier errors, fail the HTLC" is not safe as a blanket rule. There are two situations where producing a fail resolution would be actively wrong. Those are the two safeguards:
Both carve-outs are correct. The PR is sound. But there is a tradeoff:
Bottom line: The tradeoff is "a rare, transient receiver-side outage terminally fails in-flight payments instead of being retryable, and MPP siblings linger until timeout". Weighed against "the same outage currently takes down the whole channel and may trigger a peer force-close." My call on this, is to merge. (After rebase) |
6300207 to
c9aced9
Compare
|
@yyforyongyu CI passed PTAL |
|
Pending a rebase, otherwise can be merged! |
Keep unexpected invoice lookup errors retryable instead of turning them into terminal HTLC failures. Resolve recorded HTLCs before invoking the interceptor, while retaining the atomic replay check during the invoice update. Only turn interceptor errors for confirmed-new HTLCs into individual failures. Update failure reporting and multi-backend regression tests to cover the final behavior.
Assert that malformed invoice payments fail only the HTLC and leave the channel active on both peers.
Document that lookup errors remain retryable, interceptor errors fail new HTLCs individually, and replayed HTLCs retain their recorded outcomes.
c9aced9 to
0e6f2fb
Compare
|
Created backport PR for
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-11161-to-v0.20.x-branch
git worktree add --checkout .worktree/backport-11161-to-v0.20.x-branch backport-11161-to-v0.20.x-branch
cd .worktree/backport-11161-to-v0.20.x-branch
git reset --hard HEAD^
git cherry-pick -x 044d260bb6b4fb93d52b966376b0a7e0645dac6b 80b930ded9cea479befeb2b2e136e45c4a9a92f3 0e6f2fbd947b0da5ab95aa009a09fa62c1c40193
git push --force-with-lease |
|
Created backport PR for
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-11161-to-v0.21.x-branch
git worktree add --checkout .worktree/backport-11161-to-v0.21.x-branch backport-11161-to-v0.21.x-branch
cd .worktree/backport-11161-to-v0.21.x-branch
git reset --hard HEAD^
git cherry-pick -x 0e6f2fbd947b0da5ab95aa009a09fa62c1c40193
git push --force-with-lease |
…20.x-branch [v0.20.x-branch] Backport #11161: invoices: contain modifier errors and bypass interception for replays
…21.x-branch [v0.21.x-branch] Backport #11161: invoices: contain modifier errors and bypass interception for replays
Change Description
An error from the invoice settlement modifier currently fails the channel
link and sends a channel-level error to the peer. For example, this happens
when an
Invoices.HtlcModifierclient disconnects while LND is waiting for itsresponse. Stream errors and malformed responses take the same path. A local
application failure can therefore disable the link and may cause the peer
to force-close the channel.
This PR limits modifier errors for new HTLCs to an individual HTLC failure,
leaving the channel link active. It also resolves replayed HTLCs from their
recorded invoice state before invoking the modifier.
Behavior
ResultInvoiceInterceptorErrorwithout updating the invoice or cancelingother HTLCs in its set. The underlying error is logged, and local HTLC
event subscribers receive the new
INVOICE_INTERCEPTOR_ERRORdetail.takes precedence and the modifier is skipped. A previously settled HTLC
must return its preimage even if the modifier is unavailable. The replay
check inside the atomic invoice update is retained.
new processing-error result and leaves the contract unresolved. A
modifier error must not become a terminal on-chain failure decision.
The replay check is required before converting modifier errors into HTLC
failures: otherwise, an error while reprocessing an already-settled HTLC
could incorrectly replace its settlement with a failure. It also removes
the modifier as a dependency when restoring recorded HTLC outcomes.
Scope and tradeoff
This affects the invoice settlement modifier. When no client is connected,
the existing behavior already skips interception. Unexpected invoice lookup
errors still return Go errors because the HTLC's recorded state is unknown;
their recovery behavior is unchanged.
The payer receives the generic
FailIncorrectDetailswire failure, whilethe interceptor-specific detail remains local. An LND payer treats this as
a terminal payment failure, so a temporary modifier outage does not trigger
an automatic payment retry. This trades failure of the affected payment for
keeping the channel usable.
Validation
The complete invoice registry suite, including the new regressions, passed
locally under the race detector:
The registry tests run against KV, SQLite, and PostgreSQL. Coverage includes
lookup-error recovery, accepted and settled replays, canceled replays, AMP
child-preimage recovery, and a modifier error leaving an already accepted
MPP shard unchanged. Focused on-chain exit-resolver and wire/RPC failure
mapping tests also passed under the race detector.
The strengthened processing-error tests also passed against the previous
helper-based registry implementation using a temporary Go overlay, checking
that inlining the helper preserved its behavior.
The existing wrong-payment-address integration test gains assertions that
both peers still report the channel as active after the payment fails.
That test does not exercise modifier disconnection. No end-to-end
modifier-disconnect integration test was run.
Pull Request Checklist
Testing
Code Style and Documentation
lnclicommands are introduced.