Skip to content

feat(scripts): tollgate-clientd laptop client + cloud-lab battery — rebase of #425 - #683

Merged
c03rad0r merged 10 commits into
OpenTollGate:mainfrom
felixfelix-bot:pr/425-clientd-rebased
Oct 6, 2026
Merged

c03rad0r merged 10 commits into
OpenTollGate:mainfrom
felixfelix-bot:pr/425-clientd-rebased

Conversation

@felixfelix-bot

Copy link
Copy Markdown
Contributor

Rebase of #425's eight commits onto current main (ac5811ff), authorship preserved. 11 files, +2136/−0 — the same file set and the same insertions-only shape as #425.

Head: pr/425-clientd-rebased (base main).

Conflicts (one file)

tests/cloud-lab/README.md — a table of independent file rows: main added a conformance/ row (#503) where this branch adds test_clientd_autotopup.py. Both rows kept (union of the two sides); no other hunk needed a hand.

CHANGELOG.md and .gitignore applied clean.

One placement fix worth knowing about

Replaying the branch commit-by-commit put the two clientd entries under an ### Added deep inside v0.6.0-rc1 — the position the first of the branch's commits used. The branch's own last commit (CHANGELOG entries under [Unreleased]) moved them to ## [Unreleased] → ### Added, which is where they now sit (main gained an [Unreleased] section since this branch was written). Diff is @@ -32,6 +32,47 @@ — purely additive, nothing removed.

Verification (run here, on the rebased head)

python3 -m py_compile scripts/tollgate-clientd.py tests/clientd/test_tollgate_clientd.py \
                      tests/cloud-lab/test_clientd_autotopup.py   -> ok
bash -n tests/cloud-lab/run-clientd-scenarios.sh                  -> ok
bash -n tests/cloud-lab/fake-ndsctl-filecounters.sh               -> ok
docker compose -f tests/cloud-lab/docker-compose.yml config       -> ok
python3 -m pytest tests/clientd/ -q                               -> 43 passed in 18.53s

Not run: the cloud-lab battery itself (run-clientd-scenarios.sh, S2–S5, test_clientd_autotopup.py). It needs the docker lab up (mint + tollgate + client-ns images, live nutshell wallet), which is not available in this environment. The compose file parses; the scenarios are unexercised here.

(The suite needs tests/.env — tests/conftest.py refuses to collect without the four fleet variables (#509). Created locally from tests/.env.example; it is gitignored and not part of this PR.)

Review findings, reported not fixed

Two ruff findings in the new files, left as the author wrote them:

  • F401 — scripts/tollgate-clientd.py:713: import shutil inside selftest() is unused.
  • RUF059 — tests/clientd/test_tollgate_clientd.py:473: unpacked payments is never used.

Neither is a defect; both are one-line cleanups if you want them gone. Say so and I fold them in.

Merge this and #425 closes as superseded, or force-push pr/425-clientd-rebased onto feature/tollgate-clientd.

Amperstrand added 8 commits October 6, 2026 21:33
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)
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)
… 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)
…payment tokens recoverable

The two money-path blockers from the review (OpenTollGate#422/OpenTollGate#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)
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)
…set/outcome codes, fee-underbuy warning

Finding 1 (block): the rebase surfaced OpenTollGate#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 (OpenTollGate#548 resolves
clients from the socket).

(cherry picked from commit 5f0dd38)
…NGELOG entries under [Unreleased]

The S5 skip-guard grepped for mint-rotate, which now exists on main
(OpenTollGate#413/OpenTollGate#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 OpenTollGate#425 link.

(cherry picked from commit 802be6f)
@felixfelix-bot

Copy link
Copy Markdown
Contributor Author

Review record — self-authored PR, so the approval has to be yours (GitHub will not let the author approve their own).

Re-run at head e05cf188 (the rebased head):

python3 -m py_compile scripts/tollgate-clientd.py tests/clientd/test_tollgate_clientd.py \
                      tests/cloud-lab/test_clientd_autotopup.py     -> ok
bash -n tests/cloud-lab/run-clientd-scenarios.sh                            -> ok
bash -n tests/cloud-lab/fake-ndsctl-filecounters.sh                         -> ok
docker compose -f tests/cloud-lab/docker-compose.yml config                 -> ok
python3 -m pytest tests/clientd/ -q                                         -> 43 passed in 18.53s

Not run, stated plainly: the cloud-lab battery itself (S2–S5, test_clientd_autotopup.py) — it needs the mint/tollgate/client-ns images and a live nutshell wallet, which this environment does not have. That lane is unexercised here.

Two ruff findings left exactly as the author wrote them (neither is a defect): F401 scripts/tollgate-clientd.py:713 (import shutil unused in selftest()), RUF059 tests/clientd/test_tollgate_clientd.py:473 (unpacked payments unused).

…eview

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
@felixfelix-bot

Copy link
Copy Markdown
Contributor Author

Tag legend: pre-release-safe = land before the pre-release you are testing · ready = no blocker, timing is yours · post-release = hold until after · needs-author-pass = back to the author.

TAG: pre-release-safe — additive (new script + unit suite + a cloud-lab profile); nothing in the shipped binary changes.

Changes I made, as asked (commit 091a729f, pushed to this branch):

F401  scripts/tollgate-clientd.py:713    `import shutil` in selftest() — dropped
RUF059 tests/clientd/test_tollgate_clientd.py:473,487,501
        unpacked `payments` never read — renamed; the test that DOES assert
        on `payments` (test_terminal_rejection_stops_daemon) keeps its name

Re-verified after the edit: ruff check — all checks passed; python3 -m pytest tests/clientd/ -q — 43 passed; py_compile ok. (My first pass at that rename hit the wrong occurrence and broke one test — caught by the suite, fixed function-aware, re-run green. Worth saying because the head did move.)

Not run, stated plainly: the cloud-lab battery (S2–S5, test_clientd_autotopup.py) — it needs the mint/tollgate/client-ns images and a live nutshell wallet, which this environment does not have. docker compose config parses.

The head changed (091a729f), so if you had blessed the earlier one, re-approve. Self-authored — needs your approval.

@c03rad0r
c03rad0r self-requested a review October 6, 2026 21:44
@c03rad0r
c03rad0r requested a review from Amperstrand October 6, 2026 21:44
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