A user could bet and then double-spend the 1-conference deposit that funded the bet with a private chain: the bet left the chain but its participant stayed `confirmed`, and the payout failed "insufficient pool UTXOs" for ever with everyone else's money stuck. Coinbase outputs were also credited at 1 confirmation (maturity is 120). Deposits - `round_config.deposit_confirmations` (default 6, 1-100, editable from /admin) is the threshold for a deposit; an output of a platform transaction (any txid of a pending_transactions row, RBF versions included) keeps 1; a coinbase output needs 120, recognised from the raw transaction, which must hash to the txid (`is_coinbase`, cached). `read_deposit_policy` / `classify_new_deposits` run between the read and the write, so no session is open across the lookup. - The quorum must also place the coin deep enough on our own tip (ELC-17). - Confirmed outputs short of their threshold are pending deposits (memory only, replaced at every refresh): shown in `pending_balance_sats` / `has_pending` (also for DEP-13's withdrawal message), never spendable. No scripthash notification announces a maturing deposit, so each new block refreshes only the users that have one, one task per user (DEP-09 now says so). Payout - `_trigger_payout` verifies the round's confirmed participants' bet outputs with the other servers (`verify_pool_outpoints`: strict majority of >= 2 answers per outpoint, a mempool-only coin or a tie gives no verdict) and builds from exactly those outpoints (RND-22), never from whatever else sits at the address. No quorum: `payout_failed`, `pool verification unavailable`, nothing changes. Bets that are gone become `reorged_out`, one `participant_reorged_out` row each, `pool_amount_sats` rewritten (only downward) in the same transaction, and the 70/30 split is computed on what is left. - Winner's own bet gone: nothing is built; `rounds.payout_blocked_at` (Alembic f2c64a8d1e93, with `deposit_confirmations`, plain add_column) is set, `payout_blocked` and the pause with `paused_reason = 'payout_blocked'` are written in one transaction; `POST /admin/resume` clears the mark and the next attempt verifies again. /admin/rounds returns it. - Rounds opened before B-80 (pool = the shared reserve address) keep the old active-server `listunspent` choice, since that address holds no single round's bets. Ambiguities resolved: the winner's row stays `confirmed` while held (AUD-45 says every participant whose bet is gone becomes `reorged_out`, but OPS-30/DAT-68 need the verification to repeat after the resume, which only a `confirmed` row allows); `/`'s deposit hint no longer says "1 confirmation" (7 languages, same keys); guides updated. Tests changed because the spec changed: listener tests that credited height-100 coins at tip 0 now set a tip past every threshold; the payout fixtures seed confirmed participants (the new source of the inputs) instead of an active-server `listunspent`, FakeNetwork answers the verification, and test_round_pool_address asserts the quorum is asked about the round's address. New: thresholds at exactly 6/5, configured value, coinbase at 119/120 and a mismatching or missing raw tx, platform outputs at 1, dust, quorum height, waiting-deposit display and disappearance, block-triggered refresh, resume, verification verdicts, gone bets excluded with the split recomputed, winner gone held and paused, held payouts untouched until the resume, re-verification after it, no quorum, a payout with 1 and with 400 participants against reserve+fee+dust, admin config bounds and migration. Mutation-checked: threshold 1, ignored coinbase, no height bound, no verification and no winner hold each fail tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
453 lines
29 KiB
Markdown
453 lines
29 KiB
Markdown
# 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 | Non più (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 | Non più (chiuso da B-82 e B-85) | Resta [LIM-24] |
|
|
| [LIM-11], ~~[LIM-12]~~ Quorum facoltativo e parziale | Non più (chiusi da B-82, B-83 e B-84) | Resta la scelta dell'operatore di server indipendenti ([LIM-11]) |
|
|
| [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]~~ Rimosso: chiuso da B-81. Un round in `drawing` riprende da solo
|
|
dopo un errore o un riavvio, con lo stesso blocco, gli stessi partecipanti e lo
|
|
stesso vincitore ([RND-36](ciclo-di-vita-del-round.md)).
|
|
|
|
- **[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-87 (la riserva del pagamento, B-79, c'è).
|
|
- *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]~~ Rimosso: chiuso da B-82 e B-85. I depositi si accreditano a
|
|
`deposit_confirmations` conferme (le coinbase a 120), le riorganizzazioni sono
|
|
seguite, registrate e, da profondità 2, mettono in pausa; le monete del round
|
|
si verificano con il quorum prima del pagamento e i partecipanti spariti
|
|
restano fuori ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)). Resta accettato il 51% ([LIM-24]).
|
|
|
|
- **[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]~~ Rimosso: chiuso da B-83 e B-82. Conferme (prova merkle),
|
|
reintegri, abbandoni e le monete del montepremi usate per il pagamento sono
|
|
tutti decisi dal quorum degli altri server, mai dal solo server attivo.
|
|
|
|
- **[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:* chiuso da B-73.
|
|
- *Come è chiuso:* 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.** Il bersaglio dichiarato non può essere più facile del limite di rete
|
|
(B-74, [ELC-11](connessione-electrum.md#4-header-validati-non-creduti)) e
|
|
ogni salto di più blocchi è verificato come catena, quindi un header falso a
|
|
un'altezza enorme non blocca più `tip_height`.
|
|
- *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 proteggeva dal blocco di `tip_height`, ora chiuso da B-74.
|
|
- *Stato:* chiuso da B-74.
|
|
- *Come è chiuso:* 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`
|