Skip to content

tests: run the LSPS client tests against the mock service - #9658

Open
nepet wants to merge 11 commits into
ElementsProject:masterfrom
nepet:lsps-mock-4-client-tests
Open

nepet wants to merge 11 commits into
ElementsProject:masterfrom
nepet:lsps-mock-4-client-tests

Conversation

@nepet

@nepet nepet commented Oct 23, 2026

Copy link
Copy Markdown
Member

Important

26.09 FREEZE August 5th: Non-bugfix PRs not ready by this date will wait for 26.12.

RC1 is scheduled on August 17th

The final release is scheduled for September 7th.

Checklist

Before submitting the PR, ensure the following tasks are completed. If an item is not applicable to your PR, please mark it as checked:

  • The changelog has been updated in the relevant commit(s) according to the guidelines.
  • Tests have been added or modified to reflect the changes.
  • Documentation has been reviewed and updated as needed.
  • Related issues have been listed and linked, including any that this PR closes.
  • Important All PRs must consider how to reverse any persistent changes for tools/lightning-downgrade

Builds on 9657, only the last 3 commits are new.

  • Each client test that used the in-tree LSPS2 service now runs against both it and the mock (parametrized in-tree / mock). The in-tree variant is unchanged. With the mock, the JIT channel tests also run the invariant checks.
  • The MPP test never used the in-tree service for its HTLCs, an older Python mock took over, because the service does not support MPP on main. It now runs against the new mock, and the older mock is removed.
  • The helpers for mock tests move to tests/lsps2_mock_helpers.py so that other test files can use them.

Removing the in-tree service later only means dropping its parameter.

Changelog-None

nepet added 11 commits October 22, 2026 17:42
The mock LSPS2 service needs the opening fee to check `lsps2.buy`
requests and to deduct it when forwarding. Write it from the bLIP-52
text, with every operation checked against the unsigned 64-bit range as
the spec requires, instead of reusing the client's code: a bug shared by
client and mock would go unnoticed by the tests.

The spec's pseudocode divides by 10000000 and its prose mentions
`+ 9999999`; both contradict the definition of `proportional` in parts
per million and the spec's own C and Rust versions, which this follows.
Until now the mock deducted a fixed `htlc.fee_msat` (default 0) and
did none of the fee checks bLIP-52 asks of an LSP. Compute the fee
instead:

- `lsps2.buy` with a `payment_size_msat` fails with 202 if the fee is
  not below the payment size and with 203 if computing it overflows.
- In no-MPP+var-invoice mode the fee comes from the first HTLC, which
  is failed with unknown_next_peer if it is outside the payment size
  limits, if the fee overflows or if nothing would be left to forward.
- The honest deduction is the computed fee; `htlc.fee_msat` still
  overrides it.

Failed HTLCs now record why they were failed.
bLIP-52 lets the LSP open the JIT channel only while the current time
is before the params' valid_until. The mock checked valid_until at
lsps2.buy only, so it still opened channels for expired params. Fail
parts that arrive once the params expired with unknown_next_peer.

htlc.clock_offset_seconds moves the clock of that check, so a test can
expire a session without waiting.
Held parts were failed only when another part arrived after valid_until.
bLIP-52 asks for a separate timeout at valid_until for all pending
parts, so arm a timer when a session starts holding parts. Moving the
mock's clock past valid_until fails them as well, so tests do not wait
in real time.
Every LSPS2 client test should end by checking the same invariants on
what the client did, whatever the mock service misbehaved in. Add
tests/lsps2_oracles.py with the first two:

- Amount: a paid invoice received at least its amount minus the
  opening fee it owes (none for payments after the session), and the
  client holds no more in channels its peers opened than its paid
  invoices received.
- Identity: invoices route only through known channels or through
  the LSP on a jit_channel_scid it issued to this client, and the only
  channel used before its funding confirms is the session's.

The checks work on an Observation snapshot of plain data, gathered
from the client and the mock's state, so they are unit tested on hand-
written runs. For that the mock now timestamps its events, reports the
params it issued and remembers which payment opened each session.

The honest JIT tests now run the oracles.
Expired, contradictory or modified params must never lead to a
payment. Two checks:

- Every lsps2.buy the client sent uses params exactly as the LSP issued
  them, before valid_until, with usable payment size limits, a payment
  size the fee does not eat up, and a promise of at most 512 bytes of
  printable ASCII without '"' or '\'.
- Every LSPS2 invoice follows the buy result the client was sent: the
  lsp_cltv_expiry_delta in a route hint without fees, an expiry no
  later than valid_until and the payment size as amount.
The last oracle for the LSPS2 client tests: the LSPS client plugin is
still running and never crashed, no HTLC is left pending in the
client's channels, and the client sent no more than 20 LSPS requests.
check_oracles first waits for the client's HTLCs to settle, so that a
check does not race a payment still in flight.
A client that accepts a part whose extra_fee does not match what was
deducted from it breaks bLIP-52 even when the total deducted happens
to be the right fee, and the amount oracle did not see that. Check
that each part of a paid invoice arrived short by exactly its
extra_fee and that the extra_fees add up to at most what the payment
owes.
The LSPS2 service is to leave Core Lightning, so the client tests need
a counterpart that does not depend on it. Run each client test that
uses the in-tree service against both it and the mock service, which
serves the same opening fee menu as the policy plugin. Nothing in the
tests changes for the in-tree service; dropping it later only drops
its parameter.

With the mock, the JIT channel tests also check the oracles. The
non-approved zero-conf test makes its invoice itself, so the checks on
how the client makes invoices do not apply there.
The MPP+fixed-invoice test ran the in-tree service with an earlier
Python mock taking over its HTLC handling, because the service does not
support MPP. Run it against the mock service instead, which collects
the parts, opens the channel and spreads the opening fee over the parts
itself, and remove the earlier mock.

The fee stays at 1000000 msat, now computed from a proportional rate,
and the test gains the checks of the mock's own MPP test, which it
replaces: how the fee is spread over the parts, the invoice status and
the oracles.
The tests of the LSPS2 client against a misbehaving LSP go into their
own file and need the same helpers to set up nodes and talk to the
mock service. Move them out of test_cln_lsps.py, unchanged.
@nepet nepet added this to the v26.12 milestone Oct 23, 2026
@nepet
nepet requested review from cdecker and daywalker90 October 23, 2026 12:24

This branch has not been deployed

No deployments
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.

1 participant