Files

261 lines
34 KiB
Markdown
Raw Permalink Normal View History

# 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 | corretto con l'allineamento dei file e B-89 | `test_master_key_check.py` | OPS, SEC |
| B-72 | `docs/setup.md` presenta ancora l'installazione come «in locale o via Docker», contro la regola «solo Docker» | Bassa | *da registrare* | — (documentazione) | 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-100)
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 | *da registrare* | `test_payout_builder.py`, `test_broadcast.py`, `test_scheduler.py`, `test_rounds_service.py`, `test_admin.py`, `test_migrations.py` | 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 | *da registrare* | `test_draw_proof.py` | RND |
| B-82 | Un deposito annullato da una riorganizzazione dopo la scommessa rende il round impagabile; coinbase accreditate a 1 conferma | Critica | *da registrare* | `test_deposit_confirmations.py`, `test_pool_verification.py`, `test_electrum_listener.py`, `test_admin.py`, `test_users.py` | 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 | *da registrare* | `test_rbf_dust.py`, `test_broadcast.py` | 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 | *da registrare* | `test_master_key_check.py` | SEC, OPS |
| B-90 | Il ripristino di un backup riapre round già pagati e riassegna indici di derivazione | Alta | *da registrare* | `test_restore_gap.py` | OPS, REG |
| B-91 | Container eseguito come root, permessi larghi, dipendenze e immagini non fissate | Alta | *da registrare* | `test_container_hardening.py`, `test_config.py` | SEC, OPS |
| B-92 | Nessuna verifica di salute o prontezza, stato Electrum invisibile, audit non filtrabile, log incompleti | Media | *da registrare* | `test_observability.py` | 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 | *da registrare* | `test_abuse_limits.py` | SEC, API |
| B-95 | Costo della scommessa e `fee_address` letti dalla configurazione corrente invece che fotografati all'apertura | Media | *da registrare* | `test_round_snapshot.py`, `test_admin.py`, `test_rounds_route.py`, `test_scheduler.py` | 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 | *da registrare* | `test_state_consistency.py` | 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 | *da registrare* | `test_psbt_builder.py` | 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-72, 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-89, B-90, B-91, B-92, B-94, B-95, B-96, B-98, B-99, B-100.
- **Aperti:** dalla revisione del 2026-09-28, B-93 (0 critici,
0 alti, 1 medio), da implementare nell'ordine di
[piano-correzioni.md](piano-correzioni.md).
- **Prossimo numero libero:** `B-101`.