Files
plm-lottery/BUGS.md
T
davideandClaude Opus 5 23d58796b6 Refuse to open a round that could not pay its winner (B-66)
fee_address has no column default, because an operator has to supply their own —
and the payout pays the 30% commission to it, so build_payout_transaction cannot
even be built without one. A fresh instance nonetheless opened rounds happily:
each took bets, confirmed them, and only then discovered it was unpayable,
wedging in "paying_out" and retrying every 60s with money already in the pool.
One manual recovery per round, until somebody noticed.

open_new_round_if_needed now checks rounds_can_open(config) alongside `paused`:
no payout address, no round. Nothing has moved yet at that point, which is the
whole difference. Same scope as pausing — a round already in progress still
closes, draws and pays out, since clearing the address mid-round is exactly the
operator slip that must not strand a live round.

Surfaced rather than silent, in the two places that matter: lottery_configured on
GET /rounds/current, which makes / show a *different* banner from the maintenance
one (telling a player "come back later" would be false — nothing is coming until
setup finishes), and a warning at the top of /admin's Parametri card, the one
screen that can fix it. rounds_can_open is where any future
would-make-a-round-unpayable prerequisite belongs, instead of being discovered at
payout time.

The test churn is the finding restated: 26 tests expected a round to open on an
instance with no payout address. Their fixtures now seed one, so each goes back to
testing what it says — several would otherwise have passed for the wrong reason,
returning None because of the missing address rather than because of the cooldown
or pause under test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:13:00 +02:00

5.9 KiB

BUGS.md — audit of 2026-08-03

Third full-codebase audit, opened after the 2026-07-26 (B-01 … B-24) and 2026-07-27 (B-25 … B-49) lists were emptied. Numbering continues from the last fixed finding, B-51.

The list opened at B-52 … B-72 and holds only what is still open: a finding is removed from this file once it is fixed, and is not listed here afterwards. Per CLAUDE.md's convention each entry gets its own commit with its own regression test, and the B-nn marker goes in a comment next to the fix, so git log --all --grep 'B-nn' is the record of how any closed finding was closed.

State of the tree at audit time: 264 unit tests, all passing; tests/integration/ still empty; withdrawal and the RBF bump path still never live-broadcast.

Verified as not broken while looking for these: i18n key parity (146 identical keys across all 7 languages), HTML escaping of every user-controlled value in admin.js, the Alembic chain (linear, single head, matching models.py), strictly-integer satoshi arithmetic everywhere, and .gitignore coverage of secrets/DB/logs (nothing sensitive is tracked in git).

Severity is about consequence, not likelihood: critical = money stuck or lost, or the lottery stops; high = a security control that does not hold; medium = wrong behaviour with a bounded blast radius; low = drift between documentation and code.


Critical

(not new) drawing does not resume after a restart

Already tracked as an accepted gap in CLAUDE.md's "Known gaps", not re-numbered here. Worth restating in context: with restart: unless-stopped on the app container, this is the one state that gets stuck with money in play, and it remains the last prerequisite for running unattended.


Medium — correctness and robustness

B-67 — /qr/{address} is unauthenticated, synchronous and only shape-validated

app/api/routes/qr.py:11-21.

qrcode.make runs on the event loop, so the endpoint is a cheap CPU amplifier for an unauthenticated caller, and the regex accepts any plm1[a-z0-9]{10,90} string without validating the bech32 checksum — so it happily renders a QR for a non-address.

Fix: validate with is_valid_plm_address (already used by withdrawals and by the admin fee_address validator), and either offload the render or cache it per address.


Low — documentation and consistency drift

B-68 — CLAUDE.md and README describe a state the code has moved past

  • CLAUDE.md's tech-stack line still says JWT has no revocation (B-34); token_version implements exactly that revocation (app/db/models.py:29, app/auth/dependencies.py:26).
  • "Known gaps" still says no rate limiting anywhere (B-33); login and registration are throttled (app/auth/rate_limit.py). What is genuinely still unthrottled is bets, withdrawals and the admin endpoints — that is the claim worth keeping.
  • "Known gaps" still says /report-bug is a placeholder; it is fully implemented and translated, with admin triage. Only /guida is still a stub.
  • Test counts are stale in three places: 264 actual, CLAUDE.md says 253 twice, README says 232.
  • The code map omits app/auth/rate_limit.py, app/api/client_ip.py and app/api/routes/bug_reports.py.
  • README links flowchart.mmd, which does not exist (the diagrams live in flowchart/), describes docs/running-the-server.md as covering "local venv vs. Docker" (that workflow was removed in B-44), and links the anchor CLAUDE.md#tech-stack-mvp, which no longer exists.

B-69 — stale in-code comments

  • app/tx/reconcile.py:206 calls the payout retry "a future payout-retry routine — still an open gap"; it exists (B-26).
  • app/db/base.py:9 says "five concurrent background tasks"; there are six.
  • app/auth/routes.py:31 cites B-31 where it means B-33 (see B-58).

B-70 — the flowchart's WITHDRAW precondition is not implemented as written

flowchart/platform-overview.mmd:39 (node E1) states a withdrawal cannot happen together with a bet in progress. The code only serializes the builds through the per-user lock (app/tx/locks.py): a withdrawal is accepted while a bet is still unconfirmed, as long as confirmed UTXOs cover it.

CLAUDE.md declares every node and edge label of the diagrams a behaviour that must be implemented as described, so one of the two has to move — most likely the diagram, since the lock already prevents the actual double-spend hazard, but that is a decision, not a cleanup.

B-71 — .env points MASTER_KEY_PATH at a second copy of the master key

CLAUDE.md's deployment section prescribes pointing MASTER_KEY_PATH at the host-side ./data/keys/master.xprv.enc so the venv scripts and the container read one file. .env instead sets ./master.xprv.enc, and both files now exist in the working tree (both gitignored). They were verified during this audit to decrypt to the same xprv, so nothing has diverged yet — but scripts/decrypt_master_key.py reads a different file from the one the container uses, and a future generate_master_key.py --overwrite would split them silently, with an ops recovery path that then reports the wrong key.

The same duplication exists for the database (./plm_lottery.db next to data/db/plm_lottery.db), which is less dangerous but equally confusing.

Fix: set MASTER_KEY_PATH=./data/keys/master.xprv.enc in .env, delete the stray root copy once confirmed redundant, and state the same for DATABASE_URL.

B-72 — docs/setup.md still frames setup as "locally or via Docker"

docs/setup.md:1-10 lists Python as a prerequisite "for the local/venv workflow" and describes the master-key step as local or Docker, while B-44 made Docker the only supported way to run the server (docs/running-the-server.md and README were updated, this file was not). The venv genuinely is needed for tests, migrations and the key scripts — the wording just needs to say that instead of implying a second way to run the server.