invoices: clean up invoice lookup handling - #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.
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.
934988d to
ff7f12e
Compare
Change Description
This PR cleans up how final-hop invoice processing reports errors that occur
before an invoice update is attempted. Unexpected invoice lookup errors remain
retryable internal errors and trigger link recovery. HTLC interceptor errors
for confirmed-new HTLCs produce a distinct failure resolution while retaining
the generic
FailIncorrectDetailsresponse on the wire.After a successful lookup, known replays are resolved from their persisted
invoice HTLC state before the interceptor runs. This preserves accepted,
settled, and canceled outcomes and avoids invoking the interceptor again for a
decided HTLC. A second replay check remains inside the atomic invoice update.
The interceptor-error handling remains local to the affected HTLC. Since the
same invoice registry entry point is also used during on-chain resolution, the
incoming contest resolver keeps this processing outcome non-terminal,
preserving its existing unresolved behavior. Unexpected lookup errors also
leave the resolver unresolved through the existing Go error path.
The existing wrong-payment-address integration test now also checks that both
peers continue to report the channel as active after the payment fails.
Review map
Steps to Test
The
itestpackage was compiled locally. The full wrong-payment-addressintegration test was not executed locally.
Pull Request Checklist
Testing
Code Style and Documentation
lnclicommands are introduced.