Files
plm-lottery/BUGS.md
T
davide e7f844b11f Publish an SSE update from the bet/withdrawal rollback paths (B-49)
_release_failed_bet and _release_failed_withdrawal restored the balance, freed
the reserved UTXOs and (for a bet) removed the participant without calling
broadcaster.publish(), so every dashboard kept showing the phantom bet and the
reduced balance until its next poll — while the success path and the
reconciler's own abandon path both published.

The two regression tests pre-open the round before subscribing: place_bet opens
one itself, and that publish() would otherwise satisfy the assertion whether or
not the rollback published anything.
2026-07-27 23:35:10 +02:00

5.7 KiB

Known bugs

A second full-codebase audit on 2026-07-27 found 25 further issues (4 critical, 6 high, 7 medium, 8 low). B-25 through B-49 are now all fixed (see "Previously fixed" below) — nothing from either audit remains open. The 139-test suite was green at the time of the audit, so none of these were caught by existing coverage — every fix landed with a regression test, which brought the suite from 139 to 253.

The recurring pattern behind those findings is worth keeping in mind for the next one: the code is rigorous about the failure modes that have actually been hit, and silent about the ones that have not. The payout phase is now fully recoverable; the "drawing" phase (waiting on a block) is now observable (B-36) but still has no equivalent resume-after-restart — see "Known gaps / TODO" in CLAUDE.md, which is also where the by-design limitations (single-shared-token admin auth, single-process assumptions, no user-facing history, etc.) are documented. This file stays as the record of what was found and fixed; a new finding gets the next B-nn and its own regression test.


Previously fixed

  • B-25 — the payout had no two-phase write, unlike bets and withdrawals
  • B-26 — a payout failure or a process restart could wedge a round in paying_out forever
  • B-27 — an RBF bump reset the reconciler's own abandon clock, so a repeatedly-bumped tx was never abandoned
  • B-28 — a hostile Electrum server (or a MITM) could single-handedly pick the round's winner
  • B-29 — a UTXO absent from one server's listunspent was marked spent immediately, irreversibly, on a single unauthenticated reply
  • B-30 — a lost scripthash subscription meant a user's deposits were never credited, with no periodic safety net
  • B-31 — resubscribing on reconnect ran serially before anything else started, freezing the chain tip (and so an in-flight draw) for the whole sweep
  • B-42 — Swagger/ReDoc/the raw OpenAPI JSON enumerated the entire API surface, admin endpoints included, to anyone who requested them; now off by default and gated behind ENABLE_API_DOCS
  • B-43 — the Caddyfile sent no CSP, no X-Frame-Options/frame-ancestors, and no HSTS, on a page whose JWT lives in localStorage
  • B-44 — README's Quick start documented a bare uvicorn --reload workflow, and docs/running-the-server.md still had a matching "Locale / venv" section, both contradicting CLAUDE.md's Docker-only policy
  • B-45/admin/rounds//admin/audit-log's limit had no bounds (-1 means "everything" on SQLite), and /admin/pending-transactions had no limit or status filter at all
  • B-46secrets.compare_digest on a str raises TypeError on non-ASCII input, turning an invalid admin token with non-ASCII characters into a 500 instead of a 403
  • B-49_release_failed_bet/_release_failed_withdrawal restored the balance without publishing an SSE update, so a rolled-back bet or withdrawal stayed on the dashboards until their next poll
  • B-48select_utxos had no cap on input count, so a fragmented address built an ever-larger transaction whose fee (deducted from the amount being moved) ate into the bet or withdrawal, up to the point of being non-standard
  • B-47raw_tx_hex and payload_json were unbounded String columns (VARCHAR with no length) — fine on SQLite/PostgreSQL, rejected by backends like MySQL that require a length
  • B-32 — an RBF bump could retry forever below BIP125's relay-mandated minimum fee delta, with no ceiling on the fee rate either
  • B-33POST /auth/login had no rate limiting, so a password could be brute-forced against an enumerable username list
  • B-34 — password change/reset didn't invalidate already-issued JWTs, so a stolen token survived a change meant to lock it out
  • B-35 — API timestamps round-tripped as naive datetimes, so the frontend parsed them as local time instead of UTC
  • B-36 — a stalled draw wait had no timeout, no log, and no audit trail, so a frozen round showed nothing in /admin
  • B-37 — a withdrawal covered by unconfirmed change answered "insufficient balance" instead of distinguishing it from actually having no funds
  • B-38 — the SSE subscriber cap was global, so one client opening enough connections degraded every other user to polling
  • B-39 — SQLite ran without WAL or a busy_timeout, so a writer could block every reader and a second writer failed immediately instead of waiting
  • B-40bump_fee held a DB session open across N slow network calls, and computed a prevout's value from a server-reported float instead of an exact integer
  • B-41 — confirmation/reconciliation depended on a verbose blockchain.transaction.get reply many Electrum servers reject, and abandonment relied on fragile substring-matching of an error message

See git history for the fix-by-fix breakdown (commits f13f685, 50a43ae, 933760e, and the B-28/B-29/B-30/B-31/B-32/B-33/B-34/B-35/B-36/B-37/B-38/B-39/B-40/B-41/B-42/B-43/B-44/B-45/B-46/B-47 fixes). Suite grew from 139 to 248 tests over the twenty-three.

A full-codebase audit on 2026-07-26 (commit d4e0974) found 24 bugs across every Python module under app/, both static frontends, and the Docker/Caddy deployment — 5 critical, 7 high, 7 medium, 5 low. All 24 were fixed and verified against the current code on 2026-07-27; the fixes are covered by the regression suite (grew from 79 to 139 tests) and five of them were additionally confirmed against a real mainnet deployment (see git history between fb734bb (documenting the findings) and 845ba98 (recording the audit outcome) for the fix-by-fix breakdown — each commit message names the bugs it closes and where their tests live).