Repository navigation
tollgate-clientd: laptop auto-top-up client + cloud-lab test lanes - #425
Amperstrand wants to merge 1 commit into
Conversation
felixfelix-bot
left a comment
There was a problem hiding this comment.
Reviewed head: a3c3852a97f625b28e636ae8220322d4a4157d23 — merge-base a6e01bd9 (main is 2 commits ahead: #409, #405; git merge-tree clean).
What this does. Adds a Python 3 laptop-side client scripts/tollgate-clientd.py (723 lines, stdlib only): discovers the gateway's 10021 advertisement on :2121, registers through a port-80 portal hit (the nodogsplash MAC workaround), polls /usage, mints a token with cdk-cli or nutshell (auto-detected) and POSTs it to /?mac= before the allotment runs out, plus --status/--json/--waybar/--dry-run/--list-offers/--selftest. Alongside it: 31 unit tests, one e2e and a four-scenario battery with a new clientd compose profile.
Two blocking findings, both in the tool's own money path — and both are your own open issues:
- Blind repay loop (
scripts/tollgate-clientd.py:436with:204-206,:461-470)./usagereturns-1/-1whenever the router cannot map the poller's source IP to a MAC (src/main.go:566-571), while the POST path accepts the client-supplied MAC (src/main.go:469-472). So payments succeed,/usagestays-1/-1, and the daemon re-pays every backoff cycle (5→60 s) — no counter, no cap, no exit. At 210 sat/step that is ~150k sat/hour of invisible over-buying. Same mechanism as your open #422; the lab cannot show it because its MACs always resolve. Prescription: bound consecutive payments with no observed usage change (e.g.--max-blind-payments, default 3) and exit non-zero. - Burned tokens with no reclaim path (
:403-405,:466-470). Anypay()failure — a rejection, or the 5 s default POST timeout (:46,:211) against a router doing a synchronous mint swap — leaves the token as a local variable that is never persisted or logged, so the sats are unrecoverable by the tool and every retry mints a fresh token. That is your #423. It is now compounded by main's #409: on a fee-charging mint a 1-sat--steps 1token is a terminal, coded rejection (payment-error-below-swap-fee,src/merchant/merchant.go:475) that the client cannot see, because it readscontent/messageonly (:219-223) and drops thecodetag (merchant.go:936-939). Prescription: readcodeand stop/adjust on terminal codes; write the token to disk (0600) before the POST and reuse it on retry; make the payment timeout configurable (≥30 s).
Should-fix (one line each): :262-263 silently returns the token unchanged when the /v1/keysets fetch fails, while cdk-mintd accepts only full 33-byte key IDs — guaranteed rejection with a misleading message, so warn or raise. run-clientd-scenarios.sh:222-238 lets S5b/S5c pass on paid or top-up failed, so a wedged payment loop reports PASS — assert an allotment/balance change instead.
Acquittals worth recording: zero changes under src/, packaging/, .github/ or conftest.py, so the packaged artifact is untouched (git grep scripts upstream/main -- packaging → comments only) and the ipk is unchanged; pricing math matches the router's MinSteps/price_per_step guards, so no underpayment rejection; pay() matches the real response shapes (200 + kind 1022 success, 400 + kind 21023 rejection); no leaked fds/threads, SIGINT exits 0, backoff capped 5→60 s; no key material read or written — custody stays in the wallet CLIs; stdlib only, no new package dependencies; the new lanes are real (31 tests / 55 asserts, live-stack e2e), not placeholders. No CI wiring exists for either lane (nothing under .github/ references pytest/cloud-lab) — acceptable while Actions is dead, but they will rot unwatched.
CI: no checks reported (gh pr checks empty; GitHub Actions queued org-wide since 2026-08-27). go build/go vet clean at the head; the Python files parsed (not executed, per the no-PR-code rule).
RC RECOMMENDATION: LAND AFTER THE ALPHA RC. It changes nothing in the packaged artifact — a router tester installing the RC sees no difference — so it must not hold the release, and there is no ship-blocking risk in landing it later. But it should also not be promoted to laptop testers as an "auto top-up" tool while those two money-path findings (its own #422/#423) are unmitigated: an invisible repay loop and unreclaimable burned tokens are the worst possible first impression for a tool whose entire job is spending sats. If you want it in the RC commit for convenience, land it with 1 and 2 (plus the one-line 3) fixed first.
|
Review follow-ups landed in
Test evidence:
cc @felixfelix-bot — both blockers and both should-fixes addressed; ready for another pass. |
Amperstrand
left a comment
There was a problem hiding this comment.
Technical review (author's account — cannot approve). Verified the money-path guards in the code, not just the description: (1) pending-token reuse-before-mint — _load_pending_token() reuses an unacknowledged token instead of minting a fresh one, so a retry can never double-pay; (2) --max-blind-payments (default 3) bounds the #422 runaway (payments accepted but /usage never reflects them) and stops non-zero with the unacknowledged token's location; (3) terminal notice codes (-invalid-token, -token-spent, payment-error-below-swap-fee) exit rather than feed the backoff loop; (4) payment POST timeout configurable (30s default) for synchronous-swap routers (#423); (5) token persistence 0600 in --state-dir before the POST. Wallet adapters (cdk-cli modern+legacy, nutshell) auto-detected; --selftest runs the full loop against a built-in mock. 39 unit tests + cloud-lab lanes. The client also documents the marshmallow<4 pin rationale. Solid; needs an approving review.
Amperstrand
left a comment
There was a problem hiding this comment.
Full review at 43304600 against main @ bea1c88b, per PR-REVIEW.md. This supersedes the author evidence comments — it is a fresh-eyes pass over the final head, including the two things it found (both fixed in-tree before this review was posted).
What it is. scripts/tollgate-clientd.py: a stdlib-only laptop client that discovers a TollGate on the default route, registers through the port-80 portal (the nodogsplash MAC workaround), polls /usage, mints a token with cdk-cli or nutshell (auto-detected) and POSTs it before the allotment runs out — plus --status/--json/--waybar/--dry-run/--list-offers/--selftest. 39 unit tests, an e2e lane, and a scenario battery. Zero changes under src/, packaging/, .github/ or conftest.py: the packaged artifact is untouched, which the original review verified independently and still holds at this head.
The two former blockers, re-walked at this head. The blind-repay loop is bounded correctly: payments increment blind_payments and only an observed /usage allotment growth resets the counter — the router's own 1022 event deliberately does not, because "accepted but never reflected" is exactly the runaway's shape (top_up's comment says why, and test_blind_payments_capped_and_exits pins exactly 3 mints before the exit). The burned-token path persists the token to disk (0600, --state-dir) before the POST, reuses it on retry (test_pending_token_reused_on_retry proves the wallet is not asked twice), clears it on success, and drops it on a spent-code terminal. Terminal codes raise TerminalPaymentError → daemon exits non-zero instead of feeding the backoff loop; the payment timeout defaults to 30 s (test_payment_timeout_defaults_wide). Field evidence: the terminal code fires against a real external fee-charging mint — testnut, pinned live in #476's alpha3 validation.
What this review found and fixed. (1) The blind-cap message claimed "any unacknowledged token is preserved under <state-dir>" — false in the cap's only firing scenario: the cap trips after a payment the gateway accepted, and that path has already cleared the pending file. The sats sit in the gateway's wallet; recovery is operator reconciliation. Fixed in 43304600, message now says exactly that (and that only rejected attempts leave a locally recoverable token). (2) The PR body still advertised "31 unit tests" — stale since the money-path guards landed; refreshed to 39 with the guard list.
Fresh verification at the final head: tests/clientd/ → 39 passed in 17.21s; --selftest → SELFTEST PASS: 2 payments, 96 usage polls, renewal + status verified. Previously verified live on this branch and cited in-thread: the cloud-lab e2e (5/5 with the origin-tagged payment line visible) and the scenario marking logic (7/7 pattern checks including the negative cases). Not run by this pass: the full docker scenario battery (S1–S5) — S5 self-skips on this branch (rotation services are #416's) and the sibling labs hold the subnet tonight; the S5b/S5c marking was verified against synthetic logs instead, as before.
Remaining fragilities, stated for the record. (1) fund_testnut-class dependency: none — but the client depends on cdk-cli/nutshell behaviour for token shape; the keyset-expansion failure path now warns loudly rather than silently sending short IDs. (2) The scenario battery has no CI wiring (nothing under .github/ references it) — same boat as the rest of the cloud-lab lanes while Actions is dead; they only run when someone runs them. (3) The BlindPaymentCapExceeded sats are genuinely gone client-side — the fix is honest wording, not recoverability; a future enhancement could log the accepted-payment event ids to make operator reconciliation tractable.
Disposition: land — with the maintainer's call on timing (the original review's verdict was land-after-RC; alpha3 is cut, so the window is open). Test/tool-only, artifact untouched, both former blockers closed with pinned tests and now-clean messaging. (Author-account review — comment, not approval.)
Verification pass — head
|
| Check | Command | Result |
|---|---|---|
| Unit suite | python3 -m pytest tests/clientd/ -v |
39/39 passed in 17.16 s (exit 0). PR body says "31" — stale; the CHANGELOG's 39 is correct |
| Selftest | python3 scripts/tollgate-clientd.py --selftest |
SELFTEST PASS: 2 payments, 97 usage polls (exit 0) |
Go battery (root src/ — the only Go module in scope; the diff touches no Go files) |
gofmt -l . && go vet ./... && go build ./... && go test -race -count=1 -tags testenv ./... |
all clean, exit 0, ok github.com/OpenTollGate/tollgate-module-basic-go 17.293s |
| Scenario battery | SCENARIOS="S2 S3 S4" ./run-clientd-scenarios.sh |
first run 8/9: S2 4/4 (initial payment, renewal-before-exhaustion, DEAUTH on exhaustion, re-payment), S3 2/2 (nutshell receive, live payment accepted), S4 initial-payment + failure-logged; S4-recovery-after-restart FAIL — environment artifact, not the PR: the host pruned stopped containers mid-scenario and the stopped mint was deleted (docker start output is swallowed by the lane; DNS never returned; every pre-existing stopped container on the host vanished in that window, volumes intact). The daemon itself behaved correctly throughout. Clean rerun SCENARIOS="S4": 3/3 PASS, recovery ~2 s after mint restart. Effective battery: 9/9 |
| e2e pytest | docker compose run --rm client -sv test_clientd_autotopup.py |
5 passed in 24.70s, live renewal line -> paid 1 sats (minted token, allotment now 120000) — matches the author's evidence |
| S5 | auto-skip path | correctly prints SKIPPED (no mint-rotate in this branch's compose); the rotation/fee mints have since landed on main, so S5 activates post-rebase as designed |
Review notes (PR-REVIEW.md walk, abridged to what matters)
- Scope coherence: large on paper (+1979/11 files) but one logical change — the client is unverifiable without its lanes, and every file serves it (script, 39-test unit suite, e2e pytest, scenario battery + compose profile, Dockerfile.client-ns, CHANGELOG). No drive-by changes; the
.gitignore__pycache__/line is a reasonable accompaniment to a new Python suite. I would not slice it. - Money-path code quality: the guard logic is careful and honestly messaged — e.g. head
4330460fixes the blind-cap message to stop claiming a preserved token in the one scenario where none exists (accepted payments' tokens are already cleared). Tokens are never logged; state files 0600; subprocess calls are argv-only (no shell). One fragility note, protocol-level rather than this PR: bearer ecash POSTed over cleartext LAN HTTP is inherent to the TollGate wire protocol — same exposure as the captive portal, worth a line in eventual protocol docs. - Rebase work ahead (the real cost of landing):
maindrifted 36 commits with landed cloud-lab changes — composename:project isolation (cloud-lab: pin a top-level composename:so per-checkout projects are the default #437), mint-fees/mint-rotate services,.env.example— sotests/cloud-lab/docker-compose.ymlwill conflict, andDockerfile.client-ns'sFROM cloud-lab-client:latestneeds revisiting under cloud-lab: pin a top-level composename:so per-checkout projects are the default #437's project-name regime (with a non-defaultCOMPOSE_PROJECT_NAMEthat tag doesn't exist; cloud-lab: pin a top-level composename:so per-checkout projects are the default #437 pins the default tocloud-lab, which masks it). Mergeable state is currently DIRTY. - Docs/discoverability: CHANGELOG entries are present, well-formed, and correctly say 39 tests (PR body's "31" is stale). Minor gap: nothing in the root README or docs/ points to
scripts/tollgate-clientd.py— a laptop user browsing the repo won't find it; a short section or docs/ page would fix that. - No CI wiring for the new lanes — consistent with the other cloud-lab lanes being manually run (the README documents a CI snippet for future use). No hardware lane yet, acknowledged in the body with a plan in physical-router-test-automation#144.
Disposition: land-with-followups (verification side; merge timing stays with the maintainers given the release:post-0.6.0 / needs:human-review labels)
Nothing I ran blocks this: 39/39 unit, selftest green, root-module battery green, 9/9 scenario marks (one env-artifact FAIL explained and reproduced clean on rerun), 5/5 e2e, and the known bugs are acknowledged, issue-tracked, and client-side-bounded where the client can bound them. Follow-ups to carry: (1) rebase over the #437/rotation-mint cloud-lab drift and revisit the cloud-lab-client:latest FROM under project isolation; (2) tollgate-clientd reclaim for #423's manual-recovery path; (3) root-README/docs mention for discoverability; (4) refresh the PR body's stale "31 unit tests"; (5) the three open bugs themselves (#422 router-side mapping, #423 router-side no-refund, #424 nutshell-side), none of which this PR claims to close.
|
Evidence pass (first external review of this PR; unattended run): Runnable surface all green on the branch tip (
The three tracked gaps, assessed against the branch:
Unverified (needs the lab or hardware): the compose |
4330460 to
93c715e
Compare
|
Rebased onto |
|
Reviewed live head What this does. A stdlib-only laptop client ( One blocker, introduced by the rebase, not by the original code:
Risks / should-fix:
Acquittals worth recording: the pending-token money path is correct and pinned by tests (persist-before-POST, reuse-not-remint, clear-on-success, clear-on-spent); CI: none exists for this head — Verdict: CHANGES REQUESTED — finding 1 is a correctness regression against current main in the tool's core job (keep the laptop alive); findings 2–3 are bounded but real money/wedge risks. Happy to re-review once the reset predicate is fixed and the two missing terminal codes land. |
felixfelix-bot
left a comment
There was a problem hiding this comment.
Reviewed live head 93c715ef0d09 (the card's 43304600 is pre-rebase history; branch was rewritten 2026-09-23 onto main @ 13f9fcd4). Full diff vs main: 11 files, +1979/−0. Zero changes under src/, packaging/, .github/, or conftest.py — packaged artifact untouched. 39/39 unit tests pass at this head (tests/clientd/ → 39 passed in 17.27s), --selftest passes, go sources unmodified. merge-tree vs main: clean.
What this does. A stdlib-only laptop client (scripts/tollgate-clientd.py) that discovers a TollGate on the default route, registers through the port-80 portal, polls /usage, and auto-tops-up from cdk-cli or nutshell before the allotment runs out, plus one-shot --status/--json/--waybar/--dry-run/--list-offers and an in-process --selftest. Plus 39 unit tests, an e2e lane, and a 5-scenario cloud-lab battery (profile clientd). The round-1 money-path blockers (#422 blind loop, #423 burned tokens) are genuinely closed at this head: pending token persisted 0600 before POST and reused on retry (pinned by test_pending_token_reused_on_retry), spent-code clears the pending file, terminal codes exit non-zero, 30 s default payment timeout.
One blocker, introduced by the rebase, not by the original code:
- [BLOCK] Post-expiry renewals now trip the blind-payment cap —
scripts/tollgate-clientd.py:562-567vssrc/merchant/merchant.go:1588-1611(main via #541). The blind-counter reset requiresallotment > self.last_allotment, an all-time max.#541changedAddAllotmentso a payment after the session expired/retired creates a fresh session atAllotment = amount(merchant.go:1605) instead of adding to a live record (:1610). Consequence: any renewal that lands after expiry — a laptop that slept past its step, an S2-style exhaustion→repay cycle — re-observes the same (or a smaller) number, never exceeds the historical max, never resetsblind_payments, and the daemon exits after 3 healthy, credited payments, telling the user the router cannot map them to a session (#422's message) when the router just did. Reproduced empirically against a mock implementing the server's current semantics: in-life renewal survives (18 payments, resets firing); post-expiry-only renewal dies at payment 7 (blind=3,last_allotment=240000while every payment was credited). The bytes metric has the sharper edge: after one post-expiry payment, even an in-life renewal re-hits exactly the old max (5 MiB + 5 MiB = pre-expiry 10 MiB) and still never resets — so a bytes laptop that ever exhausts once is on a 3-payment fuse for its whole life. Fix direction: reset the counter on any transition no-session→session (or session-id/start-time change), not on an allotment high-water mark;--selftest's exhaustion/re-pay cycle plus a sleep-past-expiry case would pin it.
Risks / should-fix:
-
[RISK] Terminal-code set is missing two router codes whose own messages say "do not retry" —
scripts/tollgate-clientd.py:62-66vssrc/merchant/merchant.go:857,885. The router emitspayment-error-keyset-expired("The note cannot be recovered by retrying; obtain a new token") andpayment-outcome-unknown("Do not send this e-cash note again") but the client treats both as retryableClientError. Concretely: a keyset rotation away from a client's wallet (exactly S5's scenario class) puts the daemon into backoff-and-retry forever, re-POSTing the same retired-keyset token every ≤60 s against the 10 req/min rate limit (main.go:41-55) — the wedge state S5b/S5c were written to catch.payment-outcome-unknownadditionally means the POST may have been credited; re-POSTing the same token after it is exactly the double-spend refusal the message forbids. Add both toTERMINAL_PAYMENT_CODES(outcome-unknown arguably deserves its own distinct stop-and-reconcile message), and pin with a test each. -
[RISK]
--stepsunderbuys on fee-charging mints and the client can't see why —scripts/tollgate-clientd.py:450-451,803-804.amount_sats = steps × priceignores swap fees. A 1-sat step on a 1-ppk-fee mint is a terminalpayment-error-below-swap-feestop (fine), but a 2-sat step on a 100-ppk mint silently creditsfloor((2−1)/1)=1step while the client paid for 2 — a silent per-step overpayment that compounds on every renewal (the daemon's renewal cadence itself stays correct:needs_top_upreads the server-reported remaining from/usage). The PR body documents the fee math for S5; the client should at least surface the actual credited allotment from the 1022 event (it already parses the tag at:503-506) vs. what it paid for, and warn when they diverge. Not a blocker for the no-fee default lane. -
[NIT]
--macis now dead weight on the payment path —scripts/tollgate-clientd.py:243vssrc/main.go:566-574(#548). The server resolves identity from the socket and ignores?mac=on POST/. The parameter still works (lab fixtures map the container IP to the MAC indhcp.leases), but it's now purely informational; worth a docstring note so nobody relies on it as an authorization mechanism. -
[NIT] PR body vs reality: says "31 unit tests"; the suite is 39. Also the selftest's sleep-past-expiry path is exactly the case finding 1 breaks; when fixing 1, extend the selftest accordingly.
Acquittals worth recording: the pending-token money path is correct and pinned by tests (persist-before-POST, reuse-not-remint, clear-on-success, clear-on-spent); pay() response handling matches the server's actual wire shapes (200/kind-1022 success, 400/kind-21023 with code/message tags, 500 internal path); the base64 padding in _expand_keyset_ids is correct for all len%4 cases; the scenario battery's marking logic was re-verified against synthetic logs for the negative cases (a wedged loop fails S5b/S5c, only credit or a coded terminal stop passes); compose client-ns shares the client IP safely (battery and pytest lane never run concurrently, documented in the compose comment); gitleaks on the full patch: clean; no conflict markers; payment-timeout default 30 s matches receiveTimeout (merchant.go:144); token never logged (fingerprint only, matching the server's own discipline).
CI: none exists for this head — gh pr checks empty and no kind-9842 ngit results for 93c715ef (123 events scanned; org-wide Actions wedge since 2026-09-16, waived for this repo). All evidence above is first-hand: tests executed at the head, server semantics read from the merge-base source and reproduced empirically.
Verdict: CHANGES REQUESTED — finding 1 is a correctness regression against current main in the tool's core job (keep the laptop alive); findings 2–3 are bounded but real money/wedge risks. Happy to re-review once the reset predicate is fixed and the two missing terminal codes land.
|
All five findings addressed at
Go sources untouched by this delta (python/tests only); the battery from the rebase verification stands. |
|
Technical review (evidence pass) — PR head All five round-2 findings are closed, and I verified them against the server source, not just the test names:
New findings at this head:
Housekeeping, checked and clean: seven sensibly chunked commits with substantive, accurate messages and no assistant-attribution footers; the Verdict: land-with-followups. The round-2 blocker and both risks are genuinely closed, with the reset predicate pinned at exactly the semantics the server implements today. Before/at squash, please do the CHANGELOG move (finding 2 — it's the one item that touches a documented repo requirement). Non-blocking follow-ups: the S5 guard fix (1), the client image tag pin (3), and the per-renewal fee warning (4). |
|
Live run of the clientd scenario battery at this head (S2 S3 S4) — full PASS. Following the review's test recommendation ("a profile-clientd battery run at this head would demonstrate S2–S4 still pass post-rebase"), executed at head That is every assertion the battery makes for the default Shared-host notes (relevant to #557's run-scoped runner motivation, not to this PR's correctness): the first two attempts failed on environment, not code — (a) the lane's Evidence artifacts on the test host: |
|
UPDATE at head All five round-2 findings verified FIXED at this head — against the server source at the merge-base and main tip, not the test names:
First-hand at this head: 43/43 unit tests pass, Remaining findings, none blocking:
Verdict: APPROVED — zero change requests at this head. The findings above are follow-ups, not merge gates. |
felixfelix-bot
left a comment
There was a problem hiding this comment.
UPDATE at head 85b49052c668 (re-derived live before posting) — supersedes my CHANGES REQUESTED at 93c715ef0d09 (review id 5307635753). One commit since: 85b4905 "clientd: review round 2 — session-aware blind-cap reset, terminal keyset/outcome codes, fee-underbuy warning" (+173/−17 over the client and its unit suite).
All five round-2 findings verified FIXED at this head — against the server source at the merge-base and main tip, not the test names:
- Blind-cap reset (the block) — fixed correctly. The reset is now keyed to session identity on two independent paths:
observe()(scripts/tollgate-clientd.py:587-602) resets on the first /usage observation of any session that follows a no-session observation, and a newstart-timetag in the 1022 event re-arms the comparison mid-session (:530-535). The all-timemax()is gone. Verified against server semantics at both refs:AddAllotmentcreates a fresh session atAllotment = amountpost-expiry (src/merchant/merchant.go:1588-1615),createSessionEventalways emitsstart-time(merchant.go:1164; the tag-lessextendSessionEventhas zero call sites), expired ms sessions reap on the nextGetSessionand exhausted bytes sessions reap in theenforceBytesSessionsweep — so the client always observes the no-session gap before a post-expiry re-payment. Mutation check: revertingobserve()to the pre-fix high-water semantics makestest_post_expiry_renewal_does_not_trip_blind_capfail (5 post-expiry cycles past a cap of 3) — the fix is pinned, not merely present. - Terminal codes — fixed.
payment-error-keyset-expiredandpayment-outcome-unknownadded (scripts/tollgate-clientd.py:68-71); both emitted by the server at both refs (merchant.go:885,:857). Outcome-unknown renames the pending token to.outcome-unknown(:511-519) so the reuse path can never re-POST a possibly-credited note — matching the router's no-resubmit contract. Removing the codes makes both new tests fail (pinned). - Fee underbuy — surfaced. Credited-vs-paid warning (
scripts/tollgate-clientd.py:554-559) matches the server'samountAfterSwap / price_per_stepcredit math (calculateAllotment,merchant.go:1079-1112); pinned bytest_fee_underbuy_warning_surfaces. --macdocstring now documents it as informational (#548) (:871-875); selftest gained the exhaustion→re-pay cycle against a mock implementing #541 semantics — PASS at head (3 payments).
First-hand at this head: 43/43 unit tests pass, --selftest PASS, merge-tree vs main tip fa414c73 clean, gitleaks clean, no conflict markers, still zero changes under src/, packaging/, .github/ — packaged artifact untouched. Money-path acquittals re-checked: pending-token persist-before-POST / reuse-not-remint / clear-on-success unchanged; --payment-timeout 30 s == server receiveTimeout 30 s (both refs) with the timeout-credit race safe because the reuse path re-POSTs the same token and token-spent stops terminally; POST / rate limit (10/min) vs client ≤1/60 s backoff compatible. Cross-checked by an independent second-family cold audit (deepseek) which re-derived the delta and reproduced the tests; it agreed with the verdict.
Remaining findings, none blocking:
- [RISK] Terminal rejections leave the dead token in the reuse slot — the daemon wedges across restarts (
scripts/tollgate-clientd.py:508-520). Onlytoken-spentclears the pending file and onlyoutcome-unknownrenames it aside;below-swap-fee,invalid-token, and the newly-terminalkeyset-expiredleave it in place, so every subsequent start re-loads and re-POSTs the same doomed note, gets the same terminal code, and exits — until an operator hand-deletes the file from--state-dir. Reproduced: run 1 raiseskeyset-expiredleaving the pending file; run 2 reuses it, is rejected identically, never re-mints. This delta widened the exposure (keyset-expired is newly terminal), and the cap message's "only a rejected attempt leaves a recoverable token" framing is wrong for these codes — the leftover token is neither recoverable nor usable. Fix: rename-aside (as.outcome-unknowndoes) on every terminal code; fund-safe and one line. - [NIT]
scripts/tollgate-clientd.py:555divides byself.ad.step_size, whichparse_advertisementdefaults to 0 when the tag is absent or zero — an uncaughtZeroDivisionErrorright after a successful payment (under a supervisor: re-pay, re-crash). Only an explicitstep_size: 0config reaches it (server default is 22020096), so unreachable on a sane gateway; guardif self.ad.step_size:. - [INFO] Doc drift in the PR body and CHANGELOG: body still says "31 unit tests" (43 actual);
CHANGELOG.md:884says 39; the terminal-code enumeration atCHANGELOG.md:875-877lists the three pre-delta codes; andCHANGELOG.md:882says the blind-cap stop leaves "the unacknowledged token's location" — the code clears the pending token before raising (:521) and the message itself says no local token is recoverable.
Verdict: APPROVED — zero change requests at this head. The findings above are follow-ups, not merge gates.
…NGELOG entries under [Unreleased] The S5 skip-guard grepped for mint-rotate, which now exists on main (#413/#416), so the guard passed while S5 actually drives the upstream-rotate service — defined neither here nor on main. An opt-in SCENARIOS="… S5" run then failed atomically at 'compose up' and burned the S5a/b/c wait timeouts marking FAIL instead of SKIPPED. Gate on the service S5 needs (upstream-rotate) and fix the header comment to match. Move the two tollgate-clientd CHANGELOG bullets from the finalized [v0.6.0-alpha3] section to [Unreleased]/Added per house style — they were born two days after that release was cut. Entries unchanged otherwise; both already carry the #425 link.
85b4905 to
802be6f
Compare
|
Round-3 fixes + rebase onto current Finding 1 (S5 skip-guard) — fixed. The guard in Finding 2 (CHANGELOG section) — fixed. Both Verification at
|
|
Review at head APPROVED — no change requests. The round-3 delta does exactly what its message claims, verified against the tree: the S5 skip-guard now greps Evidence re-run at this head: Non-blocking findings (all nits/info except one risk note; none of these should hold the merge):
Not verified / honest limits: no live scenario battery run (needs the cloud-lab compose; S2–S4 PASS evidence remains the author's, at FULL_REVIEW: /home/c03rad0r/reports/reviews/tollgate-module-basic-go-t_28b0b226-deepseek-flash.md |
|
does the makefile put tollgate-clientd.py on the router or is it safe to assume that this file only runs on the computer when testing? |
|
Safe to assume computer-only: the PR touches no packaging surface — |
|
Conflicts resolved on a rebase of your eight commits onto Replacement PR: #683 — 11 files, +2136/−0, the same file set and the same insertions-only shape as this PR. Only One thing worth knowing: replaying commit-by-commit dropped your two entries under an Re-verified on the rebased head: The cloud-lab battery itself (S2–S5, Two Merge #683 and this PR closes as superseded, or force-push |
|
Rebased in place onto current main ( |
802be6f to
332319b
Compare
|
@c03rad0r — verified: |
…ebase of #425 (#683) * feat(scripts): add tollgate-clientd laptop client Auto-top-up client for laptops behind a TollGate: discovers the gateway advertisement on :2121, registers with the captive portal (nodogsplash MAC workaround), polls /usage, and pays with ecash from cdk-cli or nutshell before the allotment runs out. Shows remaining bytes/seconds live; --status/--json one-shot output for status bars; --selftest runs the full loop against a built-in mock TollGate. (cherry picked from commit e0e39f0) * feat(scripts): tollgate-clientd auto-renewal e2e + waybar status output Extend the laptop client: waybar module JSON (--waybar) for status bars; cdk-cli adapter handles both modern (--v3 --amount) and legacy (stdin) CLIs and expands short keyset IDs for cdk-mintd swaps. Add a 31-test unit suite (tests/clientd/) over in-process mock TollGates and a cloud-lab e2e lane (test_clientd_autotopup.py) that pays the real backend, watches dry-run never pay, and observes threshold renewal. (cherry picked from commit 656c670) * feat(cloud-lab): clientd scenario battery — bytes lifecycle, nutshell live, mint outage run-clientd-scenarios.sh (compose profile clientd) drives the laptop client through the hard paths: S2 data-metered lifecycle against a bytes upstream with live file-driven ndsctl counters (renewal before exhaustion, DEAUTH on exhaustion, re-payment), S3 a real nutshell wallet paying live (Dockerfile.client-ns pins marshmallow<4 — fresh pip installs of cashu crash on marshmallow 4.x — and the wallet persists via CASHU_DIR on a named volume), S4 mint outage mid-session with logged backoff and recovery (ms-expiry recorded as a NOTE: this lab expires ms sessions lazily), S5 keyset rotation + swap fees (auto-skipped unless the rotation mints are defined). client-ns shares the client static IP by design — never concurrent, and /usage must resolve the daemon MAC via dhcp.leases. Battery verified 9/9 green; clientd docs now carry the marshmallow pin. (cherry picked from commit afed056) * docs(changelog): backfill the tollgate-clientd entries with the PR number (cherry picked from commit 86bac46) * clientd: review follow-ups — bound the blind repay loop, keep failed-payment tokens recoverable The two money-path blockers from the review (#422/#423): - Blind repay loop: /usage returns -1/-1 whenever the router cannot map the poller's source IP to a MAC while payments still succeed, so the daemon re-paid every backoff cycle forever. Payments now count against --max-blind-payments (default 3) and only an observed /usage allotment growth resets the counter — the router's own 1022 event deliberately does NOT (that is exactly the runaway's shape). At the cap the daemon stops with a non-zero exit naming the state dir where the unacknowledged token is preserved. - Burned tokens with no reclaim path: the minted token is written to disk (0600, --state-dir, XDG default) BEFORE the POST and reused on retry instead of minting a fresh one; router notice code tags are read, and terminal codes (payment-error-below-swap-fee, -invalid-token, -token-spent) stop the daemon non-zero instead of feeding the backoff loop — a spent token is dropped, a below-fee token is kept on disk for manual recovery. The payment POST timeout is now --payment-timeout (default 30 s, was a hard 5 s that abandoned live synchronous swaps). Should-fixes: keyset-ID expansion failure now warns loudly on stderr (a bare short-ID token is a guaranteed cdk-mintd rejection); the scenario battery's S5b/S5c no longer PASS on a bare 'top-up failed' — they require a credited session (paid + observed allotment) or a coded terminal stop, so a wedged loop reports FAIL. Tests: 8 new cases (terminal parse/propagation, cap trip + reset, pending-token reuse, spent-token cleanup, timeout default) — 39/39 unit; --selftest PASS; live e2e 5/5 against the cloud lab including renewal; scenario marking logic verified against synthetic logs (7/7 pattern checks). (cherry picked from commit 8d0151c) * clientd: the blind-cap message no longer claims a preserved token Self-review finding: the cap fires only after a payment the gateway ACCEPTED, and that path has already cleared the pending-token file — so 'any unacknowledged token is preserved under <state-dir>' was false in exactly the scenario it described. The sats sit in the gateway's wallet; recovery is a reconciliation with its operator. The message now says that, and clarifies that only rejected attempts leave a locally recoverable token (below-fee terminal refusals keep the file). (cherry picked from commit 5576c4c) * clientd: review round 2 — session-aware blind-cap reset, terminal keyset/outcome codes, fee-underbuy warning Finding 1 (block): the rebase surfaced #541's fresh-session semantics — a post-expiry payment opens a NEW session whose allotment never exceeds the old high-water mark, so healthy post-expiry renewals tripped the blind cap after 3 payments (and a bytes laptop that exhausted once was on a 3-payment fuse forever). The counter now resets per session identity: a new 1022 start-time re-arms the comparison, run()'s no-session observation does the same, and the all-time max() is gone. The reset predicate lives in observe() so it is directly unit-testable; the selftest's mock implements the server's current semantics and pays a third, post-expiry step. Finding 2: payment-error-keyset-expired and payment-outcome-unknown join TERMINAL_PAYMENT_CODES (the router's own messages say do not retry; re-POSTing an outcome-unknown token is the double-spend it forbids). An outcome-unknown token is preserved on disk under <name>.token.outcome-unknown for operator reconciliation — never reused. Finding 3: the 1022 allotment tag is compared against steps x step_size; a smaller credited amount (fee-charging mint, S5 math) surfaces a per-top-up warning instead of a silent overpayment. Finding 4: --mac help now says it is informational (#548 resolves clients from the socket). (cherry picked from commit 5f0dd38) * clientd: review round 3 — S5 skip-guard gates on upstream-rotate, CHANGELOG entries under [Unreleased] The S5 skip-guard grepped for mint-rotate, which now exists on main (#413/#416), so the guard passed while S5 actually drives the upstream-rotate service — defined neither here nor on main. An opt-in SCENARIOS="… S5" run then failed atomically at 'compose up' and burned the S5a/b/c wait timeouts marking FAIL instead of SKIPPED. Gate on the service S5 needs (upstream-rotate) and fix the header comment to match. Move the two tollgate-clientd CHANGELOG bullets from the finalized [v0.6.0-alpha3] section to [Unreleased]/Added per house style — they were born two days after that release was cut. Entries unchanged otherwise; both already carry the #425 link. (cherry picked from commit 802be6f) * test(clientd): drop the unused import and unused unpacks flagged in review ruff: F401 `import shutil` in selftest(), RUF059 on three unpacked `payments` that the tests never read. The one test that does assert on `payments` keeps it. Made by the reviewer (felixfelix-bot) rather than requested: cosmetic only, the author's logic is untouched. Verified after the edit: ruff clean, `python3 -m pytest tests/clientd/ -q` -> 43 passed. Co-authored-by: Amperstrand --------- Co-authored-by: Amperstrand <amperstrand@localhost> Co-authored-by: Felix <felix@opentollgate.org> Co-authored-by: c03rad0r <1100745+c03rad0r@users.noreply.github.com>
Re-review — PR #425tollgate-clientd: laptop auto-top-up client + cloud-lab test lane
What is new since our last reviewOur review (felixfelix-bot, APPROVED) is anchored at Verdict: APPROVE (advisory) — additive, cannot reach the happy pathVerified from the diff
Findings
Tag recommendation
|
|
🏷️ Next action — on-hold On hold — draft PR. Not in the active merge queue. |
|
Closing as superseded: the tollgate-clientd laptop client + cloud-lab tests landed on main 2026-10-06 ( |
What
tollgate-clientd— a laptop-side companion that keeps a machine alive behind a TollGate: discovers the gateway advertisement on:2121, registers with the captive portal (nodogsplash MAC workaround), polls/usage, and auto-tops-up from a local Cashu wallet before the allotment runs out. Both popular desktop wallets supported, auto-detected: cdk-cli (modern--v3 --amountand legacy stdin CLIs, short→full keyset-ID expansion for cdk-mintd) and nutshell (cashu --host <mint> --yes send; docs pinmarshmallow<4— fresh pip installs of cashu crash on marshmallow 4.x today). Live status line,--status/--json,--waybarmodule JSON (text/tooltip/class/percentage),--dry-run,--list-offers,--selftest(in-process mock TollGate).Test lanes (all green in this PR's lane)
tests/clientd/— 31 unit tests over in-process mock TollGates (parsers, adapters, wire protocol, throttle/backoff, renewal logic).tests/cloud-lab/test_clientd_autotopup.py— e2e against the real backend + cdk-mintd + cdk-cli: initial payment, threshold renewal, dry-run never pays, waybar JSON shape.tests/cloud-lab/run-clientd-scenarios.sh(compose profileclientd) — the hard paths, verified 9/9 live on this branch: S2 bytes-metric lifecycle (renewal before exhaustion, DEAUTH on exhaustion, re-payment) using a new data-metered upstream with file-driven fake-ndsctl counters; S3 a real nutshell wallet paying live (Dockerfile.client-ns, wallet persisted viaCASHU_DIR); S4 mint outage mid-session (logged backoff, recovery after restart; ms-expiry recorded as NOTE — this lab expires ms sessions lazily); S5 keyset rotation + swap fees, auto-skipped unless the rotation mints are defined (validated 3/3 in an isolated run: fee-charging mint creditsfloor((amount−fee)/price)steps — 2 sats → 1 step at 100ppk; expired-keyset tokens rejected cleanly). S5 activates in the standard compose once the rotation/fee mint services land via test(cloud-lab): fee-charging mint + swap-fee e2e suite (#409 follow-up) #413/test(cloud-lab): keyset-rotation lane + swap-fee boundary pins (#409/#413 follow-up) #416.Token-format coverage: the cdk-cli path sends V3 (
cashuA, with short→full keyset-ID expansion); the nutshell path in S3 sends V4 (cashuB) by default and the router accepted it live, so both wire formats are exercised end-to-end in this PR's lanes.Honest limits
No physical-router validation yet — the real NDS MAC-registration/ARP path is the one class cloud-lab cannot test; a hardware lane is planned in physical-router-test-automation. Known clientd gaps tracked in issues: rapid-repay circuit breaker / usage-blind drain (#422), burned-token reclaim on rejected payments (#423), and a nutshell balance-visibility anomaly after send (#424).