Skip to content

l402: pay challenge invoices via router's SendPaymentV2#249

Open
DelleonMcglone wants to merge 1 commit into
lightninglabs:masterfrom
DelleonMcglone:l402-router-sendpayment
Open

l402: pay challenge invoices via router's SendPaymentV2#249
DelleonMcglone wants to merge 1 commit into
lightninglabs:masterfrom
DelleonMcglone:l402-router-sendpayment

Conversation

@DelleonMcglone

Copy link
Copy Markdown

Problem

The L402 client interceptor pays challenge invoices through lndclient's LightningClient.PayInvoice, which drives lnd's legacy SendPaymentSync RPC. That RPC is deprecated in lnd, and on recent builds the payment can fail to even initiate. When that happens the failure mode is nasty:

  1. payL402Token stores the pending token, dispatches the payment, and times out — the operator sees payment timed out. try again to track payment.
  2. Every retry then finds the pending token and calls trackPayment, which fails with payment isn't initiated (lnd has no record of the payment), forever — until the operator manually deletes the pending token file.

Real-world reproduction: running poold 0.7.1 (which vendors this package) against lnd 0.21.0-beta on testnet, InitAccount consistently failed exactly this way; lncli listpayments --include_incomplete showed zero payment attempts, confirming the send never reached the router. Paying the same L402 invoice out-of-band via SendPaymentV2 (lncli payinvoice) succeeded in ~1s and, with a hand-completed token, all subsequent auctioneer RPCs worked.

Change

Dispatch the payment through the router subserver (RouterClient.SendPaymentSendPaymentV2) and consume status updates until a terminal state, mirroring the flow the interceptor's own trackPayment already uses. Notes:

  • i.lnd.Router is already part of the LndServices the interceptor holds — no new dependencies.
  • PaymentStatus.Value/.Fee are msat, so the sat→msat conversions drop out.
  • Non-terminal states (IN_FLIGHT etc.) are consumed and waited out; FAILED returns the failure reason; channel errors, the payment timeout, and parent-context cancellation keep their existing messages/hints.
  • The internal test mock already implements RouterClient.SendPayment, so the interceptor tests move from SendPaymentChannel to RouterSendPaymentChannel and deliver PaymentStatus updates instead of a PaymentResult.

Verification

  • go build ./..., go vet ./l402/... clean
  • go test ./l402/... passes (all interceptor cases incl. pending-token resume and expired-token repay)

🤖 Generated with Claude Code

The client interceptor paid L402 challenge invoices through
lndclient's LightningClient.PayInvoice, which uses lnd's legacy
SendPaymentSync RPC. That RPC is deprecated in lnd and payments
made through it can fail to initiate on recent lnd builds: the
interceptor then times out waiting, leaving a pending token that
every subsequent call tries (and fails) to resume with 'payment
isn't initiated', until the token file is removed manually.

Dispatch the payment through the router subserver instead
(RouterClient.SendPayment -> SendPaymentV2) and consume status
updates until a terminal state, mirroring the existing
trackPayment flow. The router client is already part of the
LndServices the interceptor holds, and the test mock already
implements RouterClient.SendPayment, so the interceptor tests move
from the SendPaymentChannel to the RouterSendPaymentChannel and
deliver PaymentStatus updates instead of a PaymentResult.

Observed in the wild running poold 0.7.1 against lnd 0.21.0-beta
on testnet: InitAccount consistently failed with 'payment timed
out' / 'payment isn't initiated' while lnd showed no payment
attempt at all; paying the same invoice via SendPaymentV2
succeeded in about a second.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hieblmi
hieblmi self-requested a review July 14, 2026 20:30

@hieblmi hieblmi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @DelleonMcglone, thanks for your contribution!

If we upgrade to github.com/lightninglabs/lndclient v0.21.0-2 we get a PayInvoice which already uses SendPaymentV2. So could we upgrade the client instead of calling it manually from aperture?

@DelleonMcglone

Copy link
Copy Markdown
Author

Thanks for taking a look! You're right — and digging into it, it's even simpler than an upgrade: the lndclient that aperture already pins (the replace to the btcd-v2 fork, pinned 2026-06-23) has the same change as v0.21.0-2 — PayInvoice delegates to router.payInvoiceSendPaymentV2. So current master already dispatches L402 payments through the router with no dependency change needed. (Upgrading to the official v0.21.0-2 specifically would be tricky right now, since it's still on btcd v1 modules and would clash with the btcd v2 migration from #244 — presumably that gets resolved whenever upstream lndclient picks up btcd v2 and the replace is dropped.)

Given that, this PR is only worth keeping if you prefer the explicit Router.SendPayment call over relying on the PayInvoice compatibility helper's internals — happy to keep it rebased for that, or just close it as already-addressed. One small point in favor of the current PayInvoice path: the new implementation handles AlreadyExists by switching to TrackPaymentV2, which neatly covers the resume case that motivated this in the first place (the failure I reproduced was on poold 0.7.1, which vendors an older lndclient where PayInvoice was still SendPaymentSync-backed).

Your call — close, or trim to the explicit-call refactor?

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.

2 participants