Repository navigation
Conversation
| return [cid for cid in found if cid.startswith(oracle)] | ||
|
|
||
|
|
||
| def test_lsps2_oracles_honest_run(): |
There was a problem hiding this comment.
What's the rationale for these tests. They seem to be checking the message formats, and it seems to me like a JSON schema che per would be better suited for this. Otherwise it may end up with tests testing the test payloads without running the tests 😅
There was a problem hiding this comment.
Good question, I should have made that clearer! These aren't really format checks. They're unit tests for the oracles in lsps2_oracles.py, and most of those compare data from different places: the invoice amount against the fee computed from the bought params, each HTLC's extra_fee against its onion and forward amounts, the route-hint SCID against the SCIDs issued to this client, the buy time against valid_until, and so on. As far as I can tell, a schema can't express those. The promise check is the only one that's really schema-like.
As for why they get their own tests: in the real client tests (#9658) the client behaves, so every oracle passes. An oracle with a bug that means it can never fire would pass just the same, and we'd never notice. So each test here takes an honest run, breaks one thing, and checks that exactly the matching check fires. The honest run itself makes sure there are no false positives.
I've added this to the module docstring in 0c842c3.
|
needs rebase |
eb8df2a to
842cce7
Compare
| return not any(c["htlcs"] for c in channels) | ||
|
|
||
| try: | ||
| wait_for(settled) |
There was a problem hiding this comment.
if a test expects HTLCs to stay held, this waits the full TIMEOUT before reporting. should waiving robustness.hanging skip the wait?
There was a problem hiding this comment.
When a test waives robustness.hanging it's saying HTLCs may stay held, so check_oracles now skips the wait then. Fixed in fef4211
| MAX_PROMISE_BYTES = 512 | ||
|
|
||
| # LSPS requests a client may send in one test; a retry loop exceeds it. | ||
| MAX_LSPS_REQUESTS = 20 |
There was a problem hiding this comment.
20 requests is a limit for the whole test. a test with +-7 jit invoices would hit it without any retry loop. should this be per session?
There was a problem hiding this comment.
I'd keep it per test. No test in this stack sends more than one JIT invoice, so the limit is nowhere near. Per session doesn't fit well: list_protocols and get_info come before any session exists, and a client that keeps buying would open a new session each time and so raise its own limit. If a test ever needs many invoices, we can raise the bound then.
| for hop in route: | ||
| session = self.session(jit_channel_scid=hop["short_channel_id"]) | ||
| if session is not None: | ||
| out.append((inv, session)) |
There was a problem hiding this comment.
an invoice with two hops on the same session is added twice, so violations are reported twice. maybe a break after the first match would fix it?
There was a problem hiding this comment.
Right. A break would only cover hops within one route, and the same session can also show up in two route hints, so lsps2_invoices now pairs an invoice with each session only once. Two hops on the JIT SCID are flagged by identity.route_hint as well. Fixed in f20d209.
| only_one(l1.rpc.listpeerchannels()["channels"]) | ||
| paid = only_one(l1.rpc.listinvoices("lsp-jit-channel-0")["invoices"]) | ||
| assert paid["status"] == "paid" | ||
| check_oracles(l1, l2) |
There was a problem hiding this comment.
i think this check_oracles fails on ci (Valgrind 5/12): params.invoice: invoice 'lsp-jit-channel-0' expires after the params' valid_until
but i think the oracle is right. the client computes expiry once at client.rs at line 421, then creates the invoice twice (public and reduced copy). if more than +-1s passes in between, the second invoice outlives valid_until
maybe we could either fix the client, or waive params.invoice here with a todo?? what do you think?
There was a problem hiding this comment.
The oracle is right: the client computes expiry once and uses it for both the public invoice and its reduced copy, so the copy can outlive valid_until by however long the first invoice and the delinvoice took. It's a small fix, so I've added it here as the first commit: the copy's expiry is now computed from valid_until right before it's created.
| e | ||
| for e in self.events | ||
| if e["type"] == "htlc" | ||
| and e["decision"] == "forward" |
There was a problem hiding this comment.
this counts every htlc the mock forwarded, including ones the client later failed. if the client rejects a bad part and the payer retries, both forwards count. then amount.extra_fee and amount.received fire, but the client did the right thing?
maaaaybe the mock can record in_channel/in_htlc_id, so we keep only forwards that are settled in listforwards?
There was a problem hiding this comment.
Yes. The mock now records each forward's incoming in_channel/in_htlc_id, and Observation.from_nodes takes the forward's status from the LSP's listforwards and only settled parts count. There's a unit test for a failed part next to a settled one. Fixed.
|
|
||
| def settled(): | ||
| channels = client.rpc.listpeerchannels()["channels"] | ||
| return not any(c["htlcs"] for c in channels) |
There was a problem hiding this comment.
htlcs is not always in listpeerchannels?? (for example, an OPENINGD entry has none), so this can raise KeyError. maybe we should use c.get("htlcs", [])? and the same stuff with the line 485, and to_us_msat on line 319
There was a problem hiding this comment.
Yes, an OPENINGD entry has no htlcs, so both places use .get now. to_us_msat on line 319 is only read for channels the peer opened, and an OPENINGD entry is always our own open, so it can't get there. Fixed.
1190231 to
a86df62
Compare
For a fixed amount the client creates the invoice twice: the public one and, after deleting it, a copy for the amount minus the opening fee, which the reduced HTLCs pay. Both used an expiry computed before the first call, so the copy expired later than the public invoice by however long creating and deleting that took, which can be past the params' valid_until. Compute the copy's expiry from valid_until when creating it. Changelog-Fixed: lsps-plugin: for a fixed amount, the invoice `lsps-jitchannel` stores for the reduced payment no longer stays valid past the opening fee params' `valid_until`.
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, remembers which payment opened each session and records the incoming HTLC of each part it forwards, so that only the parts the client settled count. 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, unless the test waives robustness.hanging.
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.
a86df62 to
220062b
Compare
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:
tools/lightning-downgradeThis adds
tests/lsps2_oracles.py, a set of checks that each LSPS client test runs at its end, whatever the mock did.check_oracles(client, lsp)reads the client's invoices, channels and log, plus the mock's records, and reports every violation it finds:extra_feeThe checks work on a plain-data snapshot, so
tests/test_lsps2_oracles.pyunit-tests them on hand-written runs. The client itself is not changed yet.