select_utxos had no ceiling on input count, so an address fragmented into many small deposits built an ever-larger transaction whose fee — deducted from the amount being moved — eroded the bet's share of the pool or the withdrawn amount, and past a few hundred inputs stopped being standard at all. MAX_TX_INPUTS (50) now bounds the selection. Reaching the cap without covering the target is reported as its own "too_many_inputs" code, distinct from having no funds, with the cap carried in the error params for the 7 translations. The payout path records the same distinction in its payout_failed audit reason.
69 lines
5.8 KiB
Markdown
69 lines
5.8 KiB
Markdown
# Known bugs
|
|
|
|
A second full-codebase audit on 2026-07-27 found **25 further issues** (4 critical, 6 high,
|
|
7 medium, 8 low), listed below as B-49. B-25 through B-48 are fixed (see "Previously
|
|
fixed" below) — no Critical-, High- or Medium-severity finding remains open; the remaining 1 is
|
|
Low/hygiene. The 139-test suite was green at the time of the audit, so none of these were caught
|
|
by existing coverage — every fix lands with a regression test (the twenty-four fixes so far
|
|
brought the suite from 139 to 251).
|
|
|
|
The recurring pattern across the open findings is worth stating once: 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](CLAUDE.md), which is also where other by-design limitations (single-shared-token
|
|
admin auth, single-process assumptions, no user-facing history, etc.) are documented.
|
|
|
|
---
|
|
|
|
## Low / hygiene
|
|
|
|
### B-49 — Rollback paths do not publish an SSE update
|
|
|
|
`bets/service.py:_release_failed_bet` and `withdrawals/service.py:_release_failed_withdrawal`
|
|
restore the balance without calling `broadcaster.publish()`, so dashboards only find out on
|
|
their next poll.
|
|
**Fix:** one `broadcaster.publish()` at the end of each, as every other state-changing path
|
|
already does.
|
|
|
|
---
|
|
|
|
## 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-46** — `secrets.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-48** — `select_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-47** — `raw_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-33** — `POST /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-40** — `bump_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).
|