# BUGS.md — open findings Two audits currently have open entries: 2026-08-03 (B-71, B-72, below) and the 2026-09-28 specification review (B-73 … B-98, at the end of the file). ## 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. --- ## Low — documentation and consistency drift ### 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. --- # Audit of 2026-09-28 — specification review (B-73 … B-98) Fourth audit, run against the new specification in `docs/` rather than the code alone: four independent reviews (coin/chain mechanics, money state machine, DevOps, spec cross-validation), with the critical claims re-verified in code. The target behaviour of every entry below is now written into `docs/` as requirements marked `*Stato:* da implementare (B-nn)`; the Italian overview, the design decisions and the implementation order are in [docs/piano-correzioni.md](docs/piano-correzioni.md). The code has **not** been changed yet. Same convention as above: one commit per entry, with its own regression test, spec + code + test together. Design decisions taken with the owner on 2026-09-28 (they change existing decisions, see `docs/limiti-noti.md`): - **Draw by commit-reveal.** The winner must not be computable by anyone who controls block production, since PLM's hashrate (~22 TH/s) is rentable. - **Never release funds on doubt.** A money-moving row is rolled back only on proof (an explicit rejection, or its inputs confirmed-spent elsewhere), never on a timeout or on one server's silence. - **One pool address per round**, so a round's money, history and payout are isolated from every other round's. - **Deposits need 6 confirmations** (configurable), everything else keeps 1: the deposit is the only input a user can double-spend. - **Contain reorgs instead of pretending they don't happen:** detect them, pause on deep ones, and pay out only what is verifiably on chain. ## High ### B-91 — Host and supply-chain hardening The container runs as root, `.env` and key files are world-readable, dependencies are unpinned `>=` and base images float, secrets sit in env vars visible to `docker inspect`. Fix: non-root `USER`, `0600`/`0700` permissions, a hash-pinned lock file, digest-pinned base images, the Fernet key mounted as a file rather than an env var. ## Medium ### B-92 — No health, readiness or real audit browsing `/health` always answers ok; nothing exposes Electrum state; the audit endpoint returns only the latest 500 rows unfiltered; registration writes no audit row; the access log records Caddy's IP; app logs go only to the file. Fix: `GET /health/ready` (Electrum connected, tip age < 1800s, scheduler heartbeat < 30s) used as the container healthcheck; Electrum status in `/admin`; audit filters by event type, round, user and time, with pagination; `user_registered` audit row with index and address; uvicorn proxy headers from Caddy only; logs also to stdout. ### B-93 — Deploys and rollbacks A deploy with an open SSE stream never shuts down gracefully (uvicorn waits, Docker kills after 10s); an older image cannot start on a newer DB; images are untagged; `SITE_ADDRESS` passed per command reverts to `localhost` on the next plain `docker compose up`. Fix: close SSE streams on shutdown, `stop_grace_period: 30s`, await task cancellation; tag images per release; `SITE_ADDRESS` in `.env` with `${SITE_ADDRESS:?}`; documented pre-deploy backup and downgrade procedure. ### B-94 — Account and endpoint abuse limits `change-password` has no rate limit, no audit and no maximum length; login fields have no length bounds; `POST /bug-reports` is anonymous and unlimited; `GET /admin/users` has no pagination; admin, SSE and framework errors are bare strings; static JS/CSS have no `Cache-Control`. Fix: throttle and audit (`password_changed`) the password change, cap every credential field at 256, rate-limit bug reports per IP, paginate the user list, structured errors everywhere, `Cache-Control: no-cache` on static files. ### B-96 — Smaller state inconsistencies A reduced full-balance withdrawal is not re-checked against the minimum; an abandoned payout leaves `winner_amount_sats`/`fee_amount_sats` set; a promoted payout writes no `payout_sent` and a promoted bet no `bet_placed`; the reconciler pings SSE for every row it checks; concurrent refreshes of one user can collide on the unique outpoint and end the Electrum session; `listener.client` is read outside the per-user lock; a bet covered only by pending change gets `insufficient_balance` instead of `balance_pending_confirmation`. A round waiting in `closing` on a bet that never leaves the mempool gives no signal at all. Fix: each as described, one regression test per item; for the `closing` wait, a `closing_stalled` audit row every 1800s past the deadline and `closing_waiting_since` on `GET /rounds/current` (spec: TX-29).