The seed was the hash of the first block seen after the close, so a miner holding tickets
could withhold blocks until one made them win, the active Electrum server could pick which
real block was used by not announcing another (a failed corroboration even moved the draw to
the next block), and the participant order was the server-only `broadcast_at` with no
tie-break, so nobody could recompute the result.
- Open: every round gets a 32-byte `server_seed`, stored only Fernet-encrypted with the
master-xprv key (`server_seed_enc`), with its published `server_seed_hash`;
`draw_confirmations` is copied from round_config. The plaintext column is written only by
the reveal.
- Entering `drawing` (one transaction, `draw_committed` audit row): participants ordered by
bet txid, each `draw_position`, `participants_hash` (sha256 of `txid:vout:address\n`
lines; new `bet_vout`), `draw_height = tip_at_close + 1`. A tip of 0 keeps the round
`closing` (`draw_baseline_tip_unknown`, once per round). All persisted, so a resumed draw
reads rather than recomputes (the `_tick` branch itself is B-81).
- `_complete_draw` / `_wait_for_draw_block` replace `_wait_for_next_block`: wait for
`draw_confirmations` blocks from `draw_height`, then `ElectrumListener.agreed_block_hash`
asks every server, the active one included, for the header at that exact height (valid
work, >= 2 other answers, strict majority of all); no agreement means the same height is
asked again and `draw_header_corroboration_failed` is written once per distinct reported
hash. `draw_stalled` now counts from the last tip advance, or from when the quorum started
to fail. Seed = sha256(server_seed || block_hash || participants_hash), index = seed mod n.
- The winner, the seed and the reveal (`server_seed`, `winner_drawn` payload) are written in
one transaction; a round closing with no participants reveals too. A round opened before
this change (no commitment) is given a secret as it enters `drawing`.
- `GET /rounds/{id}/proof` (public, 404 `round_not_found`, translated in all 7 languages) and
the new fields on `GET /rounds/current`; `draw_confirmations` in `/admin/config` and the
/admin form; `server_seed_hash`/`draw_height` in `/admin/rounds`, never the secret.
- Alembic d3e8b15a7c42 (plain add_column / create_index, expression indexes untouched).
- Docs: the B-73 `*Stato:*`/`*Oggi:*` lines are gone, guides and CLAUDE.md describe the new
draw. Ambiguities resolved in api.md: the proof's participants also carry `amount_sats`
(RND-37 lists it); `seed` is decimal in the proof and hex in the audit row, as specified.
Tests changed because the spec changed: ElectrumListener.corroborate_header (asked only the
other servers about the block the active one announced) is replaced by agreed_block_hash and
its quorum tests ported; draw_winner takes the secret and the list fingerprint; the four
_wait_for_next_block scheduler tests are replaced by _wait_for_draw_block ones (fixed height,
confirmations, same-height retry, stall clock). New: test_draw_proof.py recomputes the winner
independently from the public proof, checks the plaintext secret is nowhere in the database,
the API or the log before the reveal, and that a second pass over a `drawing` round yields
the same winner. Mutation-checked: ordering by broadcast_at, an early reveal and asking the
tip instead of the draw height each fail tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
261 lines
33 KiB
Markdown
261 lines
33 KiB
Markdown
# Storico dei bug
|
|
|
|
Registro di tutti i difetti numerati `B-nn` trovati finora: cosa non andava,
|
|
quanto era grave, quale commit lo ha corretto e quale test impedisce che
|
|
ritorni. È la memoria storica del progetto; le regole valide oggi stanno negli
|
|
altri documenti della specifica, a cui la colonna **Area** rimanda per
|
|
prefisso (vedi [processo-di-modifica.md](processo-di-modifica.md#prefissi)).
|
|
|
|
Questo file non contiene requisiti e quindi non ha identificativi propri.
|
|
|
|
## Convenzione
|
|
|
|
- **Un difetto nuovo prende il prossimo numero libero**, senza riusare né
|
|
rinumerare quelli esistenti. **Il prossimo numero libero è `B-99`.**
|
|
- **Un commit dedicato per ogni difetto**, con il numero nel messaggio (ad
|
|
esempio `Snapshot a round's timing when it opens (B-61)`), così
|
|
`git log --all --grep 'B-nn'` ritrova sempre come è stato chiuso.
|
|
- **Un test di regressione per ogni difetto**, che cita il numero nella sua
|
|
docstring o in un commento. Dove un test automatico non è possibile (resa
|
|
grafica nel browser, layout dell'immagine Docker) lo si dichiara, come nelle
|
|
righe marcate *manuale* qui sotto.
|
|
- **Il marcatore `B-nn` va anche nel codice**, in un commento accanto alla
|
|
correzione: le centinaia di riferimenti sparsi in codice e test sono puntatori verso
|
|
questo registro e verso la storia git.
|
|
- **Prima la specifica, se era sbagliata.** Una correzione che riporta il
|
|
codice a ciò che i documenti già dicevano è solo un `B-nn`. Se invece il
|
|
difetto rivela che la specifica era sbagliata o incompleta, si corregge prima
|
|
il requisito in `docs/`, come per qualunque altra modifica
|
|
([processo-di-modifica.md](processo-di-modifica.md#il-ciclo-di-una-modifica)).
|
|
- I limiti accettati **per scelta** non sono bug e non prendono un `B-nn`:
|
|
stanno in [limiti-noti.md](limiti-noti.md).
|
|
|
|
### Da dove vengono questi dati
|
|
|
|
I difetti sono stati tracciati in un file `BUGS.md` alla radice del
|
|
repository, riscritto a ogni audit:
|
|
|
|
| Versione | Commit | Contenuto |
|
|
|---|---|---|
|
|
| Audit 2026-07-26 | `fb734bb`, `9b6a4a2`, `845ba98` | B-01 … B-24, con gravità, correzione proposta e, nell'ultima versione, la nota **Fixed** di ciascuno |
|
|
| Audit 2026-07-27 | `43d2321`, `7224ca0` | B-25 … B-49 con gravità |
|
|
| Chiusura | `0d2fef6` | file cancellato quando entrambe le liste erano vuote |
|
|
| Audit 2026-08-03 | `99d7a1e` e successivi | B-52 … B-72, ogni voce rimossa al momento della correzione |
|
|
| Revisione della specifica 2026-09-28 | `88221b0` e successivi | B-73 … B-98, aggiunte in coda al file |
|
|
|
|
Per leggere una versione: `git show <commit>:BUGS.md`. Il `BUGS.md` attuale,
|
|
presente nel ramo `main`, contiene solo le voci ancora aperte (B-71 … B-98).
|
|
|
|
La gravità è quella registrata in `BUGS.md` al momento dell'audit. Il terzo
|
|
audit la definiva così, e la stessa scala vale in sostanza anche per i primi
|
|
due: **critica** = denaro bloccato o perso, oppure la lotteria si ferma;
|
|
**alta** = un controllo di sicurezza che non regge, o un difetto con effetti
|
|
seri sul denaro; **media** = comportamento errato con danni circoscritti;
|
|
**bassa** = igiene, oppure documentazione non allineata al codice.
|
|
|
|
---
|
|
|
|
## Audit del 2026-07-26 (B-01 … B-24)
|
|
|
|
Primo audit completo del codice (commit di partenza `d4e0974`, 79 test verdi,
|
|
nessuno dei quali copriva questi casi). Le correzioni sono raggruppate per
|
|
tema in otto commit, fra `cc88763` e `dc4d576`, e il loro esito è registrato
|
|
in `845ba98`. La suite passò da 79 a 139 test.
|
|
|
|
La causa comune dei quattro difetti critici principali (B-01 … B-04) era che
|
|
**il codice considerava un invio come definitivo e la connessione come
|
|
infallibile**: non esisteva alcuna riconciliazione fra database e catena.
|
|
|
|
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
|
|---|---|---|---|---|---|
|
|
| B-01 | Una connessione Electrum caduta bloccava per sempre l'intero server: nessuna riconnessione, nessun timeout sulle richieste, nessun keepalive | Critica | `7c4e998` Survive a dropped Electrum connection, and fall back to other servers | `test_electrum_client.py::test_request_times_out_instead_of_hanging_forever`, `::test_wait_closed_resolves_when_the_read_loop_dies`; `test_electrum_listener.py::test_rotates_to_the_next_server_after_a_failed_session` | ELC |
|
|
| B-02 | Un RBF su una scommessa lasciava `RoundParticipant.bet_txid` sul vecchio txid, e il round restava in `closing` per sempre | Critica | `d528c5b` Let the system recover from a broadcast that never confirms | `test_confirmation.py::test_bet_confirms_after_an_rbf_bump_changed_the_txid`; `test_broadcast.py::test_bump_fee_retargets_every_stored_txid` | TX, BET |
|
|
| B-03 | Un solo txid irrisolvibile interrompeva il rilevamento delle conferme per tutte le altre transazioni | Critica | `d528c5b` | `test_confirmation.py::test_one_unresolvable_candidate_does_not_block_the_others` | TX |
|
|
| B-04 | Nessuna riconciliazione: una transazione scartata dalla rete congelava per sempre le monete spese e i fondi dell'utente | Critica | `cc88763` Make "one active round" a DB invariant and record why a tx failed; `d528c5b` | `test_reconcile.py` | TX |
|
|
| B-05 | `fee_address` e parametri numerici della configurazione non validati: un valore errato bloccava il pagamento o mandava la commissione a uno script non spendibile | Critica | `daf66fd` Fix the round-open race, the advertised jackpot, and payout error handling; `85dce22` Validate admin config and registration input, and log config changes | `test_admin.py::test_config_rejects_unusable_values` | API, RND |
|
|
| B-06 | Venivano create uscite di resto sotto la soglia di polvere, rendendo la transazione non inoltrabile | Alta | `b4d7038` Leave dust-sized change to the fee instead of creating it | `test_psbt_builder.py::test_a_below_dust_change_output_is_never_created` | BET, WDR |
|
|
| B-07 | Un invio rifiutato dalla rete arrivava al client come HTTP 500, fuori dal contratto d'errore strutturato | Alta | `d528c5b` | `test_bets.py::test_failed_broadcast_reports_the_broadcast_failed_code`; `test_withdrawals.py::test_failed_broadcast_marks_the_withdrawal_failed_and_frees_the_coins` | BET, WDR, API |
|
|
| B-08 | La scommessa veniva inviata prima di essere salvata: un errore al commit perdeva il denaro senza lasciare traccia | Alta | `d528c5b` | `test_bets.py::test_bet_is_persisted_before_it_is_broadcast`, `::test_failed_broadcast_leaves_nothing_behind` | BET, TX |
|
|
| B-09 | Una corsa permetteva due round `open` contemporanei, che bloccavano per sempre i round successivi | Alta | `cc88763`; `daf66fd` | `test_rounds_service.py::test_losing_the_open_race_reuses_the_winning_round`, `::test_the_database_refuses_a_second_active_round` | RND, DAT |
|
|
| B-10 | `PUT /admin/config` non scriveva nulla nel registro di audit | Alta | `85dce22` | `test_admin.py::test_config_update_is_audit_logged`, `::test_pause_cannot_be_toggled_through_the_config_endpoint` | AUD |
|
|
| B-11 | Il montepremi mostrato non era l'importo che il vincitore avrebbe ricevuto | Alta | `daf66fd` | `test_rounds_route.py::test_jackpot_comes_from_the_participants_actual_bets` | RND, API |
|
|
| B-12 | La registrazione non validava né username né password, a differenza del cambio password | Alta | `85dce22`; `dc4d576` Format amounts, guard the loading state, and translate the new errors | `test_users.py::test_register_rejects_weak_credentials` | REG, SEC |
|
|
| B-13 | `verify_password` trasformava un hash salvato malformato in un 500 invece che in un 401 | Media | `85dce22` | `test_security.py::test_verify_password_returns_false_for_an_unparseable_hash` | SEC |
|
|
| B-14 | Token admin confrontato con `!=` invece che a tempo costante | Media | `85dce22` | coperto dai test di autenticazione admin esistenti (`test_admin.py::test_admin_rejects_wrong_token`); nessun marcatore B-14 nel codice | SEC |
|
|
| B-15 | `JWT_SECRET` o `XPRV_ENCRYPTION_KEY` vuoti non venivano rifiutati all'avvio | Media | `25f4a1c` Refuse to start half-configured, and keep failures machine-readable | `test_config.py` | SEC |
|
|
| B-16 | `GET /guida` rispondeva 500 in Docker perché `docs/` non era copiata nell'immagine | Media | `25f4a1c` (crash eliminato, 404 `guide_unavailable`); superato da `447bbba`, che serve un segnaposto statico `app/static/guida.html` | *manuale* (dipende dal layout dell'immagine); nessun marcatore B-16 nel codice | ARC, OPS |
|
|
| B-17 | La ricerca dell'uscita di resto nell'RBF poteva ridurre l'uscita del destinatario invece del resto (prelievo verso il proprio indirizzo) | Media | `d528c5b` | `test_withdrawals.py::test_withdrawal_to_own_address_is_rejected` | WDR, TX |
|
|
| B-18 | Sessione del database tenuta aperta durante le chiamate di rete del pagamento | Media | `daf66fd` | coperto dai test dello scheduler esistenti (`test_scheduler.py`) | RND, TX |
|
|
| B-19 | `tip_height` assegnato senza controllo di monotonia (reorg o cambio server lo facevano regredire) | Media | `7c4e998` | `test_electrum_listener.py::test_tip_never_moves_backwards` | ELC |
|
|
| B-20 | `PendingTransaction.replaced_by_txid` mai scritto: colonna morta mostrata in `/admin` | Bassa | `d528c5b` | `test_broadcast.py::test_bump_fee_retargets_every_stored_txid` | TX, DAT |
|
|
| B-21 | Variabile locale inutilizzata e accesso ad attributi di oggetti ORM staccati dalla sessione nel poller delle conferme | Bassa | `d528c5b` | coperto da `test_confirmation.py` | TX |
|
|
| B-22 | Importi mostrati con una divisione in virgola mobile: artefatti visibili agli utenti | Bassa | `dc4d576` | *manuale* (resa nel browser) | ARC |
|
|
| B-23 | Due `withLoading` concorrenti sullo stesso pulsante potevano lasciarlo bloccato sull'etichetta di caricamento | Bassa | `dc4d576` | *manuale* (interazione nel browser) | ARC |
|
|
| B-24 | Il gestore globale delle eccezioni rispondeva con un `detail` stringa invece che strutturato | Bassa | `25f4a1c` | `test_rounds_route.py::test_unhandled_errors_use_the_structured_detail_shape` | API |
|
|
|
|
Note:
|
|
|
|
- **B-09** fu corretto diversamente dal piano: invece di rendere lo scheduler
|
|
l'unico a poter aprire un round, entrambi i chiamanti restano e il conflitto
|
|
sull'indice unico viene recuperato con un numero limitato di tentativi.
|
|
- **B-15** fu corretto con una validazione all'avvio (`validate_runtime_secrets`
|
|
chiamata dal lifespan) invece che con un validatore Pydantic, per non
|
|
rendere ogni import e l'intera suite di test dipendenti da un `.env`.
|
|
- **B-16** era stato dichiarato «parzialmente corretto per scelta»: la copia di
|
|
`docs/` nell'immagine fu scritta e poi ritirata su richiesta del
|
|
proprietario. Oggi `/guida` serve un segnaposto (vedi
|
|
[limiti-noti.md](limiti-noti.md)).
|
|
|
|
---
|
|
|
|
## Audit del 2026-07-27 (B-25 … B-49)
|
|
|
|
Secondo audit completo: 25 difetti (4 critici, 6 alti, 7 medi, 8 bassi), con
|
|
139 test verdi al momento dell'audit. Corretti nel ramo `audit-2026-07-27`,
|
|
unito con `5cfe2d6`; ognuno ha il proprio commit. La suite passò da 139 a 253
|
|
test.
|
|
|
|
L'osservazione finale di quell'audit resta valida: il codice era rigoroso sui
|
|
guasti già capitati e silenzioso su quelli mai visti.
|
|
|
|
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
|
|---|---|---|---|---|---|
|
|
| B-25 | Il pagamento non aveva la scrittura in due fasi (salvare prima, inviare poi) che avevano scommesse e prelievi | Critica | `f13f685` Persist the payout before broadcasting it | `test_scheduler.py::test_trigger_payout_persists_before_broadcasting`, `::test_trigger_payout_broadcast_failure_leaves_a_recoverable_row` | RND, TX |
|
|
| B-26 | Un errore transitorio al momento del pagamento, o un riavvio, lasciava il round in `paying_out` per sempre | Critica | `50a43ae` Retry a stuck payout automatically, and log every failure | `test_scheduler.py::test_tick_retries_a_stuck_paying_out_round_with_no_recent_failure`, `::test_trigger_payout_logs_a_failure_when_not_connected` | RND |
|
|
| B-27 | Ogni RBF azzerava l'orologio di abbandono del riconciliatore, che quindi non scattava mai | Critica | `933760e` Decouple the RBF abandon clock from the bump clock | `test_reconcile.py::test_abandons_a_repeatedly_bumped_tx_despite_a_recent_last_broadcast`; `test_broadcast.py::test_should_bump_measures_from_last_broadcast_not_first` | TX |
|
|
| B-28 | Un server Electrum ostile (o un attacco man-in-the-middle) poteva scegliere da solo il vincitore del round | Critica | `0ce0562` Validate Electrum headers and corroborate the draw's block | `test_electrum_listener.py::test_apply_header_rejects_one_that_fails_its_own_pow_target`; `test_scheduler.py::test_wait_for_next_block_retries_after_a_failed_corroboration`; `test_draw.py::test_header_meets_its_own_target_accepts_a_real_mined_header` | ELC, RND |
|
|
| B-29 | Una moneta assente da un solo `listunspent` veniva segnata come spesa all'esterno subito e irreversibilmente | Alta | `e8fdea0` Corroborate an external spend before marking a UTXO gone | `test_electrum_listener.py::test_refresh_user_does_not_mark_when_corroboration_fails`; `test_deposits.py::test_find_utxos_missing_from_skips_a_totally_empty_response` | DEP, ELC |
|
|
| B-30 | Nessuna riconciliazione dei depositi: una sottoscrizione persa significava depositi mai accreditati | Alta | `63df38d` Add a periodic deposit reconciler, and stop losing subscribe tasks | `test_deposit_reconcile.py`; `test_electrum_listener.py::test_address_for_new_user_retains_and_logs_a_failed_subscribe_task` | DEP |
|
|
| B-31 | Alla riconnessione la risottoscrizione di tutti gli utenti era seriale e bloccava l'altezza della catena, e con essa un'estrazione in corso | Alta | `12df041` Resubscribe concurrently and in the background on reconnect | `test_electrum_listener.py::test_subscribe_all_users_bounds_concurrency`, `::test_run_once_keeps_consuming_headers_while_resubscribing` | ELC |
|
|
| B-32 | `bump_fee` poteva ritentare per sempre sotto il minimo di commissione imposto da BIP125, senza tetto al fee rate | Alta | `17c557b` Meet BIP125's relay minimum on every RBF bump, and cap the fee rate | `test_broadcast.py::test_bump_fee_meets_bip125_minimum_when_old_fee_already_exceeds_target`, `::test_bump_fee_refuses_once_at_the_max_fee_rate` | TX |
|
|
| B-33 | Nessuna protezione dai tentativi ripetuti sul login di un portafoglio custodiale | Alta | `16802ca` Throttle login and registration with exponential backoff | `test_auth.py::test_login_locks_out_after_repeated_failures`, `::test_login_failures_against_one_account_do_not_lock_out_another` | SEC |
|
|
| B-34 | Cambio password e reset admin non invalidavano le sessioni già emesse | Alta | `739fc9f` Invalidate existing sessions on password change/reset | `test_users.py::test_change_password_invalidates_the_old_token_but_not_the_new_one`; `test_admin.py::test_admin_resets_user_password`; `test_security.py::test_jwt_decode_defaults_token_version_for_tokens_issued_before_it_existed` | SEC |
|
|
| B-35 | Ogni orario dell'API era senza fuso, e il frontend lo interpretava come ora locale | Media | `bb8b712` Stamp UTC on naive API timestamps before serializing | `test_api_timeutil.py::test_naive_datetime_is_stamped_utc`; `test_users.py::test_me_created_at_is_utc_stamped` | API |
|
|
| B-36 | L'attesa del blocco per l'estrazione non aveva né timeout né visibilità | Media | `7fa26df` Make a stalled draw wait observable | `test_scheduler.py::test_wait_for_next_block_logs_a_stall_audit_entry_past_the_threshold`; `test_rounds_route.py::test_draw_waiting_since_is_exposed_only_while_drawing` | RND, AUD |
|
|
| B-37 | Saldo mostrato e saldo spendibile divergevano, e l'errore «saldo insufficiente» non lo spiegava | Media | `0b44fe6` Distinguish a pending-only balance from a truly insufficient one | `test_withdrawals.py::test_withdrawal_distinguishes_pending_from_truly_insufficient_balance` | WDR, DEP |
|
|
| B-38 | Il tetto globale di 500 abbonati SSE era un modo gratuito per negare il canale in tempo reale a tutti | Media | `702b37b` Cap SSE subscribers per client IP instead of only globally | `test_round_events.py::test_subscribe_evicts_the_same_ips_oldest_connection_past_its_cap` | SEC, ARC |
|
|
| B-39 | SQLite senza WAL né `busy_timeout`, con più attività che scrivono in parallelo | Media | `97545ad` Run SQLite in WAL mode with a busy_timeout | `test_db_base.py` | ARC, DAT |
|
|
| B-40 | `bump_fee` teneva aperta una sessione del database durante N chiamate di rete, e calcolava gli importi da un valore in virgola mobile | Media | `08c566d` Restructure bump_fee into three phases, drop float fee math | `test_broadcast.py::test_bump_fee_holds_no_session_open_during_network_calls` | TX |
|
|
| B-41 | Conferme e riconciliazione dipendevano da `blockchain.transaction.get` in modalità verbose, rifiutata da molti server, e l'abbandono si basava sul confronto testuale del messaggio d'errore | Media | `4124dc0` Check confirmation/existence via scripthash history, not verbose replies | `test_confirmation.py::test_one_unresolvable_candidate_does_not_block_the_others`; `test_reconcile.py`; `test_pending_address.py` | TX, ELC |
|
|
| B-42 | `/docs`, ReDoc e lo schema OpenAPI esposti in produzione, endpoint admin compresi | Bassa | `22e3cfb` Disable Swagger/ReDoc/OpenAPI JSON by default | `test_docs_disabled.py` | SEC |
|
|
| B-43 | Nessun header di sicurezza HTTP (CSP, HSTS, `X-Frame-Options`…) | Bassa | `d60da11` Add baseline HTTP security headers in Caddy | `test_caddyfile_security_headers.py` | SEC |
|
|
| B-44 | README e `docs/running-the-server.md` documentavano un `uvicorn --reload` diretto, contro la regola «solo Docker» | Bassa | `a574db0` Align README and running-the-server.md with the Docker-only policy | `test_docs_no_bare_uvicorn.py` | OPS |
|
|
| B-45 | Endpoint admin di elenco senza limiti validati né paginazione | Bassa | `6045c89` Bound admin list endpoint limits, add status filter to pending-transactions | `test_admin.py::test_admin_list_endpoints_reject_out_of_range_limit`, `::test_admin_pending_transactions_status_filter` | API |
|
|
| B-46 | `secrets.compare_digest` su una stringa con caratteri non ASCII sollevava `TypeError` (500 invece di 403) | Bassa | `31bc9a3` Compare admin token as UTF-8 bytes to avoid TypeError on non-ASCII input | `test_admin.py::test_admin_rejects_non_ascii_token_with_403_not_500` | SEC |
|
|
| B-47 | Colonne `String` senza lunghezza per testi lunghi (`raw_tx_hex`, `payload_json`) | Bassa | `6a90136` Widen raw_tx_hex and payload_json from String to Text | `test_models_text_columns.py` | DAT |
|
|
| B-48 | Nessun limite al numero di monete in ingresso: un indirizzo frammentato erodeva la propria scommessa o il proprio prelievo in commissioni | Bassa | `4c80c1c` Cap the number of inputs a transaction may spend | `test_psbt_builder.py::test_select_utxos_never_exceeds_the_input_cap`; `test_bets.py::test_place_bet_reports_a_too_fragmented_balance_distinctly` | BET, WDR |
|
|
| B-49 | I percorsi di annullamento di scommessa e prelievo non pubblicavano un aggiornamento SSE | Bassa | `e7f844b` Publish an SSE update from the bet/withdrawal rollback paths | `test_bets.py::test_failed_broadcast_publishes_an_sse_update`; `test_withdrawals.py::test_failed_broadcast_publishes_an_sse_update` | BET, WDR, ARC |
|
|
|
|
---
|
|
|
|
## Fuori audit (B-50, B-51)
|
|
|
|
Trovati il 2026-07-31 osservando il sistema in funzione, fra il secondo e il
|
|
terzo audit; non sono mai comparsi in un `BUGS.md` e la gravità non è stata
|
|
registrata. Il terzo audit riparte «dall'ultimo difetto corretto, B-51».
|
|
|
|
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
|
|---|---|---|---|---|---|
|
|
| B-50 | Il banner di vincita appariva prima che `winner_amount_sats` fosse noto, mostrando «Hai vinto! +— PLM» | — | `9207bbc` Don't reveal the win banner before winner_amount_sats is known | *manuale* (solo `app/static/app.js`); nessun marcatore B-50 nel codice | RND, ARC |
|
|
| B-51 | Il saldo in attesa contava due volte il resto di una scommessa o di un prelievo già accreditato dal listener | — | `fe909be` Don't double-count a bet/withdrawal's own change in pending balance | `test_balance.py::test_pending_balance_does_not_double_count_change_already_credited` (senza marcatore B-51) | DEP |
|
|
|
|
---
|
|
|
|
## Audit del 2026-08-03 (B-52 … B-72)
|
|
|
|
Terzo audit completo (264 test verdi al momento dell'audit), aperto con
|
|
`99d7a1e` nel ramo `audit-2026-08-03`, poi confluito in `main`. Ogni difetto è
|
|
stato rimosso da `BUGS.md` nel suo stesso commit di correzione. Due voci
|
|
restano aperte.
|
|
|
|
L'audit ricorda anche un problema **non numerato**, perché già elencato fra i
|
|
limiti accettati: lo stato `drawing` non riprende dopo un riavvio (vedi
|
|
[limiti-noti.md](limiti-noti.md)).
|
|
|
|
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
|
|---|---|---|---|---|---|
|
|
| B-52 | Un round con più di ~50 partecipanti bloccava la piattaforma per sempre: il pagamento usava il limite di 50 monete in ingresso pensato per le transazioni utente | Critica | `025754c` Cap participants per round and give the payout its own input limit | `test_bets.py::test_place_bet_rejects_the_bet_past_the_participant_cap`; `test_payout_builder.py::test_payout_spends_more_utxos_than_a_user_transaction_may`; `test_scheduler.py::test_trigger_payout_pays_a_round_with_more_participants_than_max_tx_inputs` | RND, BET |
|
|
| B-53 | Una scommessa poteva finire nel montepremi ed essere comunque esclusa dall'estrazione | Critica | `64f6229` Close the window where a bet pays into a round it was left out of | `test_bets.py::test_place_bet_refuses_when_the_round_closed_between_the_check_and_the_commit`, `::test_place_bet_rechecks_the_deadline_after_building_the_transaction`; `test_scheduler.py::test_close_and_draw_waits_when_a_bet_appears_after_the_tick_check` | BET, RND |
|
|
| B-54 | `X-Forwarded-For` accettato alla cieca: ogni controllo basato sull'IP era aggirabile | Alta | `907e32e` Make X-Forwarded-For trustworthy instead of attacker-controlled | `test_client_ip.py::test_client_ip_takes_the_last_hop_of_a_forwarded_chain`, `::test_client_ip_ignores_a_client_supplied_prefix`; `test_caddyfile_security_headers.py::test_forwarded_for_is_overwritten_with_the_real_peer` | SEC |
|
|
| B-55 | L'hashing Argon2 girava sull'event loop, bloccando l'intero processo a ogni login | Alta | `9c7befe` Hash and verify passwords off the event loop | `test_security.py::test_hash_password_async_keeps_the_event_loop_free`, `::test_verify_password_async_keeps_the_event_loop_free` | SEC |
|
|
| B-56 | Il dizionario dei contatori del limitatore di tentativi non veniva mai sfoltito | Alta | `ab65728` Bound the rate limiter's bucket dict | `test_rate_limit.py::test_the_dict_is_capped_even_within_one_sweep_interval`, `::test_spent_buckets_are_swept_on_the_next_failure` | SEC |
|
|
| B-57 | Username confrontati distinguendo maiuscole e minuscole, mentre la chiave del limitatore no | Alta | `5772135` Make usernames one case-insensitive namespace | `test_auth.py::test_registration_refuses_a_username_differing_only_by_case`, `::test_login_accepts_the_username_in_any_case`, `::test_the_database_itself_rejects_a_case_variant` | REG, SEC |
|
|
| B-58 | Il limite sulle registrazioni contava i successi come fallimenti ed era solo per IP | Alta | `0aac73e` Bound registrations with a per-IP quota instead of failure backoff | `test_auth.py::test_registration_is_quota_limited_per_ip`, `::test_failed_registrations_do_not_consume_the_quota` | SEC, REG |
|
|
| B-59 | L'accredito dei depositi non era corroborato dagli altri server, a differenza del rilevamento delle spese esterne | Alta | `6246b13` Corroborate deposit credits, not just external spends | `test_electrum_listener.py::test_refresh_user_does_not_credit_a_utxo_only_our_own_server_reports`, `::test_refresh_user_credits_once_the_others_corroborate` | DEP, ELC |
|
|
| B-60 | `POST /admin/bug-reports/{id}/status` non scriveva nulla nel registro di audit | Alta | `77e07e8` Audit-log bug report status changes | `test_bug_reports.py::test_status_change_is_audit_logged`, `::test_setting_the_status_it_already_has_logs_nothing` | AUD |
|
|
| B-61 | Le modifiche alla configurazione si applicavano retroattivamente al round in corso | Media | `37cc5ee` Snapshot a round's timing when it opens | `test_scheduler.py::test_tick_ignores_a_config_duration_edited_mid_round`; `test_rounds_service.py::test_a_new_round_snapshots_the_current_config_timing` | RND |
|
|
| B-62 | «Preleva tutto» produceva sempre una transazione senza resto, quindi impossibile da accelerare con RBF | Media | `8dd913e` Always keep a change output, so every tx stays fee-bumpable | `test_psbt_builder.py::test_build_signed_transaction_refuses_an_amount_that_would_leave_no_change`; `test_withdrawals.py::test_full_balance_withdrawal_keeps_a_bumpable_change_output` | TX, WDR, BET |
|
|
| B-63 | Subito dopo la connessione, con `tip_height == 0`, un'estrazione poteva usare un blocco precedente alla chiusura | Media | `8a0ebec` Never let a draw be seeded by a block that predates the close | `test_scheduler.py::test_wait_for_next_block_never_seeds_the_draw_from_a_pre_close_block`; `test_electrum_listener.py::test_run_once_publishes_the_client_only_once_the_tip_is_known` | RND, ELC |
|
|
| B-64 | `_apply_header` accettava un header alla stessa altezza senza controllo di concatenazione | Media | `c0314e2` Never swap the tip's hash sideways, and never split it from its height | `test_electrum_listener.py::test_apply_header_ignores_a_competing_header_at_the_current_tip_height`, `::test_apply_header_ignores_one_carrying_no_hex` | ELC |
|
|
| B-65 | `/rounds/current` contava i partecipanti non confermati, che estrazione e pagamento invece escludono | Media | `c4b2dc3` Advertise the jackpot that will actually be paid | `test_rounds_route.py::test_advertised_jackpot_covers_only_the_bets_that_will_be_paid` | RND, API |
|
|
| B-66 | Nulla impediva di aprire round senza `fee_address` configurato, rendendoli impagabili | Media | `23d5879` Refuse to open a round that could not pay its winner | `test_rounds_service.py::test_withholds_new_round_while_no_fee_address_is_configured`, `::test_a_round_in_progress_survives_the_fee_address_being_cleared`; `test_rounds_route.py::test_lottery_configured_flags_a_missing_fee_address` | RND |
|
|
| B-67 | `/qr/{address}` non autenticato, sincrono e validato solo nella forma | Media | `5f6abe5` Validate and bound the QR endpoint | `test_qr.py` | API, SEC |
|
|
| B-68 | CLAUDE.md e README descrivevano uno stato che il codice aveva superato | Bassa | `162ceed` Describe the code the docs actually ship with | `test_docs_current.py` | ARC |
|
|
| B-69 | Commenti nel codice non più veri | Bassa | `666cb1a` Retire in-code comments that outlived what they described | `test_code_comments.py` | ARC |
|
|
| B-70 | Il diagramma diceva che prelievo e scommessa in corso non potevano coesistere, ma il codice non lo impone (e non deve) | Bassa | `526a649` Say what bets and withdrawals actually exclude | `test_withdrawals.py::test_withdrawal_is_allowed_while_a_bet_is_still_unconfirmed`, `::test_the_docs_no_longer_claim_bets_and_withdrawals_are_exclusive` | WDR |
|
|
| B-71 | `.env` punta `MASTER_KEY_PATH` a una seconda copia della chiave master (e `DATABASE_URL` a un secondo database) invece che ai file in `./data/` letti dal container | Bassa | **aperto** | — | OPS, SEC |
|
|
| B-72 | `docs/setup.md` presenta ancora l'installazione come «in locale o via Docker», contro la regola «solo Docker» | Bassa | **aperto** | — | OPS |
|
|
|
|
Note:
|
|
|
|
- `8f3cdcb` e `421fe72` sono commit di sola contabilità di `BUGS.md` (registrano
|
|
gli hash delle correzioni di B-53 e B-54), non correzioni.
|
|
- Il test di B-70 sulla documentazione legge la sezione «Scommessa e prelievo
|
|
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)
|
|
|
|
Quarta revisione, condotta sulla specifica in `docs/` invece che sul solo
|
|
codice: quattro analisi indipendenti (meccanica della coin, macchina a stati
|
|
del denaro, operatività, coerenza dei documenti), con le affermazioni critiche
|
|
verificate nel codice. B-98 è emerso durante la stesura. Il comportamento
|
|
atteso di ogni voce è già scritto nella specifica, nei requisiti marcati
|
|
*da implementare*; le decisioni e l'ordine di lavoro sono in
|
|
[piano-correzioni.md](piano-correzioni.md), il dettaglio tecnico in
|
|
[BUGS.md](../BUGS.md).
|
|
|
|
La stessa revisione ha giudicato sottostimata la gravità di B-71: con la
|
|
chiave duplicata, la procedura di rotazione della chiave di cifratura
|
|
cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
|
|
|
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
|
|---|---|---|---|---|---|
|
|
| B-73 | L'estrazione dipende solo dall'hash del blocco: un minatore con biglietti o il server Electrum attivo possono pilotarla; ordine dei partecipanti non verificabile | Critica | *da registrare* | `test_draw.py`, `test_draw_proof.py`, `test_scheduler.py`, `test_electrum_listener.py`, `test_rounds_service.py`, `test_migrations.py` | RND, ELC |
|
|
| B-74 | Header accettati senza controllo della difficoltà minima; un salto di più blocchi non viene collegato; una riorganizzazione di un blocco chiude la sessione | Critica | *da registrare* | `test_electrum_listener.py`, `test_draw.py` | ELC |
|
|
| B-75 | Una risposta Electrum oltre 64 KiB chiude la connessione, e la cronologia dell'indirizzo del montepremi la supera dopo ~800 transazioni | Critica | *da registrare* | `test_electrum_client.py`, `test_deposits.py`, `test_psbt_builder.py` | ELC, TX |
|
|
| B-76 | Un invio ambiguo (timeout, connessione caduta) viene annullato come se fosse rifiutato | Critica | *da registrare* | `test_broadcast_outcome.py`, `test_electrum_client.py` | TX, BET, WDR |
|
|
| B-77 | Si ricorda solo l'ultimo txid di una transazione accelerata, e la sostituzione viene inviata prima di essere salvata | Critica | *da registrare* | `test_tx_versions.py`, `test_migrations.py`, `test_broadcast.py` | TX |
|
|
| B-78 | Una transazione viene abbandonata sulla base del silenzio di un solo server | Critica | *da registrare* | `test_reconcile_proof.py`, `test_reconcile.py`, `test_pause_reason.py`, `test_migrations.py` | TX, ELC |
|
|
| B-79 | Il pagamento di norma non ha resto, quindi non si può accelerare e può bloccare la lotteria | Critica | **aperto** | — | RND, TX |
|
|
| B-80 | Un solo indirizzo del montepremi per tutti i round | Critica | *da registrare* | `test_round_pool_address.py`, `test_pending_address.py`, `test_scheduler.py` | RND, BET, WDR |
|
|
| B-81 | `drawing` resta bloccato dopo qualsiasi eccezione, non solo dopo un riavvio | Critica | **aperto** | — | RND |
|
|
| B-82 | Un deposito annullato da una riorganizzazione dopo la scommessa rende il round impagabile; coinbase accreditate a 1 conferma | Critica | **aperto** | — | DEP, RND |
|
|
| B-83 | Conferme e reintegri si basano solo sul server attivo | Alta | *da registrare* | `test_confirmation_proof.py`, `test_confirmation.py` | ELC, TX, DEP |
|
|
| B-84 | Il quorum può ridursi a un solo server o a nessuno senza avvisi; certificati TLS non verificati | Alta | *da registrare* | `test_electrum_client.py`, `test_electrum_listener.py` | ELC, SEC |
|
|
| B-85 | Le riorganizzazioni della catena non vengono né seguite né segnalate | Alta | *da registrare* | `test_electrum_listener.py` | ELC, RND |
|
|
| B-86 | Un server connesso ma fermo non viene mai sostituito | Alta | *da registrare* | `test_electrum_listener.py` | ELC, OPS |
|
|
| B-87 | L'RBF può lasciare un resto sotto la soglia di dust | Alta | **aperto** | — | TX |
|
|
| B-88 | Le transazioni SQLite non sono quelle che il codice presuppone; `synchronous=NORMAL` può perdere gli ultimi commit | Alta | *da registrare* | `test_sqlite_transactions.py`, `test_db_base.py` | DAT, ARC |
|
|
| B-89 | Problemi della chiave rilevati solo al primo uso; gli script accettano qualunque xprv e scrivono file leggibili da tutti | Alta | **aperto** | — | SEC, OPS |
|
|
| B-90 | Il ripristino di un backup riapre round già pagati e riassegna indici di derivazione | Alta | **aperto** | — | OPS, REG |
|
|
| B-91 | Container eseguito come root, permessi larghi, dipendenze e immagini non fissate | Alta | **aperto** | — | SEC, OPS |
|
|
| B-92 | Nessuna verifica di salute o prontezza, stato Electrum invisibile, audit non filtrabile, log incompleti | Media | **aperto** | — | OPS, AUD, API |
|
|
| B-93 | Deploy senza arresto ordinato, rollback impossibile, `SITE_ADDRESS` che torna a `localhost` | Media | **aperto** | — | OPS |
|
|
| B-94 | Cambio password, login e bug report senza limiti adeguati; errori non strutturati in alcune risposte | Media | **aperto** | — | SEC, API |
|
|
| B-95 | Costo della scommessa e `fee_address` letti dalla configurazione corrente invece che fotografati all'apertura | Media | **aperto** | — | RND, BET |
|
|
| 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 |
|
|
|
|
## 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.
|
|
- **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 (3 critici,
|
|
4 alti, 6 medi), da implementare nell'ordine di
|
|
[piano-correzioni.md](piano-correzioni.md).
|
|
- **Prossimo numero libero:** `B-99`.
|