diff --git a/BUGS.md b/BUGS.md index 5d5f287..65fbd84 100644 --- a/BUGS.md +++ b/BUGS.md @@ -142,7 +142,7 @@ digest-pinned base images, the Fernet key mounted as a file rather than an env v 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 < 360s, scheduler heartbeat < +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. diff --git a/CLAUDE.md b/CLAUDE.md index 1ee18aa..536c62c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ The user communicates in Italian in chat — reply to them in Italian. Everythin ## Project status -All 10 stages of the original build order are code-complete and unit-tested — 697 tests, all under `tests/unit/` (`tests/integration/` is an empty package). Beyond them: Docker + Caddy deployment, admin dashboard (`/admin`), static test UI (`/`), pending-inclusive balance display, an SSE push channel layered over the original polling, self-service password change + admin password reset, and the reconciliation/corroboration machinery below. +All 10 stages of the original build order are code-complete and unit-tested — 698 tests, all under `tests/unit/` (`tests/integration/` is an empty package). Beyond them: Docker + Caddy deployment, admin dashboard (`/admin`), static test UI (`/`), pending-inclusive balance display, an SSE push channel layered over the original polling, self-service password change + admin password reset, and the reconciliation/corroboration machinery below. Verified on mainnet with real money: registration + address derivation, deposit crediting (at the time, 1-conf; deposits now need `deposit_confirmations`), a real 10 PLM bet (broadcast → confirmed → change credited back), and one full round cycle (close → draw on a real block hash → 70/30 payout with sat math checked against the broadcast tx → confirmation → close → next round auto-opened). **Withdrawal and the RBF bump path have never been exercised against a live broadcast** — unit-tested only. @@ -49,7 +49,7 @@ PYTHONPATH=. python scripts/decrypt_master_key.py # ops recovery: decrypt+pr PYTHONPATH=. python scripts/encrypt_master_key.py # ops bootstrap: import an externally-generated xprv (--overwrite to replace) PYTHONPATH=. python scripts/electrum_smoke_test.py # manual check: connect, handshake, subscribe to headers, print the tip -python -m pytest # all 697 tests +python -m pytest # all 698 tests python -m pytest tests/unit/test_hd.py # one file python -m pytest tests/unit/test_hd.py::test_derivation_is_deterministic # one test ``` @@ -160,6 +160,7 @@ One connection serves everything — deposit credits, broadcasts, confirmations, - **Rotation.** `ELECTRUM_HOST`/`PORT` is primary, `ELECTRUM_FALLBACK_SERVERS` a comma-separated `host:port[:notls][:pin=]` list (`client.py:parse_endpoints` rejects malformed entries at startup, not during the outage when the fallback is needed; `pin=` fixes the server's DER certificate hash, `ELECTRUM_CERT_PIN` does the same for the primary, B-84). The server **refuses to start with fewer than 2 fallbacks** (`config.validate_quorum_servers`, B-84; `ALLOW_NO_QUORUM=true` overrides it, development only). After any failed or dropped session the next server is tried immediately; the backoff (1s doubling to 30s) only kicks in once every server has had a turn. - **Bounded requests** (`_REQUEST_TIMEOUT_SECONDS` = 15s); a timeout tears the connection down. Unbounded waits used to hang `POST /bets` *while holding the per-user lock*, and could stall the confirmation poller permanently. - **The drop is observable**: `client.wait_closed()` resolves when the read loop dies, and `_run_once` races it against the notification consumers and a 60s `server.ping`. A server that keeps answering pings but stopped following the chain is caught too (B-86): at every ping, if no header has been *accepted* for 360s (`_STALL_SECONDS`), `_check_not_stalled` asks the other servers for their tip and ends the session with `ServerStalledError` if any is higher, so `run()` rotates; none higher means a quiet chain and the session goes on. Without it the listener sat on queues nobody would ever fill while `listener.client` still looked alive. +- **The chain is checked, not just the headers** (B-99, ELC-30): `ElectrumClient.connect` follows `server.version` with `server.features` and refuses a server whose `genesis_hash` is not PLM's. PLM shares Bitcoin's genesis *and* powLimit, so a Bitcoin Electrum server would pass every header check below; this is what tells them apart, for the quorum's connections too. - **Headers are validated, not trusted** (`_apply_header`, async since B-74): the tip never regresses, a header must meet the difficulty target it claims *and* that target must be no easier than `draw.POW_LIMIT` (PLM's powLimit; `tests/unit/conftest.py` swaps in a regtest limit so tests can mine headers, and the tests of the limit itself put `MAINNET_POW_LIMIT` back), and every advance must be linked to the tip: a single block by its `prev_hash`, a jump by downloading the headers in between (`blockchain.block.headers`, ≤2016 per request) from the active server and checking each. Failure raises `HeaderValidationError`, which ends the session like a dropped connection and rotates away — that header is the draw's only entropy, so a fabricated one picks the winner. The listener keeps the last 10 headers (`_window`, built from the first header plus its 9 predecessors); a header that does not join the tip is a reorg, walked back to its fork point inside the window and followed (`_follow_reorg`), a fork older than the window ends the session. A followed reorg writes `chain_reorg_detected` *before* the window moves (`_record_reorg`, B-85), and a depth of 2 or more pauses the lottery with `paused_reason = 'reorg'` in the same transaction; a header at the tip's own height with a different hash is a reorg too, not ignored. One more header is refused *without* ending the session (B-64): one carrying no `hex` at all (nothing to validate or draw from, and applying the height alone would break the `tip_height`/`tip_header_hex` pairing). `_run_once` separately refuses to publish the client while *no* tip is known, so the ignore-don't-kill choice can't reopen B-63. - **A quorum corroborates every money-moving decision** (`_corroborate_majority`, 10s per server, asking only the *other* endpoints — never the active one, which is what a MITM controls): `corroborate_header` before a block seeds the draw (B-28), `corroborate_utxo_spent` before a UTXO missing from one `listunspent` is written off as externally spent (B-29), and `corroborate_utxo_credit` before a new outpoint credits a balance — same outpoint, same amount, confirmed (B-59). Balances move in both directions, so both directions need the same quorum — and since B-83 that includes `corroborate_utxo_reinstatement` before a UTXO written off as externally spent is put back (same outpoint, amount, confirmed; `deposits/service.py:find_reinstate_candidates` only reads, `reinstate_reappeared_utxos` writes what the quorum agreed on), and `confirmation_proven` before a tx is marked confirmed: `tx/confirmation.py:poll_once` takes the height the active server reports only as a *claim*, and the row confirms when a strict majority of at least 2 of the *other* servers return a `get_merkle` proof that recomputes to the merkle root of the block's header (`electrum/merkle.py`; the header is the tip we hold, or for an older block the one the quorum agrees on and that meets its own target). With no `network` nothing confirms. Since B-82 the pool-coin check before a payout is on the quorum too (`verify_pool_outpoints`, below). A quorum needs **at least 2 answers** and a strict majority of those that answered (B-84): fewer → returns **False**, since one voice decides nothing independently and an unreachable network proves nothing. Only `ALLOW_NO_QUORUM=true` keeps the old reduced rule (no other server → True, otherwise a majority of whoever answers). A failed credit corroboration only *delays*: `find_new_credit_candidates` re-offers the outpoint on the next refresh or `DepositReconciler` sweep. diff --git a/README.md b/README.md index 85a8fb8..ec20849 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ python -m pytest # all tests python -m pytest tests/unit/test_hd.py # one file ``` -697 unit tests cover HD derivation, PSBT building, the Electrum client, bets, +698 unit tests cover HD derivation, PSBT building, the Electrum client, bets, deposits, withdrawals, the round/draw engine, RBF fee-bumping, admin config, the pending-inclusive balance calculation, and the SSE push channel. No automated integration tests against a live Electrum connection — mainnet diff --git a/docs/architettura.md b/docs/architettura.md index 34163dc..9dc5360 100644 --- a/docs/architettura.md +++ b/docs/architettura.md @@ -787,7 +787,7 @@ flowchart LR *Codice:* `app/static/admin.js:startChainStatusPolling` · `app/static/admin.js:connectAdminEvents` · `app/static/admin.js:onAdminServerEvent` - *Stato:* da implementare (B-86, B-92), solo per lo stato Electrum e i + *Stato:* da implementare (B-92), solo per lo stato Electrum e i filtri dell'audit · *Oggi:* il pannello mostra solo l'altezza della catena, e l'audit log le ultime righe senza filtri. diff --git a/docs/limiti-noti.md b/docs/limiti-noti.md index 8ce9b61..0390133 100644 --- a/docs/limiti-noti.md +++ b/docs/limiti-noti.md @@ -16,8 +16,6 @@ Un limite che il [piano delle correzioni](piano-correzioni.md) chiuderà porta atteso; quando la correzione entra nel codice, il limite si barra come `~~[LIM-nn]~~ Rimosso: chiuso da B-nn`. -Formato di ogni voce: descrizione, *Impatto*, *Mitigazione attuale*, *Per -chiuderlo*, e il codice coinvolto. Formato di ogni voce: descrizione, *Impatto*, *Mitigazione attuale*, *Per chiuderlo*, e il codice coinvolto. diff --git a/docs/storico-bug.md b/docs/storico-bug.md index 4343719..e0bbff8 100644 --- a/docs/storico-bug.md +++ b/docs/storico-bug.md @@ -204,7 +204,7 @@ Note: insieme» di [scommesse-e-prelievi.md](scommesse-e-prelievi.md): quel titolo e le frasi che il test cerca vanno mantenuti. -## Revisione della specifica del 2026-09-28 (B-73 … B-98) +## Revisione della specifica del 2026-09-28 (B-73 … B-100) Quarta revisione, condotta sulla specifica in `docs/` invece che sul solo codice: quattro analisi indipendenti (meccanica della coin, macchina a stati @@ -247,14 +247,16 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)). | B-96 | Incoerenze minori di stato (prelievo ridotto sotto il minimo, importi di un pagamento abbandonato, audit mancanti, attesa in `closing` senza segnali, …) | Media | **aperto** | — | TX, BET, WDR, DEP | | B-97 | Le migrazioni possono eliminare in silenzio gli indici su espressione | Media | *da registrare* | `test_migrations.py` | DAT | | B-98 | La selezione delle monete si ferma alla prima che copre l'importo e rifiuta una scommessa pagabile | Media | **aperto** | — | BET, WDR | +| B-99 | Un server Electrum di Bitcoin supera ogni controllo sugli header (PLM ne condivide genesis e `powLimit`): il genesis non veniva verificato | Media | *da registrare* | `test_electrum_client.py` | ELC | +| B-100 | Il tetto del fee rate coincide con il `maxfeerate` del nodo (10 000 sat/vB): una tariffa al tetto, con vsize stimato per eccesso, verrebbe rifiutata | Bassa | *da registrare* | — (costante; copertura da `test_rbf_dust.py`) | TX | ## Stato attuale - **Corretti:** B-01 … B-70, tranne B-16, che è chiuso nella sostanza (il crash è eliminato e `/guida` serve un segnaposto) ma lascia la guida vera - come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83, B-74, B-85, B-86, B-73, B-81, B-79, B-82, B-87, B-95. + come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83, B-74, B-85, B-86, B-73, B-81, B-79, B-82, B-87, B-95, B-99, B-100. - **Aperti:** B-71, B-72 (gravità bassa, operativi o di documentazione) e B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (0 critici, 3 alti, 5 medi), da implementare nell'ordine di [piano-correzioni.md](piano-correzioni.md). -- **Prossimo numero libero:** `B-99`. +- **Prossimo numero libero:** `B-101`.