# Limiti noti Questo documento raccoglie i **limiti noti (`LIM`)**: difetti di copertura o di robustezza accettati consapevolmente, con l'impatto, ciò che oggi li attenua e cosa servirebbe per chiuderli. Le **decisioni di progetto** non sono più qui: ognuna è un ADR in [adr/](adr/README.md). Un limite noto **non è un bug**: un bug è un comportamento che contraddice la specifica e prende un numero `B-nn` ([storico-bug.md](storico-bug.md)). Se si decide di chiudere un limite, si aggiorna prima la voce `LIM` qui (o la si barra come rimossa) e poi il requisito del documento di dominio, secondo [processo-di-modifica.md](processo-di-modifica.md). Un limite che il [piano delle correzioni](piano-correzioni.md) chiuderà porta `*Stato:* da chiudere con B-nn`, e *Per chiuderlo* descrive il comportamento atteso; quando la correzione entra nel codice, il limite si barra come `~~[LIM-nn]~~ Rimosso: chiuso da B-nn`. Formato di ogni voce: descrizione, *Impatto*, *Mitigazione attuale*, *Per chiuderlo*, e il codice coinvolto. Formato di ogni voce: descrizione, *Impatto*, *Mitigazione attuale*, *Per chiuderlo*, e il codice coinvolto. ### Quali limiti bloccano il funzionamento non presidiato La revisione del 2026-09-28 ha mostrato che la tabella precedente era incompleta: oltre a `drawing` e all'RBF, bloccavano il funzionamento non presidiato le falle critiche B-73 … B-82 (estrazione manipolabile, risposte oltre 64 KiB, abbandoni su un solo server, pagamento senza riserva, deposito spendibile due volte). La colonna *Dopo il piano* dice cosa resta una volta implementato il [piano delle correzioni](piano-correzioni.md). | Limite | Blocca il non presidiato oggi? | Dopo il piano | |---|---|---| | [LIM-01] `drawing` non riprende dopo un errore o un riavvio | **Sì** — è lo stato che resta fermo con denaro in gioco | Chiuso da B-81 | | [LIM-02] RBF di una sola forma (e pagamento senza resto) | **Sì, in caso di congestione della rete** | Chiuso da B-79 e B-87 | | [LIM-03] Attesa in `closing` senza limite di tempo | **Sì, in caso di congestione** | Resta senza limite di tempo per scelta ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)); con B-78 e B-87 finisce quando la rete decide | | [LIM-04] Prelievo e RBF mai provati in rete reale | **Sì, come verifica** | Da chiudere con la prova su rete reale ([operativita.md](operativita.md#prova-su-rete-reale)) prima di aprire al pubblico | | [LIM-10] Riorganizzazioni non gestite | **Sì** — una scommessa sparita rende il round impagabile | Chiuso da B-82 e B-85; resta [LIM-24] | | [LIM-11], [LIM-12] Quorum facoltativo e parziale | **Sì** — un solo server decide conferme e abbandoni | Chiusi da B-83 e B-84 (resta la lettura delle monete del montepremi, B-82) | | [LIM-13] Estrazione manipolabile | **Sì** — per un giocatore che mina | Chiuso da B-73; resta [LIM-25] | | [LIM-14] Header senza difficoltà minima | **Sì** — un header falso congela ogni estrazione | Chiuso da B-74 per il blocco; resta il costo basso di un header a difficoltà minima | | [LIM-06] Token admin unico | No per il funzionamento, ma è il rischio di sicurezza più ampio | Invariato | | tutti gli altri | No | — | ### Robustezza del ciclo di vita - **[LIM-01]** **Lo stato `drawing` non riprende dopo un errore o un riavvio.** Lo scheduler gestisce `open`, `closing` e `paying_out` a ogni giro e salta `drawing`; l'attesa del blocco (`_wait_for_next_block`) vive solo dentro la chiamata a `_close_and_draw` che l'ha avviata, con `tip_at_close` e la lista dei partecipanti in memoria. Non serve un riavvio: **qualunque eccezione** durante l'attesa (un `database is locked` scrivendo una riga di audit, un header malformato) la interrompe, e nessuno la riprende. Il terzo audit (2026-08-03) lo ha ribadito come problema **critico**, la revisione del 2026-09-28 lo ha numerato (B-81). - *Impatto:* il round resta in `drawing` per sempre con il montepremi dentro; poiché un nuovo round si apre solo a round chiuso, **l'intera lotteria si ferma**. Con `restart: unless-stopped` un crash in quella finestra è esattamente il caso che non si ripara da solo. - *Mitigazione attuale:* l'attesa è osservabile — log periodici, evento `draw_stalled` dopo 360s (e poi a ogni intervallo), `draw_waiting_since` su `GET /rounds/current`. Il recupero è manuale (vedi [operativita.md](operativita.md)). - *Stato:* da chiudere con B-81. - *Per chiuderlo:* al passaggio in `drawing` si salvano `draw_height`, `participants_hash` e la posizione di estrazione di ogni partecipante; `_tick` rientra nell'attesa per un round in `drawing`, in modo idempotente. Con l'altezza fissa e il seme impegnato di [ADR-24](adr/ADR-24-l-estrazione-commit-reveal-fra-server.md)/[ADR-25](adr/ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md), un'estrazione ripresa dà sempre lo stesso vincitore. *Codice:* `app/rounds/scheduler.py:RoundScheduler._tick` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` · `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` - **[LIM-02]** **L'RBF gestisce una sola forma di transazione**: un'unica uscita di resto verso il mittente, abbastanza grande da assorbire l'aumento. Non c'è il ripiego «aggiungi un'altra moneta». Ogni aumento costa almeno 1 sat/vB sull'intera dimensione della transazione (141 sat per una transazione con un ingresso e due uscite) e si toglie dal resto; quando l'aumento supera il resto, `bump_fee` solleva `RbfError`. Inoltre `bump_fee` **non controlla che il resto ridotto resti sopra la soglia di polvere**: con un resto vicino al minimo di 294 sat ([ADR-19](adr/ADR-19-ogni-transazione-utente-conserva-uscita-resto.md)) già il primo aumento produce un'uscita sotto soglia, che le regole di inoltro standard rifiutano. **Il pagamento del round, nel caso normale, non ha alcun resto** (spende esattamente il montepremi del round, e un resto sotto soglia va alla commissione), quindi non è accelerabile affatto. - *Impatto:* con la rete congestionata una transazione a 1 sat/vB può restare in mempool indefinitamente. Poiché il riconciliatore abbandona solo ciò che il server **non conosce**, una transazione ferma ma presente in mempool non viene mai abbandonata: una scommessa così blocca il round in `closing`, un pagamento così lo blocca in `paying_out`. `RbfBumper` registra un errore ogni 30s. - *Mitigazione attuale:* resto minimo garantito sulle transazioni utente; tetto al fee rate (`MAX_FEE_RATE_SAT_VB`); abbandono dopo 6 ore per ciò che sparisce dalla rete; ritentativo automatico del pagamento dopo un abbandono. - *Stato:* da chiudere con B-79 e B-87. - *Per chiuderlo:* ogni pagamento porta un'uscita di riserva che l'accelerazione riduce ([ADR-17](adr/ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md), B-79); un aumento che lascerebbe un resto sotto `DUST_LIMIT_SATS` elimina il resto e ne versa il valore in commissione, e quella sostituzione è l'ultima ([ADR-19](adr/ADR-19-ogni-transazione-utente-conserva-uscita-resto.md), B-87); ogni `RbfError` e ogni sostituzione rifiutata scrivono `rbf_bump_failed` in audit. Resta accettato, dopo il piano: nessun ripiego «aggiungi un'altra moneta» né CPFP; una transazione che ha esaurito resto o riserva e non conferma nemmeno al tetto del fee rate resta in attesa, visibile, e non viene abbandonata ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)). Il docstring di `bump_fee` cita ancora come esempio «una scommessa senza resto», caso che dopo B-62 non esiste più. *Codice:* `app/tx/broadcast.py:bump_fee` · `app/wallet/psbt_builder.py:build_payout_transaction` · `app/tx/reconcile.py:reconcile_once` - **[LIM-03]** **L'attesa in `closing` non ha un limite proprio.** Il round aspetta ogni scommessa `building`/`broadcast`. A porre fine all'attesa di una scommessa che non conferma è la sua conferma (una qualsiasi delle sue versioni) oppure il riconciliatore, ma solo con la prova che non è partita ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)); una scommessa ferma ma presente in mempool trattiene il round senza limite ([LIM-02]). - *Impatto:* un round può restare chiuso ma non estratto per ore; nessun nuovo round si apre nel frattempo. - *Mitigazione attuale:* lo stato è visibile (`closing`) e il pannello «Transazioni pendenti» mostra la scommessa che trattiene il round. - *Stato:* B-78 e B-77 chiusi; resta da chiudere B-87 (resto minimo), e per scelta l'assenza di un limite di tempo. - *Per chiuderlo:* con B-78 l'attesa finisce quando la scommessa conferma (una qualsiasi delle sue versioni, B-77) o quando c'è la prova che non confermerà mai ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)), non dopo un tempo fisso; con B-87 la scommessa resta accelerabile fino all'eliminazione del resto. Un **limite di tempo resta escluso per scelta**: escludere e rimborsare una scommessa lenta libererebbe fondi senza prova, che è ciò che [ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md) vieta. Resta quindi accettato che una scommessa ferma al tetto del fee rate trattenga il round. *Codice:* `app/rounds/scheduler.py:RoundScheduler._tick` · `app/tx/reconcile.py:_is_due` - **[LIM-04]** **Prelievo e accelerazione RBF non sono mai stati inviati in una rete reale.** Sono coperti da test unitari, rami di errore e annullamento compresi; il resto del flusso (registrazione, deposito, scommessa, un ciclo completo di round con pagamento) è stato verificato in mainnet. - *Impatto:* un errore di costruzione o di firma specifico della rete reale emergerebbe al primo prelievo di un utente vero. - *Stato:* da chiudere con la fase 7 del [piano](piano-correzioni.md#fase-7--prova-su-rete-reale). - *Per chiuderlo:* un prelievo e un RBF reali in mainnet, con verifica dell'aritmetica sulla transazione inviata, dentro la prova con importi piccoli e guasti provocati di [operativita.md](operativita.md#prova-su-rete-reale). *Codice:* `app/withdrawals/service.py:request_withdrawal` · `app/tx/broadcast.py:bump_fee` - **[LIM-05]** **Nessun test di integrazione con una connessione Electrum vera.** `tests/integration/` è vuota; la verifica dal vivo è stata tutta manuale (`scripts/electrum_smoke_test.py`, script ad hoc, transazioni reali). - *Impatto:* le differenze di comportamento fra implementazioni di server Electrum (formati di risposta, errori) emergono solo in produzione. - *Per chiuderlo:* lo strato di test di guasto e di integrazione previsto in [architettura.md](architettura.md#test) (server Electrum simulati che rispondono in ritardo, tagliano la connessione, mandano risposte enormi o catene divergenti), più la prova su rete reale di [operativita.md](operativita.md#prova-su-rete-reale). Nessuna delle falle critiche del 2026-09-28 sarebbe stata trovata dai test attuali. *Codice:* `tests/integration/__init__.py` · `scripts/electrum_smoke_test.py` ### Sicurezza e amministrazione - **[LIM-06]** **Un unico token admin condiviso, senza identità per operatore.** Il registro di audit dice *cosa* è cambiato (per esempio `config_updated` con prima/dopo) ma non *chi*. Il token dà accesso a elenco utenti, esportazione delle chiavi private, reset delle password e storico. Non c'è limite ai tentativi sul token. - *Impatto:* una fuga del token ha il raggio d'azione più ampio del sistema, ed è impossibile attribuire un'azione a una persona. - *Mitigazione attuale:* confronto a tempo costante, pannello chiuso se il token è vuoto, ogni azione sensibile in audit, `/admin` non collegato dalla pagina pubblica. Dettagli in [sicurezza.md](sicurezza.md). - *Per chiuderlo:* account amministrativi individuali (con l'identità scritta in ogni voce di audit) e limitazione dei tentativi sul token. *Codice:* `app/api/routes/admin.py:require_admin` · `app/audit/log.py:write_audit_log` - **[LIM-07]** **Nessuna limitazione di frequenza su scommesse, prelievi, endpoint admin, cambio password e segnalazioni di bug.** Solo login e registrazione sono limitati ([ADR-41](adr/ADR-41-limitazione-tentativi-su-login-registrazione.md)); il canale SSE ha tetti di concorrenza (500 globali, 5 per IP), non di frequenza. - *Impatto:* un utente autenticato può martellare scommesse e prelievi (il blocco per-utente li serializza, non li limita); il cambio password permette di provare password attuali senza ritardi a chi possiede già un token valido; le segnalazioni anonime possono riempire il database. - *Stato:* da chiudere in parte con B-94 (cambio password limitato e registrato, segnalazioni di bug limitate per IP, lunghezza massima di 256 caratteri sui campi delle credenziali). Scommesse, prelievi ed endpoint admin restano senza limite di frequenza anche dopo il piano. - *Per chiuderlo:* un limitatore per utente e per IP sugli endpoint interessati, sullo stesso modello di `RateLimiter`/`RollingQuota`. *Codice:* `app/api/routes/bets.py:create_bet` · `app/api/routes/withdrawals.py:create_withdrawal` · `app/api/routes/users.py:change_password` · `app/api/routes/bug_reports.py:create_bug_report` - **[LIM-08]** **Nessun registro di sicurezza lato utente.** Registrazione, login, login falliti e cambio password dell'utente non scrivono voci di audit (le scrive solo il reset da parte dell'admin). - *Impatto:* dopo una compromissione non c'è traccia di quando e da dove sia avvenuto l'accesso. - *Stato:* da chiudere in parte con B-92 (`user_registered`, con indice di derivazione e indirizzo) e B-94 (`password_changed`). Login riusciti e falliti restano senza voce di audit dopo il piano. - *Per chiuderlo:* eventi di audit per questi casi (vedi [audit-log.md](audit-log.md)). *Codice:* `app/auth/routes.py:login` · `app/api/routes/users.py:change_password` - **[LIM-09]** **Il JWT sta in `localStorage` e la CSP ammette script inline.** Una falla XSS permetterebbe di leggere il token. - *Mitigazione attuale:* CSP `default-src 'self'`, `frame-ancestors 'none'`, escape dei valori controllati dall'utente nelle pagine; sessioni revocabili ([ADR-36](adr/ADR-36-password-argon2-sessioni-jwt-hs256-24.md)). - *Per chiuderlo:* togliere gestori e stili inline dalle SPA per poter eliminare `'unsafe-inline'`, oppure spostare la sessione in un cookie `HttpOnly`. *Codice:* `Caddyfile` · `app/static/app.js` - **[LIM-26]** **La piattaforma è custodiale: gli utenti devono fidarsi dell'operatore.** Tutte le chiavi stanno sul server ([ADR-01](adr/ADR-01-piattaforma-custodiale-tutte-chiavi-generate-tenute.md)): l'operatore (o chi compromette il server) può spendere ogni fondo, leggere `server_seed` prima della rivelazione e scegliere il vincitore con un minatore ([LIM-25]). Nessuna misura del piano lo cambia. - *Impatto:* il rischio di perdita totale dei fondi depositati dipende dalla sicurezza del server e dall'onestà dell'operatore. - *Mitigazione dopo il piano:* chiave cifrata a riposo con la chiave di cifratura in un file separato (B-91), container non root, verifica della chiave all'avvio (B-89), prova pubblica di ogni estrazione ([ADR-24](adr/ADR-24-l-estrazione-commit-reveal-fra-server.md)), audit di ogni azione sensibile. Modello di minaccia in [sicurezza.md](sicurezza.md#modello-di-fiducia). - *Per chiuderlo:* un disegno non custodiale, scartato in [ADR-01](adr/ADR-01-piattaforma-custodiale-tutte-chiavi-generate-tenute.md). *Codice:* `app/wallet/hd.py:_load_account_key` · `app/wallet/keystore.py:decrypt_xprv` ### Fiducia nella rete - **[LIM-10]** **Riorganizzazioni non gestite, depositi a 1 conferma.** Un deposito accreditato a 1 conferma può sparire con una riorganizzazione; la punta della catena non regredisce mai, un header concorrente alla stessa altezza è ignorato e nessuna riorganizzazione viene rilevata. - *Impatto:* il caso grave è una scommessa **già confermata** che sparisce: un utente scommette con un deposito a 1 conferma e, se perde, sostituisce il deposito con una catena privata. La scommessa non è più in catena, ma il partecipante resta `confirmed` e contato, e il pagamento fallisce per sempre con «monete del montepremi insufficienti», con il denaro di tutti gli altri giocatori bloccato. Anche le uscite coinbase sono accreditate a 1 conferma, mentre maturano a 120. - *Mitigazione attuale:* una moneta che sparisce dall'elenco del server viene, previo quorum, segnata come spesa all'esterno e tolta dal saldo; se riappare viene ripristinata. Una scommessa **non ancora inviata** costruita su una moneta scomparsa è rifiutata dalla rete e annullata; per una scommessa già confermata non esiste alcuna mitigazione (la versione precedente di questa voce diceva il contrario). - *Stato:* da chiudere con B-82 e B-85. - *Per chiuderlo:* depositi a `deposit_confirmations` conferme e coinbase a 120 ([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)); riorganizzazioni rilevate, registrate e, se profonde, con pausa; monete del round verificate prima del pagamento, con i partecipanti spariti esclusi ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)). Resta accettato il 51% ([LIM-24]). *Codice:* `app/deposits/service.py:find_utxos_missing_from` · `app/deposits/service.py:reinstate_reappeared_utxos` · `app/electrum/listener.py:ElectrumListener._apply_header` - **[LIM-11]** **I server del quorum li sceglie l'operatore.** Il server rifiuta di partire con meno di 2 server di riserva e ogni corroborazione richiede almeno 2 risposte ([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md), B-84), ma più server controllati dalla stessa parte contano come voci indipendenti. I certificati TLS non sono verificati contro un'autorità; si possono fissare per endpoint (`host:porta:pin=`, `ELECTRUM_CERT_PIN`). - *Impatto:* chi controlla la maggioranza dei server configurati (o raggiungibili) può scegliere il vincitore o alterare i saldi mostrati. - *Mitigazione attuale:* quorum minimo obbligatorio; meno di 2 risposte → la corroborazione **fallisce** (una rete irraggiungibile non prova nulla); pin facoltativo del certificato. Il vecchio caso «senza riserve tutto passa» e «una sola risposta decide» è chiuso da B-84. - *Per chiuderlo:* non si chiude: resta una scelta dell'operatore usare server indipendenti fra loro. *Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority` - **[LIM-12]** **Non tutte le letture di rete sono corroborate.** Passano dal solo server attivo: l'elenco delle monete del montepremi usato per il pagamento. - *Impatto:* un server ostile potrebbe far scegliere al pagamento monete che non esistono (il pagamento verrebbe rifiutato dalla rete) o nasconderne di vere (il pagamento fallirebbe e verrebbe ritentato). - *Mitigazione attuale:* l'abbandono richiede una prova ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)) e l'invio arriva a tutti i server; un invio basato su monete inesistenti è rifiutato dalla rete. - *Stato:* da chiudere con B-82 (monete del round prima del pagamento); conferme e ripristini sono chiusi da B-83. - *Per chiuderlo:* una conferma vale quando il quorum restituisce una prova merkle valida (`blockchain.transaction.get_merkle`) contro un header che possediamo; il ripristino richiede lo stesso quorum di un accredito; l'abbandono richiede la prova di [ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md); le monete del round si verificano con il quorum prima di costruire il pagamento ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)); ogni transazione accettata dal server attivo viene inviata anche a tutti i server di riserva. *Codice:* `app/tx/confirmation.py:poll_once` · `app/tx/reconcile.py:reconcile_once` · `app/deposits/service.py:reinstate_reappeared_utxos` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` - **[LIM-13]** **L'estrazione attuale è manipolabile da chi produce o trasmette i blocchi.** - *Minatore:* il seme è il solo hash del blocco, quindi un minatore con biglietti in gioco può trattenere i blocchi finché uno lo fa vincere. Non è un'ipotesi remota: la potenza di calcolo di PLM (~22 TH/s) si può affittare, e scartare qualche blocco costa solo le ricompense perse. - *Server attivo:* può scegliere quale blocco reale usare semplicemente non annunciandone uno; la corroborazione verifica che il blocco esista, non che sia il primo dopo la chiusura, e dopo una corroborazione fallita si passa comunque al blocco successivo. - *Verificabilità:* l'ordine dei partecipanti è l'orario di invio registrato dal server, un dato non pubblico; un giocatore non può ricalcolare da solo l'esito (il commento di `draw_winner` dice il contrario). - *Pareggi:* due scommesse con lo stesso `broadcast_at` non hanno un criterio secondario di ordinamento, quindi l'estrazione non è riproducibile. - *Distorsione del modulo:* un seme da 256 bit modulo al più 400 partecipanti introduce una distorsione inferiore a 400/2²⁵⁶, accettata anche dopo il piano. - *Stato:* da chiudere con B-73. - *Per chiuderlo:* il commit-reveal di [ADR-24](adr/ADR-24-l-estrazione-commit-reveal-fra-server.md) con l'altezza fissa e le conferme di [ADR-25](adr/ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md): seme che dipende da un segreto impegnato all'apertura, partecipanti ordinati per txid e fissati da un'impronta, prova pubblica. Resta accettata la collusione fra operatore e minatore ([LIM-25]). *Codice:* `app/rounds/draw.py:draw_winner` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` - **[LIM-14]** **La validazione degli header non ricostruisce la difficoltà reale.** Oggi il bersaglio non è confrontato con il limite di rete: un server può dichiarare `bits=0x207fffff` e l'header passa **senza alcun lavoro** (la versione precedente di questa voce parlava di «hardware modesto», sottostimando il problema). La concatenazione si verifica solo per un avanzamento di un blocco esatto, quindi un header falso a un'altezza enorme blocca `tip_height`, che non regredisce, e con esso ogni estrazione. - *Mitigazione attuale:* la corroborazione del blocco usato per l'estrazione ([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md), [ADR-25](adr/ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md)); non protegge dal blocco di `tip_height`. - *Stato:* da chiudere con B-74. - *Per chiuderlo:* bersaglio non più facile di `powLimit` e catena verificata su ogni salto ([ADR-26](adr/ADR-26-validazione-degli-header-concatenata-senza-ricalcolo.md)). Resta accettato dopo il piano: senza il ricalcolo della difficoltà, un header a difficoltà minima (`powLimit`, in media 2³² hash) costa una frazione di secondo a un miner ASIC e pochi secondi a una GPU, quindi un server ostile può ancora produrre una breve catena falsa concatenata alla nostra. A neutralizzarla sono il quorum sull'header di estrazione ([ADR-25](adr/ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md)), il segreto del commit-reveal ([ADR-24](adr/ADR-24-l-estrazione-commit-reveal-fra-server.md)) e il rilevamento delle riorganizzazioni ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)); una validazione completa con le regole di ricalcolo della difficoltà di PLM resta fuori scopo. *Codice:* `app/rounds/draw.py:header_meets_its_own_target` · `app/electrum/listener.py:ElectrumListener._apply_header` - **[LIM-15]** **La sottoscrizione di un nuovo indirizzo è a sforzo singolo.** Se fallisce subito dopo la registrazione, l'indirizzo resta non osservato fino alla riconnessione successiva o al giro di `DepositReconciler` (300s, che attende prima di partire). - *Impatto:* un deposito fatto subito dopo la registrazione può tardare fino a cinque minuti. *Codice:* `app/electrum/listener.py:ElectrumListener.address_for_new_user` · `app/deposits/reconcile.py:DepositReconciler` - **[LIM-24]** **Un attacco al 51% su PLM resta possibile: è contenuto, non impedito.** Chi controlla la maggioranza della potenza di calcolo (che per PLM, ~22 TH/s, si può affittare) può riscrivere più di 6 blocchi e annullare depositi già accreditati o scommesse già confermate. Riguarda chiunque usi PLM, non solo la lotteria. - *Impatto:* un deposito accreditato e poi annullato da una riorganizzazione più profonda di `deposit_confirmations` è denaro che la piattaforma ha già contato; può essere stato scommesso o prelevato. - *Mitigazione dopo il piano:* 6 conferme sui depositi ([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)) rendono l'attacco costoso quanto sei blocchi dell'intera rete; il rilevamento e la pausa sulle riorganizzazioni profonde ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)) lo rendono visibile e fermano nuovi round; la verifica delle monete prima del pagamento fa sì che un round colpito paghi ciò che resta invece di bloccarsi. Il denaro sottratto dall'attaccante è perso per la piattaforma. - *Aperto:* nessuna di queste misure lega il costo dell'attacco alla dimensione del montepremi. Il tetto opzionale `max_round_pool_sats` ([ADR-21](adr/ADR-21-tre-limiti-distinti-sul-numero-monete.md)) è disattivato per default (0) e, anche impostato, il suo valore non discende da alcun calcolo del costo di noleggiare potenza sufficiente a riscrivere `deposit_confirmations` blocchi: resta a discrezione dell'operatore. Per un montepremi abbastanza grande, quindi, l'attacco può convenire nonostante le mitigazioni sopra. Decisione rimandata: se e come legare `max_round_pool_sats` a una stima di quel costo (o attivarlo per default) non è stato ancora deciso. - *Per chiuderlo:* non è chiudibile dal software della lotteria. *Codice:* `app/deposits/service.py:find_new_credit_candidates` · `app/electrum/listener.py:ElectrumListener._apply_header` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` - **[LIM-25]** **Operatore e minatore insieme possono scegliere il vincitore.** Con il commit-reveal ([ADR-24](adr/ADR-24-l-estrazione-commit-reveal-fra-server.md)) l'operatore conosce `server_seed` e può calcolare l'esito di ogni blocco candidato; un minatore d'accordo con lui può scartare i blocchi sfavorevoli. Lo stesso vale per chi ottiene sia il database sia la chiave di cifratura: il segreto è cifrato a riposo ([sicurezza.md](sicurezza.md#sec-56) SEC-56), quindi il solo database, o un suo backup, non basta. - *Impatto:* un round può essere vinto da un complice. - *Perché è accettato:* l'operatore è già fidato come custode di tutti i fondi ([LIM-26]); chi può leggere il database può già fare di peggio. Il commit-reveal impedisce a chi produce o trasmette i blocchi *da solo* di scegliere il vincitore, che era il rischio da chiudere. - *Per chiuderlo:* un contributo di casualità che l'operatore non conosce (segreti dei giocatori, beacon esterno), scartato in [ADR-24](adr/ADR-24-l-estrazione-commit-reveal-fra-server.md). *Codice:* `app/rounds/draw.py:draw_winner` · `app/rounds/service.py:open_new_round_if_needed` - **[LIM-27]** **Le soglie di conferma differiscono da quelle del portafoglio di riferimento.** `PalladiumWallet` considera spendibile un'uscita normale dopo `MinConfirmations = 6` conferme e una coinbase dopo 121 (maturità 120 più un blocco di margine). La piattaforma adotta 6 solo per i depositi ([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)) e 120 per le coinbase; un prelievo o una vincita, confermati qui a 1 conferma, compaiono nel portafoglio di riferimento dell'utente come non ancora spendibili per altri 5 blocchi. - *Impatto:* un utente che preleva verso `PalladiumWallet` vede i fondi arrivati ma bloccati per altri 5 blocchi (10 minuti in media) rispetto a quanto dice la piattaforma; nessun rischio per i fondi. - *Per chiuderlo:* non serve: è una politica del portafoglio del destinatario, non una regola di consenso. Le guide ([guida-utente.md](guida-utente.md)) possono dirlo. *Codice:* `app/tx/confirmation.py:poll_once` · `app/deposits/service.py:find_new_credit_candidates` ### Configurazione ed economia - **[LIM-16]** **Solo i tempi del round sono fotografati all'apertura.** Costo della scommessa, fee rate e `fee_address` sono letti dal vivo. - *Impatto:* cambiare il costo a metà round fa pagare poste diverse a giocatori con la stessa probabilità, e cambia subito il prelievo minimo; cambiare `fee_address` a metà round devia la commissione del round in corso (la modifica è comunque registrata in audit). - *Stato:* da chiudere con B-95. - *Per chiuderlo:* fotografare costo, `fee_address` e `draw_confirmations` sulla riga del round, come già si fa per durata e pausa ([ADR-10](adr/ADR-10-parametri-round-fotografati-all-apertura.md)). Il fee rate resta letto dal vivo per scelta, e il prelievo minimo segue il costo corrente della configurazione ([ADR-20](adr/ADR-20-prelievo-minimo-costo-corrente-scommessa.md)). *Codice:* `app/bets/service.py:place_bet` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/withdrawals/service.py:request_withdrawal` - **[LIM-17]** **Nessun consolidamento automatico del montepremi.** Monete avanzate e resti si accumulano sugli indirizzi del montepremi (fino a B-80 su uno solo, per tutti i round); il margine fra 400 partecipanti e 500 ingressi ([ADR-21](adr/ADR-21-tre-limiti-distinti-sul-numero-monete.md)) si consumava nel tempo. - *Impatto:* raggiunto il limite, il pagamento fallisce con `too_many_inputs` e il round resta in `paying_out`, con un `payout_failed` ogni 60s. - *Stato:* la parte del margine che si consumava è chiusa da B-80: con un indirizzo per round ([ADR-03](adr/ADR-03-ogni-round-ha-proprio-indirizzo-montepremi.md)) il pagamento spende solo le monete del proprio round, e il margine non si consuma più (resta lo stato descritto sopra per i round aperti prima di B-80, che condividono l'indirizzo di riserva). - *Resta accettato:* le riserve per l'RBF ([ADR-17](adr/ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md)) e le monete estranee inviate agli indirizzi di round chiusi si accumulano senza consolidamento automatico; recuperarle è un'operazione manuale ([operativita.md](operativita.md#intervento-manuale-sui-fondi)). *Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/wallet/psbt_builder.py:build_payout_transaction` ### Prodotto e interfacce - **[LIM-18]** **Il sistema presuppone un solo processo.** Canale SSE, blocchi per-utente, limitatori di tentativi, cache dei QR e della chiave dell'account sono in memoria. - *Impatto:* con più worker, due costruzioni dello stesso utente potrebbero scegliere le stesse monete, i limiti per IP si dividerebbero per worker e gli eventi SSE arriverebbero solo ai client dello stesso worker. - *Per chiuderlo:* canale condiviso (Redis pub/sub) e lock su database o Redis. L'invariante del round attivo è già un indice e non rientra qui. Il piano del 2026-09-28 **non** lo chiude: un solo processo e un solo file SQLite restano la scelta di [ADR-13](adr/ADR-13-processo-file-sqlite.md), adeguata alla scala prevista; il piano rende solo più affidabile quel processo (B-88, B-93). *Codice:* `app/tx/locks.py:UserLocks` · `app/rounds/events.py:RoundEventBroadcaster` · `app/auth/rate_limit.py:AuthRateLimiters` - **[LIM-19]** **Nessuna cronologia per l'utente.** L'utente non vede lo storico dei round, delle scommesse, dei depositi o dei prelievi: un prelievo fallito lascia una riga `failed` che l'utente non può mai vedere. Esistono solo `GET /users/me/last-round-result` (l'ultimo round giocato) e `GET /bug-reports/mine`. L'operatore ha invece `/admin/rounds`, `/admin/pending-transactions`, `/admin/audit-log`. - *Per chiuderlo:* endpoint e sezione di cronologia personale. *Codice:* `app/api/routes/users.py:last_round_result` · `app/withdrawals/service.py:request_withdrawal` - **[LIM-20]** **`/guida` è un segnaposto.** Il collegamento funziona ma la pagina dice che la guida arriverà. (`/report-bug` invece è completa e tradotta.) *Codice:* `app/main.py:user_guide` · `app/static/guida.html` - **[LIM-21]** **La parità delle chiavi di traduzione non è verificata da un test.** Oggi le 7 lingue hanno le stesse 151 chiavi, ma nulla lo impone: una chiave mancante ricade sull'inglese e, se manca anche lì, viene mostrata la chiave grezza. - *Stato:* da chiudere con il test di parità di [ARC-63](architettura.md#test) (nessun B-nn: non è un difetto del codice). - *Per chiuderlo:* un test che confronti gli insiemi di chiavi di `TRANSLATIONS`. *Codice:* `app/static/i18n.js:t` - **[LIM-22]** **L'API di amministrazione e alcuni errori di contorno non usano il formato d'errore strutturato.** Gli endpoint `/admin/*` rispondono con un `detail` stringa (per esempio `"invalid admin token"`, `"user not found"`), e così il 503 del canale SSE oltre il tetto; è coerente con un pannello non tradotto, ma non è dichiarato altrove. - *Stato:* da chiudere con B-94. - *Per chiuderlo:* errori strutturati ovunque: `/admin/*`, il 503 del canale SSE e gli errori generati dal framework usano lo stesso formato `{code, message, params}` ([ADR-47](adr/ADR-47-l-api-parla-sola-lingua-risponde.md)). *Codice:* `app/api/routes/admin.py:require_admin` · `app/api/routes/rounds.py:round_stream` - **[LIM-23]** **`GET /admin/users` non ha limite né paginazione**, a differenza degli altri elenchi admin. - *Impatto:* con molti utenti la risposta cresce senza limite. - *Stato:* da chiudere con B-94 (paginazione dell'elenco utenti). *Codice:* `app/api/routes/admin.py:list_users`