Files
plm-lottery/docs/limiti-noti.md
T

516 lines
34 KiB
Markdown
Raw Normal View History

# 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 |
| [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`. Oggi a porre fine all'attesa
di una scommessa che non conferma è solo il riconciliatore, dopo 6 ore dal
primo invio se la transazione è sparita dalla rete del server attivo; 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:* da chiudere con B-78 (in parte, per scelta).
- *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=<sha256>`, `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: la conferma di scommesse, pagamenti e prelievi
(`poll_once`), la decisione del riconciliatore che una transazione «non
esiste più», l'elenco delle monete del montepremi usato per il pagamento, e il
ripristino di una moneta prima segnata come spesa all'esterno.
- *Impatto:* un server ostile potrebbe far considerare confermata una
scommessa che non lo è, o far abbandonare a torto una transazione viva
(liberandone le monete e, per un pagamento, azzerando `payout_txid`),
lasciando il database in disaccordo con la catena; il ripristino non
corroborato può reintegrare nel saldo una moneta davvero spesa.
- *Mitigazione attuale:* l'abbandono avviene solo dopo 6 ore per `pending`; un
invio basato su monete inesistenti è rifiutato dalla rete.
- *Stato:* da chiudere con B-83 (conferme e ripristini), B-78 (abbandono),
B-82 (monete del round prima del pagamento) e B-76 (invio a tutti i
server).
- *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`