All bets of all rounds paid one address (m/84'/746'/0'/1/0), so its history
grew forever, other rounds' money and stray coins mixed with a round's own, and
the payout picked from everything it held.
- hd.py: derive_pool_key/derive_pool_address(round_id) on branch 2
(m/84'/746'/0'/2/<round_id>); no argument is the reserve address 1/0.
derive_round_pool_key checks the key against the address stored on the round,
so a payout can never be signed for money its key does not control.
- rounds.pool_address (migration 477bddae1a84, existing rounds backfilled with
the reserve address since that is where their bets went; add_column, not a
batch alter, so the expression indexes stay): written in the transaction that
inserts the round, once its id exists.
- place_bet pays round.pool_address; the payout reads, signs and takes change at
the round's own address; own_address_for and bump_fee's _signing_context follow
the round (confirmation poller and reconciler pass round_id).
- Withdrawals to any round pool address, the reserve address or fee_address are
refused (withdrawal_to_platform_address, 400, all seven languages).
- GET /rounds/current and GET /admin/rounds expose pool_address.
- RND-22/DAT-11 stay "da implementare" for B-82: the payout still spends every
confirmed coin at the round's address, not only the participants' outpoints
(participants carry no bet_vout yet).
- tests/unit/conftest.py: every test now gets a throwaway master key. Opening a
round derives an address, and tests that merely opened a round were silently
reading the real key from the local .env.
- Existing tests changed because the spec changed: payout fixtures give their
round a pool_address; the reconciler's address-cache test uses two payouts of
one round (rows of different rounds no longer share an address);
own_address_for now takes the round id.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
batch_alter_table on SQLite rebuilds the table and silently drops
ix_rounds_single_active and ix_users_username_lower, autogenerate never sees them,
and no test ran the migrations (the others build the schema with create_all).
tests/unit/test_migrations.py runs `alembic upgrade head` on an empty file DB in a
subprocess and asserts both indexes exist with their definition (read from
sqlite_master) and that a second active round and a case-variant username are
refused. Verified to fail when a batch_alter_table on rounds is added to the last
migration. The current chain already preserves both indexes, so no migration changes.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A reply line over asyncio's default 64 KiB StreamReader limit raised out of
_read_loop and closed the only connection; the pool address's get_history passes
that after ~800 transactions, and dust sent to any public address does it on
purpose.
- ElectrumClient opens the stream with a 16 MiB limit and frames lines itself:
an oversized line is discarded up to its newline, a non-JSON line is dropped,
and the request whose id is readable in the first/last 256 bytes fails at once;
the connection stays up either way.
- Coins below DUST_LIMIT_SATS are never offered as credit candidates, never
credited, and select_utxos never selects them.
- Spec (ELC-20, DEP-01, DAT-10, ARC), storico, BUGS.md and test counts updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Add docs/README.md as the index of all documentation.
- Split the 52 DEC entries of decisioni-e-limiti.md into docs/adr/ADR-NN
files (same numbers), add ADR-53 (audit log), TEMPLATE.md and an ADR index
with a feature -> ADR table.
- Rename decisioni-e-limiti.md to limiti-noti.md (LIM only) and rewrite all
DEC-nn references as links to the ADRs.
- Document the docs-first / SSOT / ADR rules in processo-di-modifica.md and
map the docs in CLAUDE.md and README.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pysqlite's legacy mode emitted BEGIN only at the first DML, so every read
before it ran outside any transaction and a lock wait surfaced as "database is
locked" at an arbitrary later statement; place_bet also mistook that error for
a write-snapshot conflict, which SQLite cannot produce.
- _register_sqlite_pragmas: synchronous=FULL, isolation_level=None and a
`begin` event that emits BEGIN / BEGIN IMMEDIATE.
- begin_immediate(session): takes the write lock before the first read; called
first thing in place_bet, request_withdrawal, the scheduler transitions,
_trigger_payout, the confirmation poller, the reconciler, bump_fee's persist
phase, deposit crediting / external-spend marking / reinstating, and the
derivation-index allocation at registration.
- A lock not granted before a broadcast answers service_busy (503, all seven
languages), nothing built or sent; after a broadcast nothing is undone, the
rows stay `building` for the reconciler.
- Spec, storico, BUGS.md and CLAUDE.md updated; 37 new tests on a real file DB.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Finishes what 88221b0 started: sicurezza.md, audit-log.md and storico-bug.md
now describe the target behaviour too, the operator guides are corrected
against today's code, and api.md gains the missing /health/ready and
/admin/electrum sections plus every new error code in its catalog.
Harmonised names and owners across documents (the `resurrected_tx` pause
reason, the reconciler as the one that watches failed rows, the full
`winner_drawn` payload, a reason for an unanswered pre-payout pool check,
one corroboration-failure row per hash instead of one per tick).
Adds B-98 (coin selection stops at the first covering UTXO and refuses a
payable bet), tightens RND-40 to check both 1 and 400 participants and to
guard PUT /admin/config, and gives a stalled `closing` round a signal
(TX-29, closing_stalled).
Records three decisions confirmed on 2026-09-29: the draw's server_seed is
stored Fernet-encrypted until revealed (SEC-56, DAT-64), the chosen abuse
and key-check thresholds (SEC-49, SEC-54, SEC-55), and a broadcast
rejection is verified against every server before any coin is released,
so matching "already known" text is a shortcut, not a safety condition
(TX-04).
Spec only; no code changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fourth full audit, run against the docs/ specification instead of the code
alone, found logical flaws rooted in PLM's small hashrate (~22 TH/s): a
minable draw, no distinction between "never broadcast" and "unknown if
broadcast", an Electrum client that degrades as the pool address's history
grows, and 1-confirmation deposits on a chain where a reorg is rentable.
The spec now describes the target behaviour for every finding, marked
*Stato:* da implementare (B-nn) with an *Oggi:* line for what the code does
now; docs/piano-correzioni.md records the design decisions and the
implementation order. No code changed — this is spec-only, per B-nn
convention when a bug reveals the spec itself was incomplete.
Also documents, as an accepted-but-open risk (LIM-24), that the optional
jackpot cap (max_round_pool_sats) isn't tied to the cost of a 51% attack —
deferred, not decided.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The two Mermaid diagrams under flowchart/ described the intended flow but
had drifted from the code and carried no link back to it. docs/ now holds
the specification instead: 15 Italian documents (flows, architecture, data
model, API, security, audit log, operations, decisions and limits, bug
history) with 472 numbered requirements, each pointing at the function
that implements it and the test that covers it. docs/processo-di-modifica.md
defines the ID convention and the spec -> code -> test change workflow, and
CLAUDE.md now names docs/ as authoritative.
Writing the spec from the code also corrected stale claims in CLAUDE.md
(B-41's get_history check, BUGS.md still tracking B-71/B-72, Cache-Control
scope, the error contract's bare-string exceptions) and in the operator
guides (payouts do retry, only `drawing` fails to resume, rounds do not
open without fee_address). The B-70 doc test now pins the prose in
docs/scommesse-e-prelievi.md instead of the deleted diagram.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>