Files
plm-lottery/docs/storico-bug.md
T
davideandClaude Opus 5.5 61efbb0e37 Replace flowchart/ with a traceable specification in docs/
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>
2026-09-28 12:16:01 +02:00

214 lines
27 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-73`.**
- **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 [decisioni-e-limiti.md](decisioni-e-limiti.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 |
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 e B-72).
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
[decisioni-e-limiti.md](decisioni-e-limiti.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
[decisioni-e-limiti.md](decisioni-e-limiti.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.
## 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.
- **Aperti:** B-71, B-72 (entrambi di gravità bassa, entrambi operativi o di
documentazione).
- **Prossimo numero libero:** `B-73`.