Restructure docs: docs-first, SSOT, one ADR per decision
- Add docs/README.md as the index of all documentation. - Split the 52 DEC entries of decisioni-e-limiti.md into docs/adr/ADR-NN files (same numbers), add ADR-53 (audit log), TEMPLATE.md and an ADR index with a feature -> ADR table. - Rename decisioni-e-limiti.md to limiti-noti.md (LIM only) and rewrite all DEC-nn references as links to the ADRs. - Document the docs-first / SSOT / ADR rules in processo-di-modifica.md and map the docs in CLAUDE.md and README.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ba8e22bb4
commit
b424f670c5
76 files changed
+2147
-1530
No files matched your search
@@ -0,0 +1,73 @@
|
||||
# Documentazione di PLM Lottery
|
||||
|
||||
Questa cartella è la **fonte di verità** del progetto: si lavora **docs-first**
|
||||
(prima si scrive o si modifica la documentazione, poi il codice) e ogni
|
||||
informazione ha **un solo proprietario** (SSOT). Questo file è l'indice: da
|
||||
qui si raggiunge tutto il resto.
|
||||
|
||||
## I tre principi
|
||||
|
||||
1. **Docs-first.** Nessuna modifica di comportamento parte dal codice. Si
|
||||
modifica il requisito (o si scrive l'ADR, se è una scelta nuova), poi si
|
||||
implementa, poi si testa. Spec, codice e test entrano nello stesso commit.
|
||||
2. **SSOT — un solo proprietario per ogni informazione.** Il *perché* di una
|
||||
scelta sta in un ADR; il *cosa* verificabile in un requisito
|
||||
(`PREFISSO-NN`) del documento di dominio; il *come* nel codice; la cronologia
|
||||
dei difetti in `storico-bug.md`. Gli altri documenti **rimandano all'ID**,
|
||||
non ripetono il testo. Se due documenti dicono cose diverse, vince la
|
||||
specifica e si corregge l'altro.
|
||||
3. **Un ADR per ogni funzionalità.** Ogni funzionalità nuova inizia con un ADR
|
||||
in [adr/](adr/README.md) (modello: [adr/TEMPLATE.md](adr/TEMPLATE.md)) che
|
||||
registra contesto, decisione, alternative scartate e conseguenze, e ne
|
||||
elenca i requisiti.
|
||||
|
||||
## Dove si trova cosa
|
||||
|
||||
| Domanda | Dove |
|
||||
|---|---|
|
||||
| Perché è fatto così? Quali alternative sono state scartate? | [adr/](adr/README.md) — indice per area e tabella funzionalità → ADR |
|
||||
| Cosa deve fare esattamente il software? | I documenti di dominio qui sotto (requisiti con ID stabile) |
|
||||
| Quali limiti abbiamo accettato? | [limiti-noti.md](limiti-noti.md) (`LIM-nn`) |
|
||||
| Che bug sono stati trovati/corretti? Quali restano aperti? | [storico-bug.md](storico-bug.md); i soli aperti in [../BUGS.md](../BUGS.md) |
|
||||
| In che ordine si implementano le correzioni aperte? | [piano-correzioni.md](piano-correzioni.md) |
|
||||
| Come si modifica la documentazione/il codice? | [processo-di-modifica.md](processo-di-modifica.md) |
|
||||
| Come si installa, si avvia, si usa? | Guide operative qui sotto |
|
||||
|
||||
## Specifica (documenti di dominio)
|
||||
|
||||
Punto d'ingresso narrativo: [come-funziona.md](come-funziona.md).
|
||||
|
||||
| Documento | Prefisso | Contenuto |
|
||||
|---|---|---|
|
||||
| [registrazione-e-depositi.md](registrazione-e-depositi.md) | `REG`, `DEP` | Account, indirizzi, depositi, saldo |
|
||||
| [scommesse-e-prelievi.md](scommesse-e-prelievi.md) | `BET`, `WDR` | Scommesse e prelievi |
|
||||
| [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) | `RND` | Apertura, chiusura, estrazione, pagamento |
|
||||
| [transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md) | `TX` | Ciclo di vita delle transazioni, RBF, riconciliazione |
|
||||
| [connessione-electrum.md](connessione-electrum.md) | `ELC` | Accesso alla rete PLM e difese |
|
||||
| [architettura.md](architettura.md) | `ARC` | Struttura del codice, processi, frontend, test |
|
||||
| [modello-dati.md](modello-dati.md) | `DAT` | Tabelle, campi, stati, vincoli |
|
||||
| [api.md](api.md) | `API` | Endpoint, risposte, codici di errore |
|
||||
| [sicurezza.md](sicurezza.md) | `SEC` | Fiducia, chiavi, autenticazione, limiti sui tentativi |
|
||||
| [audit-log.md](audit-log.md) | `AUD` | Catalogo degli eventi di audit |
|
||||
| [operativita.md](operativita.md) | `OPS` | Backup, riavvii, incidenti |
|
||||
|
||||
## Decisioni, limiti, storia
|
||||
|
||||
| Documento | Prefisso | Contenuto |
|
||||
|---|---|---|
|
||||
| [adr/](adr/README.md) | `ADR` | Una decisione di progetto per file (53 al momento) |
|
||||
| [limiti-noti.md](limiti-noti.md) | `LIM` | Limiti accettati consapevolmente |
|
||||
| [storico-bug.md](storico-bug.md) | `B-nn` | Tutti i bug trovati e corretti |
|
||||
| [piano-correzioni.md](piano-correzioni.md) | — | Ordine di implementazione delle correzioni aperte |
|
||||
|
||||
## Guide operative (senza requisiti)
|
||||
|
||||
[setup.md](setup.md) · [running-the-server.md](running-the-server.md) ·
|
||||
[guida-utente.md](guida-utente.md) · [guida-admin.md](guida-admin.md). Si
|
||||
aggiornano quando la specifica cambia qualcosa di visibile.
|
||||
|
||||
## Lingua
|
||||
|
||||
La documentazione di `docs/` è in italiano, per scelta esplicita; tutto il
|
||||
resto del repository (codice, commenti, commit, `CLAUDE.md`, `BUGS.md`) è in
|
||||
inglese.
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-01 — La piattaforma è custodiale: tutte le chiavi sono generate e tenute dal server
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-91)
|
||||
- **Area:** Custodia, chiavi e indirizzi
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**La piattaforma è custodiale: tutte le chiavi sono generate e
|
||||
tenute dal server.** Una sola master xprv, cifrata con Fernet a riposo; la
|
||||
chiave di cifratura sta in un file montato nel container
|
||||
(`XPRV_ENCRYPTION_KEY_FILE`, permessi `0600`, vedi
|
||||
[architettura.md](../architettura.md#configurazione)), mai nel database né in
|
||||
git. L'utente non controlla nulla finché non preleva.
|
||||
- *Motivazione:* un'esperienza da «conto» (registrazione con username e
|
||||
password, scommessa con un clic) senza portafoglio lato utente, firme o
|
||||
PSBT da gestire nel browser.
|
||||
- *Alternative scartate:* portafoglio non custodiale con firma lato client.
|
||||
- *Conseguenze:* la compromissione del server (file cifrato + variabile
|
||||
d'ambiente) espone tutti i fondi; da qui l'esportazione WIF accettata
|
||||
([ADR-39](ADR-39-l-esportazione-chiave-privata-wif-utente.md)), l'assenza di recupero password ([ADR-37](ADR-37-nessun-recupero-password-reset.md)) e il modello di
|
||||
minaccia di [sicurezza.md](../sicurezza.md).
|
||||
|
||||
*Codice:* `app/wallet/hd.py:_load_account_key` · `app/wallet/keystore.py:decrypt_xprv` · `app/wallet/hd.py:generate_master_key` ·
|
||||
*Test:* `tests/unit/test_hd.py`
|
||||
*Stato:* da implementare (B-91) · *Oggi:* la chiave di cifratura è la
|
||||
variabile d'ambiente `XPRV_ENCRYPTION_KEY`, visibile con `docker inspect`.
|
||||
@@ -0,0 +1,25 @@
|
||||
# ADR-02 — Un indirizzo per utente, permanente, per tutti gli usi
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Custodia, chiavi e indirizzi
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Un indirizzo per utente, permanente, per tutti gli usi.**
|
||||
Alla registrazione il server deriva un indirizzo P2WPKH BIP84
|
||||
(`m/84'/746'/0'/0/<indice>`, un indice per utente, assegnato come massimo
|
||||
esistente + 1). Lo stesso indirizzo è indirizzo di deposito, riceve le
|
||||
vincite e riceve il resto di ogni scommessa e prelievo. Non esiste un
|
||||
«indirizzo vincitore» separato.
|
||||
- *Motivazione:* un solo indirizzo da osservare per utente, un solo saldo da
|
||||
ricostruire (`utxo_events`), e le vincite accreditate dal normale percorso
|
||||
dei depositi senza logica dedicata.
|
||||
- *Conseguenze:* nessuna privacy fra depositi, vincite e resti; un prelievo
|
||||
verso il proprio indirizzo è rifiutato (renderebbe indistinguibili
|
||||
destinatario e resto); il pagamento di un round si considera accreditato
|
||||
quando il listener vede la moneta sull'indirizzo del vincitore
|
||||
(vedi [registrazione-e-depositi.md](../registrazione-e-depositi.md)).
|
||||
|
||||
*Codice:* `app/auth/routes.py:register` · `app/wallet/hd.py:derive_user_address` · `app/rounds/confirmation.py:_on_payout_confirmed` ·
|
||||
*Test:* `tests/unit/test_hd.py`, `tests/unit/test_users.py`
|
||||
@@ -0,0 +1,39 @@
|
||||
# ADR-03 — Ogni round ha il proprio indirizzo del montepremi
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-80)
|
||||
- **Area:** Custodia, chiavi e indirizzi
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Ogni round ha il proprio indirizzo del montepremi**
|
||||
(decisione del 2026-09-28), derivato dalla stessa master key sul ramo
|
||||
`m/84'/746'/0'/2/<round_id>` e salvato sulla riga del round
|
||||
(`rounds.pool_address`). Le scommesse di un round pagano solo il suo
|
||||
indirizzo e il pagamento spende solo le monete di quell'indirizzo. Il vecchio
|
||||
indirizzo unico `m/84'/746'/0'/1/0` diventa l'**indirizzo di riserva** del
|
||||
montepremi, che riceve la riserva per l'RBF di ogni pagamento ([ADR-17](ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md));
|
||||
i round aperti prima della modifica mantengono quell'indirizzo come
|
||||
montepremi. Un prelievo verso un indirizzo del montepremi, di riserva o delle
|
||||
commissioni è rifiutato. Il ramo `0` resta riservato agli utenti.
|
||||
- *Motivazione:* con un indirizzo unico la cronologia cresce senza limite
|
||||
(e con essa le risposte del server, B-75), il denaro di round diversi e le
|
||||
monete estranee si mescolano, e un backup ripristinato ripete un round
|
||||
contro ciò che il montepremi contiene oggi (B-90). Separare gli indirizzi
|
||||
isola denaro, cronologia e pagamento di ogni round.
|
||||
- *Alternative scartate:* un indirizzo unico con contabilità per round nel
|
||||
database (non limita la cronologia e non isola le monete estranee);
|
||||
consolidamento periodico dell'indirizzo unico (sposta il problema, non lo
|
||||
toglie).
|
||||
- *Conseguenze:* nessun segreto aggiuntivo (tutto deriva dalla master
|
||||
key); una moneta estranea inviata all'indirizzo di un round chiuso resta
|
||||
lì, visibile ma fuori da ogni pagamento; il margine fra partecipanti e
|
||||
ingressi del pagamento ([ADR-21](ADR-21-tre-limiti-distinti-sul-numero-monete.md)) non si consuma più nel tempo ([LIM-17]).
|
||||
Regole in [ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#4--pagamento)
|
||||
e [scommesse-e-prelievi.md](../scommesse-e-prelievi.md#prelievo) (B-80).
|
||||
|
||||
*Codice:* `app/wallet/hd.py:derive_pool_key` · `app/wallet/hd.py:derive_pool_address` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* `tests/unit/test_pending_address.py`
|
||||
*Stato:* da implementare (B-80) · *Oggi:* un unico indirizzo del montepremi
|
||||
(`1/0`) riceve le scommesse di tutti i round e il pagamento sceglie fra
|
||||
tutte le sue monete.
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-04 — Accesso alla rete solo via protocollo Electrum
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Custodia, chiavi e indirizzi
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Accesso alla rete solo via protocollo Electrum**, senza nodo
|
||||
completo né P2P. Una sola connessione attiva serve tutto (accrediti, invii,
|
||||
conferme, altezza della catena).
|
||||
- *Motivazione:* nessun nodo da installare e sincronizzare.
|
||||
- *Conseguenze:* la connessione è il principale punto singolo di guasto e il
|
||||
principale rischio di integrità; da qui le difese di [ADR-07](ADR-07-ogni-decisione-sposta-denaro-base-risposta.md),
|
||||
[ADR-26](ADR-26-validazione-degli-header-concatenata-senza-ricalcolo.md) e [ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md) e i limiti [LIM-11], [LIM-12], [LIM-14]. Dettagli in
|
||||
[connessione-electrum.md](../connessione-electrum.md).
|
||||
|
||||
*Codice:* `app/electrum/client.py:ElectrumClient` · `app/electrum/listener.py:ElectrumListener` · `app/main.py:lifespan`
|
||||
@@ -0,0 +1,17 @@
|
||||
# ADR-05 — Solo indirizzi bech32 PLM (`plm1…`) come destinazione
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Custodia, chiavi e indirizzi
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Solo indirizzi bech32 PLM (`plm1…`) come destinazione.** Un
|
||||
prelievo verso un indirizzo legacy (`P…`) o di un'altra catena (`bc1…`) è
|
||||
rifiutato; lo stesso controllo vale per `fee_address` e per `/qr/{address}`.
|
||||
- *Motivazione:* un indirizzo di un'altra catena si interpreta comunque come
|
||||
witness program valido, quindi la transazione si costruirebbe e i fondi
|
||||
sarebbero irrecuperabili.
|
||||
|
||||
*Codice:* `app/wallet/address.py:is_valid_plm_address` · `app/withdrawals/service.py:request_withdrawal` · `app/api/routes/admin.py:RoundConfigUpdate` ·
|
||||
*Test:* `tests/unit/test_withdrawals.py::test_withdrawal_rejects_non_plm_address`
|
||||
@@ -0,0 +1,40 @@
|
||||
# ADR-06 — I depositi richiedono `deposit_confirmations` conferme (6 per default, configurabile in `round_config`), le uscite coinbase 120; ogni altra transazione ne richiede 1
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-82)
|
||||
- **Area:** Conferme e fiducia nella rete
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**I depositi richiedono `deposit_confirmations` conferme (6
|
||||
per default, configurabile in `round_config`), le uscite coinbase 120; ogni
|
||||
altra transazione ne richiede 1** (decisione del 2026-09-28). «Deposito» è
|
||||
una moneta arrivata all'indirizzo di un utente da una transazione che la
|
||||
piattaforma non ha costruito; le uscite delle transazioni della piattaforma
|
||||
(resto di scommesse e prelievi, vincite) si accreditano a 1 conferma come
|
||||
le transazioni che le hanno prodotte. Le soglie si cambiano solo con una
|
||||
decisione esplicita.
|
||||
- *Motivazione:* il deposito è **l'unico ingresso che un utente può spendere
|
||||
due volte**: tutto il resto lo firma solo il server. Un utente che
|
||||
scommette con un deposito a 1 conferma e poi, se perde, lo sostituisce con
|
||||
una catena privata fa sparire la propria scommessa dal montepremi mentre il
|
||||
partecipante resta contato, e il round diventa impagabile per tutti
|
||||
(B-82). La potenza di calcolo di PLM (~22 TH/s) si può affittare, quindi
|
||||
una riorganizzazione di 1–2 blocchi è alla portata di un singolo
|
||||
attaccante; 6 conferme la rendono costosa quanto sei blocchi di tutta la
|
||||
rete. È anche la soglia di spendibilità del portafoglio di riferimento
|
||||
([LIM-27]).
|
||||
- *Alternative scartate:* 1 conferma per tutto (la regola precedente,
|
||||
esposta al doppio speso del deposito); 6 conferme per tutto (allunga ogni
|
||||
round di cinque blocchi per coprire transazioni che l'utente non può
|
||||
sostituire); scommesse ammesse solo da monete a 6 conferme ma depositi
|
||||
mostrati prima (due saldi da spiegare all'utente per lo stesso rischio).
|
||||
- *Conseguenze:* un deposito diventa spendibile dopo 6 blocchi (12 minuti
|
||||
in media, con blocchi da 120s) invece di 1; il rischio residuo (una riorganizzazione più profonda di 6 blocchi)
|
||||
è contenuto da [ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md) e accettato in [LIM-24]. Regole in
|
||||
[registrazione-e-depositi.md](../registrazione-e-depositi.md#deposito) (B-82).
|
||||
|
||||
*Codice:* `app/tx/confirmation.py:poll_once` · `app/deposits/service.py:find_new_credit_candidates` ·
|
||||
*Test:* `tests/unit/test_confirmation.py::test_unconfirmed_tx_is_left_pending`, `tests/unit/test_deposits.py::test_unconfirmed_entry_is_ignored`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* ogni moneta, deposito o coinbase
|
||||
compresi, si accredita a 1 conferma.
|
||||
@@ -0,0 +1,38 @@
|
||||
# ADR-07 — Ogni decisione che sposta denaro in base a una risposta di rete è corroborata da un quorum degli altri server configurati
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-83, B-84)
|
||||
- **Area:** Conferme e fiducia nella rete
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Ogni decisione che sposta denaro in base a una risposta di
|
||||
rete è corroborata da un quorum degli altri server configurati**: il blocco
|
||||
che semina l'estrazione, una moneta data per spesa all'esterno, un nuovo
|
||||
deposito da accreditare, il ripristino di una moneta, la conferma di una
|
||||
transazione (con prova merkle contro un header che possediamo), l'abbandono
|
||||
di una transazione, le monete del round verificate prima del pagamento. Si
|
||||
interrogano solo gli *altri* server, mai quello attivo (che è ciò che un
|
||||
attaccante controllerebbe); servono **almeno 2 risposte** e la maggioranza
|
||||
stretta di chi risponde. Il server rifiuta di partire con meno di 2 server
|
||||
di riserva, salvo `ALLOW_NO_QUORUM=true` (solo sviluppo). Se rispondono meno
|
||||
di 2 server, la corroborazione fallisce.
|
||||
- *Motivazione:* un solo server ostile non deve poter scegliere il
|
||||
vincitore (B-28), azzerare un saldo (B-29) o gonfiarlo (B-59), né far
|
||||
credere confermato un pagamento mai trasmesso (B-83). I saldi si muovono in
|
||||
entrambe le direzioni, quindi entrambe richiedono il quorum. Con 0 o 1
|
||||
server di riserva la «maggioranza» è una sola voce, o nessuna (B-84).
|
||||
- *Alternative scartate:* quorum facoltativo, che passa sempre con la lista
|
||||
vuota (la regola precedente: il default di `.env.example` spegneva ogni
|
||||
difesa senza avvisare).
|
||||
- *Conseguenze:* una corroborazione fallita **ritarda**, non perde e non
|
||||
libera fondi ([ADR-51](ADR-51-dubbio-liberano-mai-fondi.md)): il deposito è riproposto al refresh successivo,
|
||||
l'estrazione richiede di nuovo lo stesso blocco. Il quorum resta fra server
|
||||
scelti dall'operatore ([LIM-11]). Regole in
|
||||
[connessione-electrum.md](../connessione-electrum.md#5-il-quorum) (B-83, B-84).
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority` · `app/electrum/listener.py:ElectrumListener.corroborate_header` · `app/electrum/listener.py:ElectrumListener.corroborate_utxo_spent` · `app/electrum/listener.py:ElectrumListener.corroborate_utxo_credit` · `app/main.py:lifespan` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-83, B-84) · *Oggi:* senza server di riserva la
|
||||
corroborazione passa sempre, basta una risposta, e conferme, ripristini e
|
||||
abbandoni si decidono sul solo server attivo.
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-08 — I parametri operativi stanno nel database, i segreti nell'ambiente
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Configurazione e ambiente
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**I parametri operativi stanno nel database, i segreti
|
||||
nell'ambiente.** Ogni parametro di business (costo della scommessa, durata e
|
||||
pausa del round, animazione, fee rate, timeout RBF, `fee_address`, pausa) è
|
||||
una colonna dell'unica riga di `round_config`, modificabile a caldo da
|
||||
`/admin`; i valori iniziali sono i default delle colonne di `RoundConfig`,
|
||||
non `app/config.py`. In `.env` restano solo segreti e collegamenti
|
||||
(master key, JWT, host Electrum, token admin, URL del database).
|
||||
- *Motivazione:* regolare la lotteria senza ridistribuire né riavviare;
|
||||
i segreti richiedono comunque un riavvio e non vanno scambiati a caldo.
|
||||
- *Conseguenze:* ogni modifica è validata con limiti espliciti ([ADR-09](ADR-09-modifiche-configurazione-validate-fidate.md)) e
|
||||
registrata in audit come `config_updated` con prima/dopo.
|
||||
|
||||
*Codice:* `app/db/models.py:RoundConfig` · `app/rounds/config.py:get_round_config` · `app/config.py:Settings` · `app/api/routes/admin.py:update_config` ·
|
||||
*Test:* `tests/unit/test_admin.py::test_admin_reads_and_updates_config`, `tests/unit/test_admin.py::test_config_update_is_audit_logged`
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-09 — Le modifiche di configurazione sono validate, non fidate
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Configurazione e ambiente
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Le modifiche di configurazione sono validate, non fidate.**
|
||||
Ogni campo numerico ha limiti (ad esempio `fee_rate_sat_vb` fra 1 e
|
||||
`MAX_FEE_RATE_SAT_VB`, `round_duration_seconds` almeno 30) e `fee_address`
|
||||
deve essere un indirizzo PLM valido. `paused` non è modificabile da
|
||||
`PUT /admin/config`.
|
||||
- *Motivazione:* un fee rate a 0 produce transazioni che nessun nodo inoltra,
|
||||
una durata a 0 fa scadere il round appena aperto, un `fee_address` estraneo
|
||||
manda la commissione in uno script irrecuperabile (B-05).
|
||||
|
||||
*Codice:* `app/api/routes/admin.py:RoundConfigUpdate` ·
|
||||
*Test:* `tests/unit/test_admin.py::test_config_rejects_unusable_values`, `tests/unit/test_admin.py::test_pause_cannot_be_toggled_through_the_config_endpoint`
|
||||
@@ -0,0 +1,27 @@
|
||||
# ADR-10 — I parametri del round sono fotografati all'apertura
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-95)
|
||||
- **Area:** Configurazione e ambiente
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**I parametri del round sono fotografati all'apertura.**
|
||||
Durata, pausa successiva, costo della scommessa (`bet_amount_sats`),
|
||||
`fee_address` e `draw_confirmations` vengono copiati sulla riga del round
|
||||
quando si apre; una modifica vale dal round successivo, mai per quello in
|
||||
corso. La pausa fra due round è quella annunciata dal round appena chiuso.
|
||||
Il fee rate resta letto dal vivo: riguarda la singola transazione, non le
|
||||
regole del round.
|
||||
- *Motivazione:* abbassare la durata a metà round lo chiudeva all'istante e
|
||||
spostava il conto alla rovescia di tutti i client (B-61); cambiare costo o
|
||||
`fee_address` a metà round creava poste diverse con la stessa probabilità
|
||||
e deviava la commissione del round in corso (B-95).
|
||||
- *Conseguenze:* il prelievo minimo ([ADR-20](ADR-20-prelievo-minimo-costo-corrente-scommessa.md)) segue il costo corrente della
|
||||
configurazione, non quello del round in corso. Regole in
|
||||
[ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#0--apertura) (B-95).
|
||||
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed` · `app/rounds/service.py:round_deadline` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py::test_a_new_round_snapshots_the_current_config_timing`, `tests/unit/test_scheduler.py::test_tick_ignores_a_config_duration_edited_mid_round`, `tests/unit/test_rounds_service.py::test_cooldown_comes_from_the_round_that_closed`
|
||||
*Stato:* da implementare (B-95) · *Oggi:* sono fotografati solo durata e
|
||||
pausa; costo, `fee_address` e fee rate sono letti dal vivo ([LIM-16]).
|
||||
@@ -0,0 +1,31 @@
|
||||
# ADR-11 — Il server rifiuta di partire con segreti inadeguati
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-91)
|
||||
- **Area:** Configurazione e ambiente
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il server rifiuta di partire con segreti inadeguati**:
|
||||
`JWT_SECRET` sotto i 32 caratteri o `XPRV_ENCRYPTION_KEY` vuota fermano
|
||||
l'avvio. Un `ADMIN_TOKEN` vuoto invece non è fatale: il pannello resta
|
||||
chiuso a tutti. Il controllo è nel lifespan, non in un validatore di
|
||||
`Settings`.
|
||||
- *Motivazione:* evitare un container che sembra sano e si rompe al primo
|
||||
utente reale; tenerlo fuori da `Settings` permette a import e test di
|
||||
funzionare senza `.env`.
|
||||
- *Alternative scartate:* validatore Pydantic su `Settings` (avrebbe fatto
|
||||
fallire la raccolta dei test su un clone pulito, B-15).
|
||||
- *Conseguenze:* lo stesso principio («rifiutare di servire piuttosto che
|
||||
rompersi al primo uso») si estende alla master key decifrata e confrontata
|
||||
con gli indirizzi già salvati (B-89) e al quorum minimo di server (B-84);
|
||||
l'ordine dei controlli all'avvio è in
|
||||
[architettura.md](../architettura.md#nel-lifespan).
|
||||
|
||||
*Codice:* `app/config.py:validate_runtime_secrets` · `app/main.py:lifespan` · `app/api/routes/admin.py:require_admin` ·
|
||||
*Test:* `tests/unit/test_config.py`
|
||||
*Stato:* da implementare (B-91) per la parte sulla chiave · *Oggi:* la
|
||||
chiave Fernet arriva da `XPRV_ENCRYPTION_KEY` (variabile d'ambiente); dopo
|
||||
B-91 arriva da un file (`XPRV_ENCRYPTION_KEY_FILE`,
|
||||
[sicurezza.md](../sicurezza.md#sec-53) SEC-53) e il rifiuto riguarda un file
|
||||
mancante o vuoto.
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR-12 — Il server gira solo in Docker, in sviluppo come in produzione
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-81)
|
||||
- **Area:** Configurazione e ambiente
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il server gira solo in Docker, in sviluppo come in
|
||||
produzione**, dietro Caddy (TLS automatico, certificato autofirmato su
|
||||
`localhost`). Il container rifiuta di partire senza il file della master
|
||||
key, applica le migrazioni Alembic all'avvio e si riavvia da solo
|
||||
(`restart: unless-stopped`). Il venv serve solo per test, migrazioni e
|
||||
script della chiave.
|
||||
- *Motivazione:* un solo modo di eseguire il server, identico ovunque.
|
||||
- *Conseguenze:* dopo un crash ogni stato del round riprende da solo,
|
||||
`drawing` compreso ([ADR-25](ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md), B-81). Procedure in
|
||||
[operativita.md](../operativita.md).
|
||||
|
||||
*Codice:* `Dockerfile` · `docker-compose.yml` · `Caddyfile` ·
|
||||
*Test:* `tests/unit/test_docs_no_bare_uvicorn.py`
|
||||
*Stato:* da implementare (B-81) · *Oggi:* dopo un crash `closing` e
|
||||
`paying_out` riprendono da soli, `drawing` no ([LIM-01]).
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-13 — Un solo processo, un solo file SQLite
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Configurazione e ambiente
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Un solo processo, un solo file SQLite** (modalità WAL con
|
||||
`busy_timeout`, `synchronous=FULL`, `BEGIN IMMEDIATE` per ogni transazione
|
||||
che sposta denaro, vedi [architettura.md](../architettura.md#database), B-88).
|
||||
Canale SSE, blocchi per-utente, limitatori di tentativi e cache sono in
|
||||
memoria.
|
||||
- *Motivazione:* semplicità a scala di ~100 utenti concorrenti.
|
||||
- *Alternative scartate:* Redis o un database con lock condivisi, da non
|
||||
introdurre in via speculativa.
|
||||
- *Conseguenze:* nessuna distribuzione su più worker ([LIM-18]). L'unico
|
||||
invariante che non dipende dal processo singolo è «al più un round
|
||||
attivo», garantito da un indice ([ADR-22](ADR-22-piu-round-attivo-come-invariante-database.md)).
|
||||
|
||||
*Codice:* `app/db/base.py:_register_sqlite_pragmas` · `app/db/base.py:begin_immediate` · `app/tx/locks.py:UserLocks` · `app/rounds/events.py:RoundEventBroadcaster` ·
|
||||
*Test:* `tests/unit/test_db_base.py` · `tests/unit/test_sqlite_transactions.py`
|
||||
@@ -0,0 +1,17 @@
|
||||
# ADR-14 — Ripartizione fissa 70% al vincitore / 30% alla piattaforma
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Ripartizione fissa 70% al vincitore / 30% alla piattaforma**,
|
||||
con l'arrotondamento a favore delle commissioni. È nel codice, non nella
|
||||
configurazione: cambiarla è una modifica del software.
|
||||
- *Motivazione:* una regola economica di fondo non deve poter cambiare con un
|
||||
clic dal pannello. Un'unica funzione alimenta sia il montepremi mostrato sia
|
||||
il pagamento, così i due non possono divergere.
|
||||
|
||||
*Codice:* `app/rounds/service.py:winner_share` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/api/routes/rounds.py:current_round` ·
|
||||
*Test:* `tests/unit/test_payout_builder.py::test_payout_deducts_fee_only_from_winner_share`
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-15 — Biglietto a prezzo fisso, una scommessa per utente per round, probabilità uguale per tutti
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Biglietto a prezzo fisso, una scommessa per utente per round,
|
||||
probabilità uguale per tutti** indipendentemente dall'importo effettivo.
|
||||
Nessun deposito minimo.
|
||||
- *Conseguenze:* l'estrazione sceglie fra partecipanti, non pesa gli
|
||||
importi. Il costo è fotografato all'apertura del round ([ADR-10](ADR-10-parametri-round-fotografati-all-apertura.md)), così
|
||||
tutti i partecipanti di un round pagano la stessa posta (B-95; oggi è letto
|
||||
dal vivo, [LIM-16]).
|
||||
|
||||
*Codice:* `app/bets/service.py:place_bet` · `app/rounds/draw.py:draw_winner` ·
|
||||
*Test:* `tests/unit/test_bets.py::test_place_bet_rejects_second_bet_same_round`
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-16 — La commissione di rete si toglie dall'importo che si muove
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**La commissione di rete si toglie dall'importo che si
|
||||
muove**, non si aggiunge sopra: per una scommessa arriva al montepremi il
|
||||
costo meno la commissione, per un prelievo arriva a destinazione l'importo
|
||||
meno la commissione.
|
||||
- *Conseguenze:* il montepremi si calcola sommando gli importi effettivi
|
||||
delle scommesse, non moltiplicando il numero di giocatori per il costo
|
||||
(B-11); il prelievo registra sia l'importo richiesto sia quello inviato.
|
||||
|
||||
*Codice:* `app/wallet/psbt_builder.py:build_signed_transaction` ·
|
||||
*Test:* `tests/unit/test_psbt_builder.py::test_build_signed_transaction_deducts_fee_from_amount_not_change`
|
||||
@@ -0,0 +1,35 @@
|
||||
# ADR-17 — La commissione del pagamento esce dalla quota del vincitore; la riserva per l'RBF esce dalla quota della piattaforma
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-79)
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**La commissione del pagamento esce dalla quota del
|
||||
vincitore; la riserva per l'RBF esce dalla quota della piattaforma.** Ogni
|
||||
pagamento ha un'uscita di **riserva** di
|
||||
`max(DUST_LIMIT_SATS, dimensione stimata × 10 sat/vB)` verso l'indirizzo di
|
||||
riserva del montepremi ([ADR-03](ADR-03-ogni-round-ha-proprio-indirizzo-montepremi.md)); un'accelerazione riduce quell'uscita. La
|
||||
commissione verso `fee_address` è quindi il 30% meno la riserva.
|
||||
- *Motivazione:* il pagamento spende di norma tutte e sole le monete del
|
||||
round, quindi non ha resto e non si potrebbe mai accelerare: un pagamento
|
||||
fermo in mempool terrebbe il round in `paying_out` per sempre (B-79). La
|
||||
riserva la paga la piattaforma perché è la piattaforma a scegliere il fee
|
||||
rate.
|
||||
- *Alternative scartate:* riserva presa dalla quota del vincitore (il
|
||||
montepremi annunciato scenderebbe ancora); CPFP da un'uscita del vincitore
|
||||
(richiede una seconda transazione e una moneta del vincitore); pagamento
|
||||
senza riserva e abbandono dopo un timeout (vietato da [ADR-51](ADR-51-dubbio-liberano-mai-fondi.md)).
|
||||
- *Conseguenze:* il montepremi annunciato (il 70% delle scommesse
|
||||
confermate) supera di poco quanto il vincitore riceve davvero; la
|
||||
differenza, la commissione del pagamento, non è nota finché il pagamento non
|
||||
è costruito. [ADR-29](ADR-29-senza-fee-address-apre-nessun-round.md) rifiuta di aprire round con una configurazione in cui
|
||||
un round da un partecipante non coprirebbe commissione del vincitore,
|
||||
riserva e una commissione sopra la soglia di polvere. Regole in
|
||||
[ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#4--pagamento) (B-79).
|
||||
|
||||
*Codice:* `app/wallet/psbt_builder.py:build_payout_transaction` ·
|
||||
*Test:* `tests/unit/test_payout_builder.py::test_payout_deducts_fee_only_from_winner_share`
|
||||
*Stato:* da implementare (B-79) · *Oggi:* nessuna riserva: il 30% va intero a
|
||||
`fee_address` e, nel caso normale, il pagamento non ha resto.
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-18 — L'accelerazione RBF la paga chi riceve il resto
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**L'accelerazione RBF la paga chi riceve il resto**: l'utente
|
||||
per scommesse e prelievi, la piattaforma (tramite la riserva di [ADR-17](ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md)) per
|
||||
i pagamenti. Le uscite verso la controparte non si toccano mai. La regola,
|
||||
con i casi in cui il resto non basta, è di
|
||||
[TX-11](../transazioni-e-riconciliazione.md#rbf-accelerare-una-transazione-lenta).
|
||||
- *Motivazione:* chi riceve il resto è chi ha scelto di spendere; ridurre
|
||||
l'uscita del destinatario cambierebbe l'importo che la controparte si
|
||||
aspetta (B-17).
|
||||
- *Conseguenze:* ogni transazione deve avere un'uscita che il mittente può
|
||||
ridurre: da qui [ADR-19](ADR-19-ogni-transazione-utente-conserva-uscita-resto.md) per le transazioni utente e la riserva di
|
||||
[ADR-17](ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md) per i pagamenti ([LIM-02]).
|
||||
|
||||
*Codice:* `app/tx/broadcast.py:bump_fee` · `app/tx/broadcast.py:_find_change_output` ·
|
||||
*Test:* `tests/unit/test_broadcast.py`
|
||||
@@ -0,0 +1,37 @@
|
||||
# ADR-19 — Ogni transazione utente conserva un'uscita di resto verso l'utente, e un'accelerazione che porterebbe quel resto sotto `DUST_LIMIT_SATS` (294 sat) lo elimina
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-87)
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Ogni transazione utente conserva un'uscita di resto verso
|
||||
l'utente, e un'accelerazione che porterebbe quel resto sotto
|
||||
`DUST_LIMIT_SATS` (294 sat) lo elimina.** Le regole sono di
|
||||
[BET-14](../scommesse-e-prelievi.md#cosa-succede) e
|
||||
[WDR-09](../scommesse-e-prelievi.md#prelievo) (resto minimo alla costruzione)
|
||||
e di [TX-11](../transazioni-e-riconciliazione.md#rbf-accelerare-una-transazione-lenta)
|
||||
(resto eliminato all'accelerazione, B-87).
|
||||
- *Motivazione:* un prelievo totale produceva una transazione a un'uscita
|
||||
che nessun RBF poteva salvare (B-62); aggiungere monete non avrebbe
|
||||
aiutato, perché la transazione le spendeva già tutte. Il resto minimo di
|
||||
294 sat però **non basta a restare accelerabile**: il primo aumento
|
||||
minimo (1 sat/vB sull'intera transazione, 141 sat per un ingresso e due
|
||||
uscite) lo porta a 153 sat, sotto la soglia di polvere, e ogni sostituzione
|
||||
successiva viene rifiutata dalle regole di inoltro (B-87). Garantisce
|
||||
l'esistenza dell'uscita, non la capacità di assorbire aumenti.
|
||||
- *Alternative scartate:* un resto minimo più grande (sottrae all'utente un
|
||||
importo che deve comunque poter prelevare, e sposta solo la soglia);
|
||||
aggiungere ingressi alla sostituzione (nel caso che conta, il prelievo
|
||||
totale, non ce ne sono altri).
|
||||
- *Conseguenze:* quando il resto scende sotto soglia, il suo valore va in
|
||||
commissione e la sostituzione è l'ultima possibile per quella transazione;
|
||||
l'utente perde al più 294 sat. **Il pagamento del round** non ha un resto
|
||||
utente: la sua uscita riducibile è la riserva di [ADR-17](ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md).
|
||||
|
||||
*Codice:* `app/wallet/psbt_builder.py:build_signed_transaction` · `app/withdrawals/service.py:request_withdrawal` · `app/tx/broadcast.py:bump_fee` ·
|
||||
*Test:* `tests/unit/test_psbt_builder.py::test_build_signed_transaction_refuses_an_amount_that_would_leave_no_change`, `tests/unit/test_withdrawals.py::test_full_balance_withdrawal_keeps_a_bumpable_change_output`, `tests/unit/test_withdrawals.py::test_a_bet_from_a_balance_equal_to_the_bet_is_refused`
|
||||
*Stato:* da implementare (B-87) · *Oggi:* `bump_fee` richiede solo che il
|
||||
resto superi l'aumento, e può lasciare un resto sotto soglia che nessun nodo
|
||||
inoltra.
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR-20 — Il prelievo minimo è il costo corrente della scommessa
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il prelievo minimo è il costo corrente della scommessa**, senza
|
||||
un campo di configurazione separato.
|
||||
- *Motivazione:* un parametro in meno da tenere coerente.
|
||||
- *Conseguenze:* alzare il costo della scommessa alza anche il prelievo
|
||||
minimo, con effetto immediato.
|
||||
|
||||
*Codice:* `app/withdrawals/service.py:request_withdrawal` ·
|
||||
*Test:* `tests/unit/test_withdrawals.py::test_withdrawal_rejects_amount_below_minimum`
|
||||
@@ -0,0 +1,33 @@
|
||||
# ADR-21 — Tre limiti distinti sul numero di monete e di giocatori:
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Economia del gioco
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Tre limiti distinti sul numero di monete e di giocatori:**
|
||||
|
||||
| Costante | Valore | Protegge da |
|
||||
|---|---|---|
|
||||
| `MAX_TX_INPUTS` | 50 | una transazione utente (scommessa, prelievo) che erode l'importo in commissioni per un indirizzo troppo frammentato; oltre, `too_many_inputs` |
|
||||
| `MAX_PAYOUT_TX_INPUTS` | 500 | un pagamento troppo grande per le regole di inoltro (~1470 ingressi a 100 kvB); un terzo del margine |
|
||||
| `MAX_PARTICIPANTS_PER_ROUND` | 400 | un round impagabile: il 401° giocatore è rifiutato con `round_full` **prima** che si muova denaro |
|
||||
|
||||
- *Motivazione:* il pagamento spende una moneta del montepremi per scommessa;
|
||||
usare per esso il limite utente rendeva impagabile ogni round oltre ~50
|
||||
giocatori e fermava la lotteria (B-52). Con un indirizzo per round
|
||||
([ADR-03](ADR-03-ogni-round-ha-proprio-indirizzo-montepremi.md), B-80) il pagamento spende solo le monete del round, quindi i 100
|
||||
di differenza fra 400 e 500 restano un margine fisso invece di consumarsi
|
||||
con le monete avanzate dai round precedenti (come accade oggi con
|
||||
l'indirizzo unico).
|
||||
- *Conseguenze:* «ogni round si può pagare» è un invariante verificato
|
||||
all'ingresso, non una scoperta al momento del pagamento. Il conteggio dei
|
||||
partecipanti include quelli non ancora confermati (direzione prudente). Un
|
||||
tetto opzionale al montepremi del round (`max_round_pool_sats`, 0 =
|
||||
disattivato) rifiuta con `round_full` anche le scommesse oltre quel valore
|
||||
(B-95); se e come legarlo al costo di un attacco al 51% è una decisione
|
||||
non ancora presa ([LIM-24]).
|
||||
|
||||
*Codice:* `app/wallet/psbt_builder.py:select_utxos` · `app/bets/service.py:place_bet` · `app/wallet/psbt_builder.py:build_payout_transaction` ·
|
||||
*Test:* `tests/unit/test_psbt_builder.py::test_select_utxos_never_exceeds_the_input_cap`, `tests/unit/test_bets.py::test_place_bet_rejects_the_bet_past_the_participant_cap`, `tests/unit/test_payout_builder.py::test_payout_at_the_participant_cap_stays_well_inside_relay_limits`
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-22 — Al più un round attivo, come invariante del database
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Al più un round attivo, come invariante del database.** Un
|
||||
indice unico su un'espressione costante, limitato agli stati attivi, fa
|
||||
fallire in modo pulito il secondo inserimento concorrente; chi perde adotta
|
||||
il round del vincitore (al massimo 3 tentativi). I round non si
|
||||
sovrappongono mai: il successivo si apre solo quando il precedente è
|
||||
`closed`.
|
||||
- *Alternative scartate:* rendere lo scheduler l'unico autore dei round (la
|
||||
prima scommessa dopo una pausa non avrebbe potuto aprirne uno, B-09).
|
||||
|
||||
*Codice:* `app/db/models.py:Round` · `app/rounds/service.py:open_new_round_if_needed` · `app/rounds/service.py:get_active_round` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py::test_the_database_refuses_a_second_active_round`, `tests/unit/test_rounds_service.py::test_losing_the_open_race_reuses_the_winning_round`
|
||||
@@ -0,0 +1,22 @@
|
||||
# ADR-23 — Il limite per scommettere è la scadenza del round stesso, non il suo stato nel database
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il limite per scommettere è la scadenza del round stesso, non
|
||||
il suo stato nel database**, e la chiusura «a semaforo giallo» aspetta che
|
||||
ogni scommessa già inviata sia confermata prima di estrarre. L'iscrizione è
|
||||
salvata con un compare-and-set sulla riga del round.
|
||||
- *Motivazione:* lo scheduler controlla ogni 5s, quindi lo stato può restare
|
||||
`open` oltre la scadenza; senza il compare-and-set una scommessa poteva
|
||||
finire nel montepremi ed essere esclusa dall'estrazione (B-53).
|
||||
- *Conseguenze:* l'attesa in `closing` finisce solo quando ogni scommessa è
|
||||
confermata o dimostrata morta ([ADR-51](ADR-51-dubbio-liberano-mai-fondi.md)); non ha un limite di tempo
|
||||
([LIM-03]). Dettagli in
|
||||
[ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#2--chiusura).
|
||||
|
||||
*Codice:* `app/rounds/service.py:round_accepts_bets` · `app/bets/service.py:place_bet` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_bets.py::test_place_bet_refuses_when_the_round_closed_between_the_check_and_the_commit`, `tests/unit/test_scheduler.py::test_close_and_draw_waits_when_a_bet_appears_after_the_tick_check`
|
||||
@@ -0,0 +1,62 @@
|
||||
# ADR-24 — L'estrazione è un commit-reveal fra il server e un blocco futuro
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-73)
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**L'estrazione è un commit-reveal fra il server e un blocco
|
||||
futuro** (decisione del 2026-09-28). All'apertura del round il server genera
|
||||
un segreto casuale di 32 byte (`server_seed`), lo salva e pubblica solo la
|
||||
sua impronta `server_seed_hash = sha256(server_seed)`. All'ingresso in
|
||||
`drawing` si fissano l'elenco dei partecipanti confermati, ordinato per txid
|
||||
della scommessa crescente, la sua impronta `participants_hash` (sha256 delle
|
||||
righe `txid:vout:indirizzo`) e l'altezza del blocco di estrazione
|
||||
(`draw_height`, [ADR-25](ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md)). Il seme è
|
||||
`sha256(server_seed || block_hash || participants_hash)` letto come intero,
|
||||
l'indice del vincitore è seme modulo numero di partecipanti. Dopo il
|
||||
salvataggio del vincitore il server rivela `server_seed` e pubblica la prova
|
||||
completa (`GET /rounds/{id}/proof`), da cui chiunque ricalcola l'esito.
|
||||
L'algoritmo resta un **componente sostituibile**: il resto del sistema non
|
||||
va architettato attorno alla sua forma, ma ogni sostituto deve mantenere le
|
||||
stesse quattro proprietà — impegno pubblicato prima delle scommesse, elenco
|
||||
dei partecipanti fissato prima del blocco, esito ricalcolabile dai dati
|
||||
pubblicati, esito non calcolabile da chi produce o trasmette i blocchi.
|
||||
- *Motivazione:* il vincitore non deve essere calcolabile da chi controlla
|
||||
la produzione dei blocchi: la potenza di calcolo di PLM (~22 TH/s) si può
|
||||
affittare. Un minatore che non conosce `server_seed` non sa chi vincerebbe
|
||||
con il suo blocco e quindi non ha motivo di scartarlo; l'operatore non
|
||||
può cambiare `server_seed` dopo averne pubblicato l'impronta e non conosce
|
||||
il blocco quando si impegna. L'ordine per txid e l'impronta dell'elenco
|
||||
rendono l'esito riproducibile e impediscono di aggiungere o togliere
|
||||
partecipanti dopo aver visto il blocco (B-73).
|
||||
- *Alternative scartate:*
|
||||
- *hash del solo blocco* (la forma precedente): un minatore con biglietti
|
||||
in gioco trattiene i blocchi finché uno lo fa vincere, e il server
|
||||
Electrum attivo sceglie quale blocco reale usare semplicemente non
|
||||
annunciandone uno;
|
||||
- *hash di più blocchi consecutivi*: chi produce l'ultimo blocco della
|
||||
serie può ancora scartarlo e riprovare, quindi resta manipolabile;
|
||||
- *segreti forniti dai giocatori* (commit-reveal fra partecipanti): chi non
|
||||
rivela blocca o condiziona l'estrazione, e servirebbe un'azione in più a
|
||||
ogni giocatore;
|
||||
- *sorgente di casualità esterna* (beacon): una dipendenza di rete in più,
|
||||
di cui fidarsi quanto del server.
|
||||
- *Conseguenze:* l'operatore, che conosce `server_seed`, può calcolare
|
||||
l'esito appena il blocco è noto: una collusione fra operatore e minatore
|
||||
resta possibile ed è accettata ([LIM-25]), perché l'operatore è già
|
||||
fidato come custode ([LIM-26]). `server_seed` non è esposto da nessun
|
||||
endpoint prima della rivelazione, e nel database è cifrato con la chiave
|
||||
della master xprv finché non viene rivelato
|
||||
([sicurezza.md](../sicurezza.md#sec-56) SEC-56): una copia del database da
|
||||
sola non basta a conoscerlo. Regole in
|
||||
[ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#3--estrazione),
|
||||
endpoint in [api.md](../api.md) (B-73).
|
||||
|
||||
*Codice:* `app/rounds/draw.py:draw_winner` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* `tests/unit/test_draw.py`
|
||||
*Stato:* da implementare (B-73) · *Oggi:* seme = hash del primo blocco
|
||||
confermato dopo la chiusura, partecipanti ordinati per `broadcast_at` (dato
|
||||
solo del server, senza criterio di pareggio), nessun impegno né prova
|
||||
pubblica.
|
||||
@@ -0,0 +1,38 @@
|
||||
# ADR-25 — Il blocco di estrazione è fissato per altezza, è successivo alla chiusura, e si usa solo quando è confermato e corroborato
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-73, B-81)
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il blocco di estrazione è fissato per altezza, è successivo
|
||||
alla chiusura, e si usa solo quando è confermato e corroborato.**
|
||||
All'ingresso in `drawing` si salva `draw_height = tip_at_close + 1`, che non
|
||||
cambia più. Si attende che quel blocco abbia `draw_confirmations` conferme
|
||||
(3 per default, fotografato all'apertura, [ADR-10](ADR-10-parametri-round-fotografati-all-apertura.md)), poi si chiede a tutti i
|
||||
server l'header *a quell'altezza*: serve l'accordo del quorum ([ADR-07](ADR-07-ogni-decisione-sposta-denaro-base-risposta.md)); in
|
||||
caso di disaccordo si attende e si richiede la stessa altezza, mai un blocco
|
||||
diverso. L'attesa non ha timeout (l'entropia dipende davvero da un blocco
|
||||
futuro) ma è osservabile (`draw_stalled`, `draw_waiting_since`) e
|
||||
riprendibile: `draw_height`, `participants_hash` e la posizione di ogni
|
||||
partecipante sono salvati, e lo scheduler riprende un round in `drawing`
|
||||
dopo un errore o un riavvio con lo stesso esito. Un'altezza `0` alla
|
||||
chiusura significa «sconosciuta»: la prima altezza appresa fa da base.
|
||||
- *Motivazione:* un hash già pubblico mentre le scommesse erano aperte
|
||||
renderebbe il vincitore prevedibile (B-63); un header fabbricato
|
||||
sceglierebbe il vincitore (B-28); un'altezza non fissata lascia al server
|
||||
attivo la scelta di quale blocco reale usare, e passare a «un blocco
|
||||
ulteriore» dopo una corroborazione fallita gli dà un secondo tentativo
|
||||
(B-73). Le conferme sul blocco di estrazione evitano che una
|
||||
riorganizzazione cambi il seme dopo l'estrazione. Uno stato tenuto solo in
|
||||
memoria bloccava il round in `drawing` a ogni eccezione (B-81).
|
||||
- *Alternative scartate:* timeout sull'attesa con ripiego su un altro blocco
|
||||
(riaprirebbe la scelta del blocco).
|
||||
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` · `app/rounds/scheduler.py:RoundScheduler._adopt_baseline_tip` · `app/rounds/scheduler.py:RoundScheduler._tick` ·
|
||||
*Test:* `tests/unit/test_scheduler.py::test_wait_for_next_block_never_seeds_the_draw_from_a_pre_close_block`, `tests/unit/test_scheduler.py::test_wait_for_next_block_retries_after_a_failed_corroboration`
|
||||
*Stato:* da implementare (B-73, B-81) · *Oggi:* si usa il primo blocco oltre
|
||||
`tip_at_close` visto dal server attivo, a 1 conferma; dopo una
|
||||
corroborazione fallita si passa al blocco successivo; `tip_at_close` e i
|
||||
partecipanti vivono solo in memoria e `_tick` salta i round in `drawing`.
|
||||
@@ -0,0 +1,39 @@
|
||||
# ADR-26 — Validazione degli header concatenata, senza ricalcolo della difficoltà
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-74, B-85)
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Validazione degli header concatenata, senza ricalcolo della
|
||||
difficoltà.** Un header deve dichiarare un bersaglio non più facile del
|
||||
limite di rete (`powLimit`, `0x00000000ffff…`), rispettare quel bersaglio e
|
||||
concatenarsi alla catena che teniamo: su un avanzamento di più blocchi si
|
||||
scaricano gli header intermedi e si verifica tutta la catena. Il listener
|
||||
tiene gli ultimi 10 header; un header che non si concatena alla punta ma a
|
||||
uno di essi è una riorganizzazione da seguire ([ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)), non un server
|
||||
ostile. Un header senza `hex` è ignorato senza chiudere la sessione. Regole
|
||||
in [connessione-electrum.md](../connessione-electrum.md#4-header-validati-non-creduti)
|
||||
(B-74, B-85).
|
||||
- *Motivazione:* il controllo «rispetta il bersaglio che dichiara», da solo,
|
||||
**non è una difesa**: il bersaglio lo sceglie chi produce l'header, e con
|
||||
`bits=0x207fffff` un header passa senza lavoro. Un salto di più blocchi non
|
||||
collegato alla punta permetteva di fissare un'altezza enorme, che non
|
||||
regredisce mai, e congelare ogni estrazione (B-74). Trattare ogni
|
||||
riorganizzazione di un blocco come un server ostile chiudeva la sessione
|
||||
su tutti i server fino al blocco successivo.
|
||||
- *Alternative scartate:* validazione completa della catena con le regole di
|
||||
ricalcolo della difficoltà di PLM (fuori scopo: servirebbe la storia delle
|
||||
difficoltà, e il commit-reveal di [ADR-24](ADR-24-l-estrazione-commit-reveal-fra-server.md) toglie a un header fabbricato il
|
||||
potere di scegliere il vincitore).
|
||||
- *Conseguenze:* un header a difficoltà minima costa ancora poco lavoro
|
||||
([LIM-14]); a fermare una catena fabbricata sono la concatenazione, il
|
||||
quorum ([ADR-07](ADR-07-ogni-decisione-sposta-denaro-base-risposta.md)) e il fatto che il seme dipende anche da `server_seed`.
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · `app/rounds/draw.py:header_meets_its_own_target` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py::test_apply_header_rejects_one_that_does_not_chain_from_the_tip`, `tests/unit/test_electrum_listener.py::test_apply_header_ignores_a_competing_header_at_the_current_tip_height`
|
||||
*Stato:* da implementare (B-74, B-85) · *Oggi:* nessun confronto con
|
||||
`powLimit`, concatenazione verificata solo sull'avanzamento di un blocco
|
||||
esatto, un header concorrente alla stessa altezza ignorato, un header che non
|
||||
si concatena chiude la sessione.
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-27 — Pausa di cortesia fra un round e l'altro
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Pausa di cortesia fra un round e l'altro**
|
||||
(`round_cooldown_seconds`, 30s), contata dalla **conferma del pagamento**, non
|
||||
dall'inizio della chiusura, così i giocatori vedono l'esito. Le regole sono di
|
||||
[RND-04](../ciclo-di-vita-del-round.md#0--apertura) (quando si apre il round
|
||||
successivo) e [RND-28](../ciclo-di-vita-del-round.md#4--pagamento) (da quando
|
||||
si conta).
|
||||
- *Motivazione:* non è nel diagramma originale; senza pausa il round
|
||||
successivo si apriva prima che i client avessero mostrato il risultato.
|
||||
|
||||
*Codice:* `app/rounds/confirmation.py:_on_payout_confirmed` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py::test_withholds_new_round_during_cooldown`
|
||||
@@ -0,0 +1,24 @@
|
||||
# ADR-28 — La pausa per manutenzione ferma solo il round successivo
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**La pausa per manutenzione ferma solo il round successivo.**
|
||||
È un'azione esplicita dell'operatore con endpoint propri e registrati in
|
||||
audit (`lottery_paused`, `lottery_resumed`), non un campo di configurazione;
|
||||
il round in corso chiude, estrae e paga come qualunque altro round.
|
||||
- *Motivazione:* sospendere a metà un round con denaro nel montepremi
|
||||
creerebbe proprio lo stato bloccato che la pausa vuole evitare.
|
||||
- *Conseguenze:* la stessa pausa è anche il modo in cui il sistema chiede
|
||||
l'intervento dell'operatore, con un motivo (`paused_reason`): una
|
||||
riorganizzazione profonda ([ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md), B-85), un pagamento il cui vincitore
|
||||
non ha più la scommessa in catena (B-82), una transazione abbandonata che
|
||||
conferma in ritardo ([ADR-51](ADR-51-dubbio-liberano-mai-fondi.md), B-78), un avvio con `START_PAUSED=true` dopo
|
||||
un ripristino (B-90). Solo l'operatore la toglie. Anche in questi casi il
|
||||
round in corso prosegue, salvo il pagamento trattenuto di B-82.
|
||||
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery` · `app/api/routes/admin.py:resume_lottery` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py::test_withholds_new_round_while_paused`, `tests/unit/test_rounds_service.py::test_pause_does_not_interrupt_a_round_in_progress`, `tests/unit/test_admin.py::test_admin_can_pause_and_resume_the_lottery`
|
||||
@@ -0,0 +1,24 @@
|
||||
# ADR-29 — Senza `fee_address` non si apre nessun round
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-79)
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Senza `fee_address` non si apre nessun round.** Il campo non ha
|
||||
valore predefinito: l'operatore deve impostare il proprio. Finché manca, nessun
|
||||
round si apre (`lottery_configured = false` su `GET /rounds/current`, avviso
|
||||
in `/admin`); un round già in corso invece chiude, estrae e paga anche se
|
||||
l'indirizzo viene cancellato. Qualunque altra condizione che renderebbe un
|
||||
round impagabile va aggiunta nello stesso punto.
|
||||
- *Motivazione:* un round aperto senza `fee_address` raccoglieva scommesse e
|
||||
solo al pagamento scopriva di non poter essere pagato (B-66).
|
||||
- *Conseguenze:* nello stesso punto si aggiunge il controllo che un round
|
||||
con un solo partecipante copra commissione del vincitore, riserva per
|
||||
l'RBF e una commissione sopra la soglia di polvere ([ADR-17](ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md), B-79).
|
||||
|
||||
*Codice:* `app/rounds/service.py:rounds_can_open` · `app/rounds/config.py:get_round_config` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py::test_withholds_new_round_while_no_fee_address_is_configured`, `tests/unit/test_rounds_service.py::test_a_round_in_progress_survives_the_fee_address_being_cleared`, `tests/unit/test_rounds_route.py::test_lottery_configured_flags_a_missing_fee_address`
|
||||
*Stato:* da implementare (B-79) · *Oggi:* `rounds_can_open` controlla solo
|
||||
`fee_address`.
|
||||
@@ -0,0 +1,20 @@
|
||||
# ADR-30 — Il montepremi annunciato è quello che sarà pagato
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Round ed estrazione
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il montepremi annunciato è quello che sarà pagato**: il 70%
|
||||
delle sole scommesse confermate, affiancato dai valori «in attesa» che
|
||||
includono le scommesse in volo (valori inclusivi, non differenze).
|
||||
- *Motivazione:* mai annunciare più di quanto l'estrazione e il pagamento
|
||||
considereranno (B-65), senza nascondere al giocatore la propria scommessa
|
||||
appena fatta.
|
||||
- *Conseguenze:* se prima del pagamento una scommessa confermata risulta
|
||||
sparita per una riorganizzazione, il partecipante esce dal calcolo
|
||||
(`reorged_out`) e il pagato scende sotto l'annunciato ([ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md), B-82).
|
||||
|
||||
*Codice:* `app/api/routes/rounds.py:current_round` ·
|
||||
*Test:* `tests/unit/test_rounds_route.py::test_advertised_jackpot_covers_only_the_bets_that_will_be_paid`
|
||||
@@ -0,0 +1,25 @@
|
||||
# ADR-31 — Tutto ciò che spende denaro si scrive prima di inviarlo
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-76, B-77)
|
||||
- **Area:** Denaro in movimento
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Tutto ciò che spende denaro si scrive prima di inviarlo** (stato
|
||||
`building`, monete già segnate come spese) e si risolve contro la catena
|
||||
dopo; nessuna sessione del database resta aperta durante una chiamata di
|
||||
rete. Vale anche per le sostituzioni RBF: la nuova versione si salva come
|
||||
candidata prima dell'invio (B-77). Solo un **rifiuto esplicito** della rete
|
||||
annulla tutto e risponde **502**; un invio dall'esito incerto (timeout,
|
||||
connessione caduta dopo la scrittura) lascia la riga `building` al
|
||||
riconciliatore e risponde **202** ([ADR-51](ADR-51-dubbio-liberano-mai-fondi.md)).
|
||||
- *Motivazione:* un crash fra invio e salvataggio lasciava monete spese in
|
||||
catena senza traccia (B-08, B-25). Dettagli in
|
||||
[transazioni-e-riconciliazione.md](../transazioni-e-riconciliazione.md).
|
||||
|
||||
*Codice:* `app/bets/service.py:place_bet` · `app/withdrawals/service.py:request_withdrawal` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/tx/broadcast.py:bump_fee` · `app/tx/reconcile.py:reconcile_once` ·
|
||||
*Test:* `tests/unit/test_bets.py::test_bet_is_persisted_before_it_is_broadcast`, `tests/unit/test_scheduler.py::test_trigger_payout_persists_before_broadcasting`, `tests/unit/test_reconcile.py`
|
||||
*Stato:* da implementare (B-76, B-77) · *Oggi:* qualunque eccezione
|
||||
dell'invio, timeout compreso, annulla tutto con 502; `bump_fee` invia la
|
||||
sostituzione prima di salvarla.
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-32 — Il pagamento si ritenta da solo, mai due volte
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Denaro in movimento
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il pagamento si ritenta da solo, mai due volte**: un tentativo
|
||||
ogni 60s, usando l'ultimo `payout_failed` in audit come orologio; prima di
|
||||
costruire si rifiuta se esiste già un pagamento `building`/`pending` per il
|
||||
round, e si escludono le monete impegnate da pagamenti non risolti.
|
||||
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._retry_payout_if_due` · `app/rounds/scheduler.py:_reserved_payout_outpoints` ·
|
||||
*Test:* `tests/unit/test_scheduler.py::test_trigger_payout_skips_when_already_in_flight`, `tests/unit/test_scheduler.py::test_tick_throttles_retry_after_a_recent_payout_failure`, `tests/unit/test_scheduler.py::test_reserved_payout_outpoints_excludes_utxos_claimed_by_a_stale_payout`
|
||||
@@ -0,0 +1,17 @@
|
||||
# ADR-33 — Scommessa e prelievo si escludono solo tramite il blocco per-utente
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Denaro in movimento
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Scommessa e prelievo si escludono solo tramite il blocco
|
||||
per-utente** sulla *costruzione*: un prelievo è accettato anche mentre una
|
||||
scommessa attende conferma, purché le monete confermate e libere lo coprano.
|
||||
- *Motivazione:* il rischio è che scelgano la stessa moneta, già escluso dal
|
||||
blocco e dal fatto che la selezione salta le monete segnate come spese;
|
||||
congelare il resto del saldo non aggiungerebbe sicurezza (B-70).
|
||||
|
||||
*Codice:* `app/tx/locks.py:UserLocks` · `app/api/routes/bets.py:create_bet` · `app/api/routes/withdrawals.py:create_withdrawal` ·
|
||||
*Test:* `tests/unit/test_withdrawals.py::test_withdrawal_is_allowed_while_a_bet_is_still_unconfirmed`
|
||||
@@ -0,0 +1,17 @@
|
||||
# ADR-34 — Saldo spendibile e saldo mostrato sono due numeri distinti
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Denaro in movimento
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Saldo spendibile e saldo mostrato sono due numeri distinti.**
|
||||
Si spende solo il confermato (`balance_sats`); si mostra quello che include i
|
||||
resti ancora in volo (`pending_balance_sats`, in ambra finché non conferma).
|
||||
Un prelievo coperto solo dal secondo riceve `balance_pending_confirmation`.
|
||||
- *Motivazione:* la selezione a monete intere sottrae l'intera moneta al
|
||||
saldo confermato già all'invio, molto più di quanto si stia spostando.
|
||||
|
||||
*Codice:* `app/wallet/balance.py:compute_pending_balance` · `app/wallet/balance.py:recompute_balance` · `app/api/routes/users.py:me` ·
|
||||
*Test:* `tests/unit/test_balance.py`, `tests/unit/test_withdrawals.py::test_withdrawal_distinguishes_pending_from_truly_insufficient_balance`
|
||||
@@ -0,0 +1,14 @@
|
||||
# ADR-35 — Aritmetica solo in satoshi interi
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Denaro in movimento
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Aritmetica solo in satoshi interi**, anche per i valori letti
|
||||
dalla rete: l'importo di una moneta spesa si ricava dalla transazione grezza,
|
||||
non dal valore decimale restituito dal server.
|
||||
|
||||
*Codice:* `app/tx/broadcast.py:_prevout_amount` · `app/wallet/psbt_builder.py:estimate_vsize` ·
|
||||
*Test:* `tests/unit/test_broadcast.py::test_bump_fee_holds_no_session_open_during_network_calls`
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-36 — Password con Argon2, sessioni con JWT HS256 da 24 ore, revocabili
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Password con Argon2, sessioni con JWT HS256 da 24 ore,
|
||||
revocabili.** Il token porta la versione dell'account (`tv`); cambio password
|
||||
e reset admin la incrementano e invalidano tutte le sessioni precedenti. Un
|
||||
token senza `tv` vale come versione 0, così il rilascio non ha disconnesso
|
||||
nessuno. Argon2 gira fuori dall'event loop.
|
||||
- *Motivazione:* un reset per account compromesso deve buttare fuori subito
|
||||
l'attaccante (B-34); Argon2 inline bloccava l'intero processo, attività in
|
||||
background comprese (B-55).
|
||||
|
||||
*Codice:* `app/auth/security.py:create_access_token` · `app/auth/security.py:decode_access_token` · `app/auth/dependencies.py:get_current_user` · `app/auth/security.py:verify_password_async` ·
|
||||
*Test:* `tests/unit/test_security.py`, `tests/unit/test_users.py::test_change_password_invalidates_the_old_token_but_not_the_new_one`
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR-37 — Nessun recupero password, solo reset
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Nessun recupero password, solo reset.** Non si raccoglie
|
||||
nessuna email. L'utente autenticato può *cambiare* la password fornendo
|
||||
quella attuale; chi l'ha persa si rivolge all'operatore, che con
|
||||
`POST /admin/users/{id}/reset-password` ne genera una casuale, mostrata una
|
||||
volta sola e registrata in audit (`admin_password_reset`).
|
||||
|
||||
*Codice:* `app/api/routes/users.py:change_password` · `app/api/routes/admin.py:reset_user_password` ·
|
||||
*Test:* `tests/unit/test_users.py::test_change_password_requires_current_password`, `tests/unit/test_admin.py::test_admin_resets_user_password`
|
||||
@@ -0,0 +1,20 @@
|
||||
# ADR-38 — Gli username sono un unico spazio di nomi senza distinzione fra maiuscole e minuscole
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Gli username sono un unico spazio di nomi senza distinzione fra
|
||||
maiuscole e minuscole**, garantito da un indice unico su `lower(username)`;
|
||||
il nome resta salvato come digitato (è ciò che mostrano `/admin` e l'audit).
|
||||
Sono ammessi solo caratteri ASCII `[A-Za-z0-9_.-]`, 3–32. La migrazione si
|
||||
rifiuta di partire se due account esistenti differiscono solo per le
|
||||
maiuscole, invece di sceglierne uno da rinominare: entrambi potrebbero avere
|
||||
fondi.
|
||||
- *Motivazione:* «Bob» e «bob» erano due account che condividevano il
|
||||
contatore dei tentativi e un comodo vettore di impersonazione (B-57).
|
||||
|
||||
*Codice:* `app/db/models.py:User` · `app/auth/routes.py:register` · `app/auth/routes.py:login` ·
|
||||
*Test:* `tests/unit/test_auth.py::test_registration_refuses_a_username_differing_only_by_case`, `tests/unit/test_auth.py::test_the_database_itself_rejects_a_case_variant`
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR-39 — L'esportazione della chiave privata WIF di un utente è voluta, non una vulnerabilità
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**L'esportazione della chiave privata WIF di un utente è
|
||||
voluta, non una vulnerabilità.** Il server possiede già la master key:
|
||||
l'endpoint espone via API solo ciò che un operatore potrebbe comunque
|
||||
ottenere con uno script (ad esempio per recuperare fondi bloccati). Ogni
|
||||
accesso scrive `admin_privkey_accessed`, e quella registrazione non va tolta.
|
||||
|
||||
*Codice:* `app/api/routes/admin.py:user_privkey` · `app/wallet/hd.py:derive_user_wif` ·
|
||||
*Test:* `tests/unit/test_admin.py::test_admin_exports_user_privkey`
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-40 — L'amministrazione usa un unico token condiviso
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**L'amministrazione usa un unico token condiviso**
|
||||
(`X-Admin-Token` confrontato a tempo costante sui byte UTF-8 con
|
||||
`ADMIN_TOKEN`), non un login per operatore. `/admin` mostra una schermata di
|
||||
inserimento del token, **non è collegato da `/`** (né `/` da `/admin`) ed è in
|
||||
italiano, non tradotto.
|
||||
- *Conseguenze:* l'audit registra *cosa* è cambiato, mai *chi* l'ha fatto
|
||||
([LIM-06]).
|
||||
|
||||
*Codice:* `app/api/routes/admin.py:require_admin` · `app/main.py:admin_panel` · `app/static/admin.html` ·
|
||||
*Test:* `tests/unit/test_admin.py::test_admin_requires_token`, `tests/unit/test_admin.py::test_admin_rejects_non_ascii_token_with_403_not_500`
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-41 — Limitazione dei tentativi solo su login e registrazione
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-94)
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Limitazione dei tentativi solo su login e registrazione**, in
|
||||
memoria. Il login usa un ritardo esponenziale per username (dopo 5 errori) e
|
||||
per IP (dopo 20, soglia più alta per non punire una rete condivisa); un
|
||||
login riuscito azzera solo il contatore dello username. La registrazione usa
|
||||
invece una **quota** di 5 account per IP all'ora, consumata solo dagli
|
||||
account creati davvero. I contatori sono limitati in numero.
|
||||
- *Motivazione:* indovinare una password e creare account in massa sono due
|
||||
problemi diversi (B-33, B-58); il ritardo punisce i tentativi, la quota
|
||||
limita la produzione.
|
||||
- *Conseguenze:* nessun limite su scommesse, prelievi, admin ([LIM-07]).
|
||||
Il cambio password (limitato e registrato come `password_changed`) e le
|
||||
segnalazioni di bug (limitate per IP) si aggiungono con B-94; ogni campo
|
||||
delle credenziali ha una lunghezza massima di 256 caratteri
|
||||
([sicurezza.md](../sicurezza.md#limiti-di-frequenza), B-94).
|
||||
|
||||
*Codice:* `app/auth/rate_limit.py:AuthRateLimiters` · `app/auth/rate_limit.py:RateLimiter` · `app/auth/rate_limit.py:RollingQuota` · `app/auth/routes.py:login` · `app/api/routes/users.py:change_password` · `app/api/routes/bug_reports.py:create_bug_report` ·
|
||||
*Test:* `tests/unit/test_auth.py`, `tests/unit/test_rate_limit.py`
|
||||
*Stato:* da implementare (B-94) · *Oggi:* cambio password e segnalazioni di
|
||||
bug non hanno limiti né lunghezze massime, e il cambio password non scrive
|
||||
audit.
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR-42 — L'IP del client è l'ultimo elemento di `X-Forwarded-For`
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**L'IP del client è l'ultimo elemento di `X-Forwarded-For`**, e
|
||||
Caddy sovrascrive l'header con il vero indirizzo del peer. Un solo proxy
|
||||
fidato sta davanti all'applicazione, che non è esposta direttamente.
|
||||
- *Motivazione:* il primo elemento è scelto dal client; leggerlo rendeva
|
||||
aggirabile ogni controllo per IP (B-54). Basta una delle due misure.
|
||||
|
||||
*Codice:* `app/api/client_ip.py:client_ip` · `Caddyfile` ·
|
||||
*Test:* `tests/unit/test_client_ip.py`, `tests/unit/test_caddyfile_security_headers.py::test_forwarded_for_is_overwritten_with_the_real_peer`
|
||||
@@ -0,0 +1,18 @@
|
||||
# ADR-43 — Header di sicurezza nel proxy, con `'unsafe-inline'` accettato
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Header di sicurezza nel proxy, con `'unsafe-inline'`
|
||||
accettato**: HSTS, `nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy` e una
|
||||
CSP `default-src 'self'` più Google Fonts. Script e stili inline restano
|
||||
permessi perché entrambe le SPA usano `onclick` e `style=""`; rimuoverli è un
|
||||
refactoring a parte. La documentazione automatica dell'API (`/docs`,
|
||||
`/redoc`, `/openapi.json`) è spenta per default (`ENABLE_API_DOCS`).
|
||||
- *Conseguenze:* [LIM-09].
|
||||
|
||||
*Codice:* `Caddyfile` · `app/main.py` · `app/config.py:Settings` ·
|
||||
*Test:* `tests/unit/test_caddyfile_security_headers.py`, `tests/unit/test_docs_disabled.py`
|
||||
@@ -0,0 +1,15 @@
|
||||
# ADR-44 — `/qr/{address}` è pubblico ma limitato
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**`/qr/{address}` è pubblico ma limitato**: nessuna
|
||||
autenticazione (lo usa un `<img>`, che non può portare il token), ma
|
||||
l'indirizzo è validato davvero, la lunghezza è limitata, la resa è in cache
|
||||
(512 voci) e fuori dall'event loop.
|
||||
|
||||
*Codice:* `app/api/routes/qr.py:address_qr` ·
|
||||
*Test:* `tests/unit/test_qr.py`
|
||||
@@ -0,0 +1,16 @@
|
||||
# ADR-45 — Segnalazioni di bug aperte a tutti, triage in admin
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Segnalazioni di bug aperte a tutti, triage in admin.** Si può
|
||||
inviare una segnalazione da autenticati (viene attribuita all'utente) o no;
|
||||
il testo va scritto in inglese, e la pagina lo dice nella lingua del
|
||||
visitatore. Gli stati sono `open` → `read` → `resolved`, e ogni cambio reale
|
||||
di stato è registrato in audit.
|
||||
|
||||
*Codice:* `app/api/routes/bug_reports.py:create_bug_report` · `app/api/routes/admin.py:update_bug_report_status` ·
|
||||
*Test:* `tests/unit/test_bug_reports.py`
|
||||
@@ -0,0 +1,23 @@
|
||||
# ADR-46 — Il canale SSE è un'aggiunta al polling, non un sostituto
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Interfacce, API e lingue
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il canale SSE è un'aggiunta al polling, non un sostituto.**
|
||||
Nessun contenuto e nessuna autenticazione: è solo un «è cambiato qualcosa,
|
||||
ricarica»; la personalizzazione resta negli endpoint REST autenticati. Un
|
||||
unico segnale per tutti (non per utente), una coda da 1 elemento per client
|
||||
così le notifiche ridondanti si fondono, tetto di 500 connessioni globali e 5
|
||||
per IP (oltre, si chiude la connessione più vecchia dello stesso IP). Le
|
||||
pagine ricaricano anche all'evento `open` di ogni riconnessione.
|
||||
- *Alternative scartate:* canale per utente (servirebbe autenticazione sul
|
||||
flusso e sapere a chi interessa ogni evento); Redis pub/sub (solo quando ci
|
||||
saranno più processi).
|
||||
- *Conseguenze:* se il canale cade o è bloccato, le pagine continuano a
|
||||
funzionare con il solo polling.
|
||||
|
||||
*Codice:* `app/rounds/events.py:RoundEventBroadcaster` · `app/api/routes/rounds.py:round_stream` ·
|
||||
*Test:* `tests/unit/test_round_events.py`
|
||||
@@ -0,0 +1,19 @@
|
||||
# ADR-47 — L'API parla una sola lingua e risponde con codici
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Interfacce, API e lingue
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**L'API parla una sola lingua e risponde con codici.** Ogni
|
||||
errore rivolto all'utente ha `detail = {code, message, params}`: `message` è
|
||||
in inglese per chi non usa la dashboard, `code` è ciò che il frontend traduce
|
||||
(`error.<code>`), `params` porta i valori (gli importi come `*_sats`). Anche
|
||||
l'errore 500 generico ha questa forma (`internal_error`) e non espone il
|
||||
testo dell'eccezione.
|
||||
- *Conseguenze:* aggiungere un errore significa dargli un codice e
|
||||
aggiungere `error.<code>` in tutte le 7 lingue (vedi [api.md](../api.md)).
|
||||
|
||||
*Codice:* `app/api/errors.py:ApiError` · `app/api/errors.py:http_error` · `app/main.py:log_unhandled_exception` ·
|
||||
*Test:* `tests/unit/test_rounds_route.py::test_unhandled_errors_use_the_structured_detail_shape`
|
||||
@@ -0,0 +1,20 @@
|
||||
# ADR-48 — Le pagine per l'utente (`/` e `/report-bug`) sono in 7 lingue
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Interfacce, API e lingue
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Le pagine per l'utente (`/` e `/report-bug`) sono in 7 lingue**
|
||||
(en, it, es, fr, de, ru, zh) da un'unica tabella piatta, senza build né
|
||||
download. Lingua scelta da `localStorage.plm_lang`, poi dal browser, poi
|
||||
inglese; nessun'altra lingua di riserva oltre l'inglese. Il selettore di `/`
|
||||
sta nella barra di stato della catena, non nella barra di navigazione
|
||||
(nascosta prima del login). Un elemento è tradotto *o* dagli attributi
|
||||
`data-i18n` *o* dal codice con `t()`, mai da entrambi. Tutte le lingue devono
|
||||
avere esattamente le stesse chiavi.
|
||||
- *Conseguenze:* la parità delle chiavi non è verificata da un test
|
||||
([LIM-21]).
|
||||
|
||||
*Codice:* `app/static/i18n.js:t` · `app/static/i18n.js:applyStaticTranslations` · `app/static/i18n.js:setLanguage` · `app/static/app.js:onLanguageChange`
|
||||
@@ -0,0 +1,12 @@
|
||||
# ADR-49 — Il pannello `/admin` e la pagina `/guida` non sono tradotti
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Interfacce, API e lingue
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Il pannello `/admin` e la pagina `/guida` non sono tradotti**
|
||||
(sono in italiano, per l'operatore).
|
||||
|
||||
*Codice:* `app/static/admin.html` · `app/static/guida.html`
|
||||
@@ -0,0 +1,20 @@
|
||||
# ADR-50 — Frontend statici, senza framework né build
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-94)
|
||||
- **Area:** Interfacce, API e lingue
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Frontend statici, senza framework né build**: due SPA servite
|
||||
direttamente da FastAPI, che parlano con la stessa API JSON (nessuna
|
||||
separazione fra API utente e admin oltre al controllo del token). Le pagine
|
||||
HTML hanno `Cache-Control: no-store`, i file JS e CSS `Cache-Control:
|
||||
no-cache` (il browser li riconvalida a ogni caricamento, così un
|
||||
aggiornamento non lascia una pagina nuova con uno script vecchio;
|
||||
[architettura.md](../architettura.md#allimport), B-94).
|
||||
|
||||
*Codice:* `app/main.py:index_page` · `app/main.py:admin_panel` · `app/static/app.js` · `app/static/admin.js`
|
||||
*Stato:* da implementare (B-94) · *Oggi:* solo le quattro pagine HTML hanno
|
||||
`Cache-Control` (`no-store`); JS e CSS, serviti da `StaticFiles`, non ne
|
||||
hanno.
|
||||
@@ -0,0 +1,43 @@
|
||||
# ADR-51 — Nel dubbio non si liberano mai i fondi
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-76, B-77, B-78)
|
||||
- **Area:** Denaro in movimento
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Nel dubbio non si liberano mai i fondi** (decisione del
|
||||
2026-09-28). Una riga che sposta denaro (scommessa, prelievo, pagamento) si
|
||||
annulla, liberandone le monete, solo con una **prova**: un rifiuto esplicito
|
||||
della rete all'invio, oppure i suoi ingressi spesi da un'altra transazione
|
||||
confermata (corroborata dal quorum), oppure nessun server del quorum che
|
||||
conosca alcun txid della riga *e* una ritrasmissione dell'ultima versione a
|
||||
tutti i server rifiutata per ingressi mancanti o già spesi. Mai per un
|
||||
timeout, per l'età della riga o per il silenzio di un solo server. Si
|
||||
seguono tutti i txid che una riga ha avuto (ogni versione RBF): la conferma
|
||||
di uno qualsiasi conferma la riga. Le righe `failed` restano sorvegliate per
|
||||
14 giorni; una conferma tardiva scrive `pending_tx_resurrected` e mette in
|
||||
pausa la lotteria ([ADR-28](ADR-28-pausa-manutenzione-ferma-round-successivo.md)). Regole in
|
||||
[transazioni-e-riconciliazione.md](../transazioni-e-riconciliazione.md#riconciliazione)
|
||||
(B-76, B-77, B-78).
|
||||
- *Motivazione:* liberare le monete di una transazione che poi conferma è
|
||||
l'errore peggiore possibile: la scommessa finisce nel montepremi senza
|
||||
partecipante, il prelievo parte mentre la riga dice `failed`, il pagamento
|
||||
viene ricostruito e il vincitore pagato due volte. Tutti e tre accadevano
|
||||
con un invio andato in timeout (B-76), con la conferma di una versione RBF
|
||||
precedente (B-77) o con un riavvio su un server che non aveva ancora visto
|
||||
la transazione (B-78).
|
||||
- *Alternative scartate:* abbandono a tempo (la regola precedente: 120s per
|
||||
`building`, 6 ore per `pending`), che trasforma un ritardo di rete in una
|
||||
perdita; abbandono sul parere del solo server attivo.
|
||||
- *Conseguenze:* una transazione ferma ma viva non si abbandona mai: la si
|
||||
accelera ([ADR-18](ADR-18-l-accelerazione-rbf-paga-chi-riceve.md)); un round può restare in `closing` o `paying_out`
|
||||
finché la rete non decide ([LIM-02], [LIM-03]). Il denaro in dubbio resta
|
||||
congelato e visibile, mai duplicato.
|
||||
|
||||
*Codice:* `app/tx/reconcile.py:reconcile_once` · `app/tx/reconcile.py:_is_due` · `app/bets/service.py:place_bet` · `app/withdrawals/service.py:request_withdrawal` · `app/tx/confirmation.py:poll_once` · `app/tx/broadcast.py:bump_fee` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-76, B-77, B-78) · *Oggi:* l'abbandono avviene
|
||||
quando la cronologia del solo server attivo non contiene il txid corrente,
|
||||
dopo 120s per `building` e 6 ore per `pending`; si segue solo l'ultimo txid;
|
||||
le righe `failed` non sono più controllate.
|
||||
@@ -0,0 +1,49 @@
|
||||
# ADR-52 — Le riorganizzazioni della catena si contengono invece di ignorarle
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-82, B-85)
|
||||
- **Area:** Conferme e fiducia nella rete
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Le riorganizzazioni della catena si contengono invece di
|
||||
ignorarle** (decisione del 2026-09-28). Tre misure:
|
||||
- *si rilevano*: il listener tiene gli ultimi 10 header e segue una
|
||||
biforcazione fino al punto comune; ogni riorganizzazione scrive
|
||||
`chain_reorg_detected` con profondità e altezze. Una biforcazione che non
|
||||
trova il punto comune entro 10 header è trattata come un header non
|
||||
valido (fine della sessione e rotazione, [ADR-26](ADR-26-validazione-degli-header-concatenata-senza-ricalcolo.md));
|
||||
- *si fermano quelle profonde*: una profondità ≥ 2 mette in pausa la
|
||||
lotteria (`paused_reason = 'reorg'`, [ADR-28](ADR-28-pausa-manutenzione-ferma-round-successivo.md)) finché l'operatore non la
|
||||
riprende;
|
||||
- *si paga solo ciò che è in catena*: prima di costruire un pagamento le
|
||||
monete dell'indirizzo del round si verificano con il quorum; i
|
||||
partecipanti la cui scommessa non c'è più diventano `reorged_out` e la
|
||||
ripartizione 70/30 si calcola su ciò che resta. Se manca la scommessa del
|
||||
vincitore, il pagamento è trattenuto (`payout_blocked`) e la lotteria va in
|
||||
pausa.
|
||||
|
||||
Regole in [connessione-electrum.md](../connessione-electrum.md#4-header-validati-non-creduti)
|
||||
e [ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#4--pagamento)
|
||||
(B-82, B-85).
|
||||
- *Motivazione:* la regola precedente («1 conferma, nessun rollback»)
|
||||
presupponeva riorganizzazioni rare e innocue. Con la potenza di PLM in
|
||||
affitto una riorganizzazione è un attacco possibile, e senza queste misure
|
||||
una sola scommessa sparita rende il round impagabile per sempre (B-82),
|
||||
mentre una riorganizzazione di un blocco, normale in qualunque catena,
|
||||
veniva scambiata per un server ostile (B-74).
|
||||
- *Alternative scartate:* ignorare le riorganizzazioni (la regola
|
||||
precedente); un rollback completo (ricalcolare saldi già spesi e ripetere
|
||||
estrazioni già rivelate non è sempre possibile e sposterebbe denaro senza
|
||||
l'operatore); pausa anche per profondità 1 (una riorganizzazione di un
|
||||
blocco è frequente e non tocca i depositi a 6 conferme, [ADR-06](ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)).
|
||||
- *Conseguenze:* un attacco al 51% resta possibile; queste misure lo
|
||||
rendono visibile, fanno sì che non blocchi la lotteria in modo
|
||||
irrecuperabile e limitano il danno a ciò che l'attaccante ha rimosso
|
||||
([LIM-24]).
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-82, B-85) · *Oggi:* nessuna riorganizzazione è
|
||||
rilevata; un header concorrente è ignorato, uno che non si concatena chiude
|
||||
la sessione; il pagamento non verifica le monete del round.
|
||||
@@ -0,0 +1,24 @@
|
||||
# ADR-53 — Il registro di audit è in sola aggiunta e si scrive nella transazione che descrive
|
||||
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Account, amministrazione e sicurezza
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
## Decisione
|
||||
|
||||
**Tutto ciò che muove denaro o cambia la configurazione lascia una riga in
|
||||
`audit_log`; la riga nasce da un'unica funzione, è salvata nella stessa
|
||||
transazione del cambiamento che descrive (tranne gli eventi puramente
|
||||
osservativi) e non viene mai modificata né cancellata.**
|
||||
|
||||
- *Motivazione:* l'amministrazione ha un solo token condiviso senza identità
|
||||
per singolo operatore ([ADR-40](ADR-40-l-amministrazione-usa-unico-token-condiviso.md)):
|
||||
il registro è l'unica forma di responsabilità per le azioni dal pannello e il
|
||||
primo posto dove ricostruire cosa è successo dopo un problema.
|
||||
- *Conseguenze:* o si salvano insieme il cambiamento e la sua riga, o nessuno
|
||||
dei due; il registro dice *cosa* è cambiato ma non *chi* (vedi
|
||||
[ADR-40](ADR-40-l-amministrazione-usa-unico-token-condiviso.md)). Il
|
||||
catalogo degli eventi e le regole di scrittura e lettura sono in
|
||||
[audit-log.md](../audit-log.md) (`AUD-01` … `AUD-05`).
|
||||
|
||||
*Codice:* `app/audit/log.py:write_audit_log`
|
||||
@@ -0,0 +1,132 @@
|
||||
# Architecture Decision Records (ADR)
|
||||
|
||||
Una decisione di progetto = un file. Ogni ADR spiega **perché** una scelta è stata fatta (contesto, alternative scartate, conseguenze); le regole verificabili che la attuano restano nei documenti di dominio con il loro `PREFISSO-NN`. Così ogni informazione ha un solo proprietario: l'ADR possiede il *perché*, il requisito possiede il *cosa*, il codice il *come*. Regole: [processo-di-modifica.md](../processo-di-modifica.md). Modello per i nuovi: [TEMPLATE.md](TEMPLATE.md).
|
||||
|
||||
Gli identificativi `ADR-NN` sono stabili e non si riusano (i primi 52 coincidono con i vecchi `DEC-NN`). Un ADR superato non si cancella: passa a *Sostituita da ADR-MM*.
|
||||
|
||||
## Indice per area
|
||||
|
||||
|
||||
### Custodia, chiavi e indirizzi
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-01](ADR-01-piattaforma-custodiale-tutte-chiavi-generate-tenute.md) | La piattaforma è custodiale: tutte le chiavi sono generate e tenute dal server |
|
||||
| [ADR-02](ADR-02-indirizzo-utente-permanente-tutti-usi.md) | Un indirizzo per utente, permanente, per tutti gli usi |
|
||||
| [ADR-03](ADR-03-ogni-round-ha-proprio-indirizzo-montepremi.md) | Ogni round ha il proprio indirizzo del montepremi |
|
||||
| [ADR-04](ADR-04-accesso-rete-via-protocollo-electrum.md) | Accesso alla rete solo via protocollo Electrum |
|
||||
| [ADR-05](ADR-05-indirizzi-bech32-plm-plm1-come-destinazione.md) | Solo indirizzi bech32 PLM (`plm1…`) come destinazione |
|
||||
|
||||
### Conferme e fiducia nella rete
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-06](ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md) | I depositi richiedono `deposit_confirmations` conferme (6 per default, configurabile in `round_config`), le uscite coinbase 120; ogni altra transazione ne richiede 1 |
|
||||
| [ADR-07](ADR-07-ogni-decisione-sposta-denaro-base-risposta.md) | Ogni decisione che sposta denaro in base a una risposta di rete è corroborata da un quorum degli altri server configurati |
|
||||
| [ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md) | Le riorganizzazioni della catena si contengono invece di ignorarle |
|
||||
|
||||
### Configurazione e ambiente
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-08](ADR-08-parametri-operativi-stanno-database-segreti-nell.md) | I parametri operativi stanno nel database, i segreti nell'ambiente |
|
||||
| [ADR-09](ADR-09-modifiche-configurazione-validate-fidate.md) | Le modifiche di configurazione sono validate, non fidate |
|
||||
| [ADR-10](ADR-10-parametri-round-fotografati-all-apertura.md) | I parametri del round sono fotografati all'apertura |
|
||||
| [ADR-11](ADR-11-server-rifiuta-partire-segreti-inadeguati.md) | Il server rifiuta di partire con segreti inadeguati |
|
||||
| [ADR-12](ADR-12-server-gira-docker-sviluppo-come-produzione.md) | Il server gira solo in Docker, in sviluppo come in produzione |
|
||||
| [ADR-13](ADR-13-processo-file-sqlite.md) | Un solo processo, un solo file SQLite |
|
||||
|
||||
### Economia del gioco
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-14](ADR-14-ripartizione-fissa-70-vincitore-30-piattaforma.md) | Ripartizione fissa 70% al vincitore / 30% alla piattaforma |
|
||||
| [ADR-15](ADR-15-biglietto-prezzo-fisso-scommessa-utente-round.md) | Biglietto a prezzo fisso, una scommessa per utente per round, probabilità uguale per tutti |
|
||||
| [ADR-16](ADR-16-commissione-rete-toglie-dall-importo-muove.md) | La commissione di rete si toglie dall'importo che si muove |
|
||||
| [ADR-17](ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md) | La commissione del pagamento esce dalla quota del vincitore; la riserva per l'RBF esce dalla quota della piattaforma |
|
||||
| [ADR-18](ADR-18-l-accelerazione-rbf-paga-chi-riceve.md) | L'accelerazione RBF la paga chi riceve il resto |
|
||||
| [ADR-19](ADR-19-ogni-transazione-utente-conserva-uscita-resto.md) | Ogni transazione utente conserva un'uscita di resto verso l'utente, e un'accelerazione che porterebbe quel resto sotto `DUST_LIMIT_SATS` (294 sat) lo elimina |
|
||||
| [ADR-20](ADR-20-prelievo-minimo-costo-corrente-scommessa.md) | Il prelievo minimo è il costo corrente della scommessa |
|
||||
| [ADR-21](ADR-21-tre-limiti-distinti-sul-numero-monete.md) | Tre limiti distinti sul numero di monete e di giocatori: |
|
||||
|
||||
### Round ed estrazione
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-22](ADR-22-piu-round-attivo-come-invariante-database.md) | Al più un round attivo, come invariante del database |
|
||||
| [ADR-23](ADR-23-limite-scommettere-scadenza-round-stesso-suo.md) | Il limite per scommettere è la scadenza del round stesso, non il suo stato nel database |
|
||||
| [ADR-24](ADR-24-l-estrazione-commit-reveal-fra-server.md) | L'estrazione è un commit-reveal fra il server e un blocco futuro |
|
||||
| [ADR-25](ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md) | Il blocco di estrazione è fissato per altezza, è successivo alla chiusura, e si usa solo quando è confermato e corroborato |
|
||||
| [ADR-26](ADR-26-validazione-degli-header-concatenata-senza-ricalcolo.md) | Validazione degli header concatenata, senza ricalcolo della difficoltà |
|
||||
| [ADR-27](ADR-27-pausa-cortesia-fra-round-l-altro.md) | Pausa di cortesia fra un round e l'altro |
|
||||
| [ADR-28](ADR-28-pausa-manutenzione-ferma-round-successivo.md) | La pausa per manutenzione ferma solo il round successivo |
|
||||
| [ADR-29](ADR-29-senza-fee-address-apre-nessun-round.md) | Senza `fee_address` non si apre nessun round |
|
||||
| [ADR-30](ADR-30-montepremi-annunciato-quello-sara-pagato.md) | Il montepremi annunciato è quello che sarà pagato |
|
||||
|
||||
### Denaro in movimento
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-31](ADR-31-tutto-cio-spende-denaro-scrive-prima.md) | Tutto ciò che spende denaro si scrive prima di inviarlo |
|
||||
| [ADR-32](ADR-32-pagamento-ritenta-mai-due-volte.md) | Il pagamento si ritenta da solo, mai due volte |
|
||||
| [ADR-33](ADR-33-scommessa-prelievo-escludono-tramite-blocco-utente.md) | Scommessa e prelievo si escludono solo tramite il blocco per-utente |
|
||||
| [ADR-34](ADR-34-saldo-spendibile-saldo-mostrato-due-numeri.md) | Saldo spendibile e saldo mostrato sono due numeri distinti |
|
||||
| [ADR-35](ADR-35-aritmetica-satoshi-interi.md) | Aritmetica solo in satoshi interi |
|
||||
| [ADR-51](ADR-51-dubbio-liberano-mai-fondi.md) | Nel dubbio non si liberano mai i fondi |
|
||||
|
||||
### Account, amministrazione e sicurezza
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-36](ADR-36-password-argon2-sessioni-jwt-hs256-24.md) | Password con Argon2, sessioni con JWT HS256 da 24 ore, revocabili |
|
||||
| [ADR-37](ADR-37-nessun-recupero-password-reset.md) | Nessun recupero password, solo reset |
|
||||
| [ADR-38](ADR-38-username-unico-spazio-nomi-senza-distinzione.md) | Gli username sono un unico spazio di nomi senza distinzione fra maiuscole e minuscole |
|
||||
| [ADR-39](ADR-39-l-esportazione-chiave-privata-wif-utente.md) | L'esportazione della chiave privata WIF di un utente è voluta, non una vulnerabilità |
|
||||
| [ADR-40](ADR-40-l-amministrazione-usa-unico-token-condiviso.md) | L'amministrazione usa un unico token condiviso |
|
||||
| [ADR-41](ADR-41-limitazione-tentativi-su-login-registrazione.md) | Limitazione dei tentativi solo su login e registrazione |
|
||||
| [ADR-42](ADR-42-l-ip-client-l-ultimo-elemento.md) | L'IP del client è l'ultimo elemento di `X-Forwarded-For` |
|
||||
| [ADR-43](ADR-43-header-sicurezza-proxy-unsafe-inline-accettato.md) | Header di sicurezza nel proxy, con `'unsafe-inline'` accettato |
|
||||
| [ADR-44](ADR-44-qr-address-pubblico-limitato.md) | `/qr/{address}` è pubblico ma limitato |
|
||||
| [ADR-45](ADR-45-segnalazioni-bug-aperte-tutti-triage-admin.md) | Segnalazioni di bug aperte a tutti, triage in admin |
|
||||
| [ADR-53](ADR-53-registro-audit-solo-aggiunta-stessa-transazione.md) | Il registro di audit è in sola aggiunta e si scrive nella transazione che descrive |
|
||||
|
||||
### Interfacce, API e lingue
|
||||
|
||||
| ADR | Decisione |
|
||||
|---|---|
|
||||
| [ADR-46](ADR-46-canale-sse-aggiunta-polling-sostituto.md) | Il canale SSE è un'aggiunta al polling, non un sostituto |
|
||||
| [ADR-47](ADR-47-l-api-parla-sola-lingua-risponde.md) | L'API parla una sola lingua e risponde con codici |
|
||||
| [ADR-48](ADR-48-pagine-l-utente-report-bug-7.md) | Le pagine per l'utente (`/` e `/report-bug`) sono in 7 lingue |
|
||||
| [ADR-49](ADR-49-pannello-admin-pagina-guida-tradotti.md) | Il pannello `/admin` e la pagina `/guida` non sono tradotti |
|
||||
| [ADR-50](ADR-50-frontend-statici-senza-framework-ne-build.md) | Frontend statici, senza framework né build |
|
||||
|
||||
## Funzionalità → ADR
|
||||
|
||||
Per ogni funzionalità, gli ADR da leggere prima di toccarla.
|
||||
|
||||
| Funzionalità | Prefisso | Documento di dominio | ADR |
|
||||
|---|---|---|---|
|
||||
| Registrazione e account | `REG` | [registrazione-e-depositi.md](../registrazione-e-depositi.md) | 01, 02, 36, 37, 38 |
|
||||
| Depositi e saldo | `DEP` | [registrazione-e-depositi.md](../registrazione-e-depositi.md) | 06, 07, 34, 35, 52 |
|
||||
| Scommesse | `BET` | [scommesse-e-prelievi.md](../scommesse-e-prelievi.md) | 15, 16, 19, 21, 23, 33 |
|
||||
| Prelievi | `WDR` | [scommesse-e-prelievi.md](../scommesse-e-prelievi.md) | 05, 18, 19, 20, 33 |
|
||||
| Round, estrazione, pagamento | `RND` | [ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md) | 03, 10, 14, 17, 21–30, 32 |
|
||||
| Transazioni, RBF, riconciliazione | `TX` | [transazioni-e-riconciliazione.md](../transazioni-e-riconciliazione.md) | 17, 18, 31, 32, 51 |
|
||||
| Connessione Electrum | `ELC` | [connessione-electrum.md](../connessione-electrum.md) | 04, 07, 26, 52 |
|
||||
| Registro di audit | `AUD` | [audit-log.md](../audit-log.md) | 40, 53 |
|
||||
| Amministrazione | `SEC`, `API` | [sicurezza.md](../sicurezza.md), [api.md](../api.md) | 09, 28, 29, 39, 40 |
|
||||
| Sicurezza (limiti, header, QR) | `SEC` | [sicurezza.md](../sicurezza.md) | 41, 42, 43, 44 |
|
||||
| Segnalazioni di bug | `API` | [api.md](../api.md) | 45 |
|
||||
| Tempo reale (SSE) | `ARC` | [architettura.md](../architettura.md) | 46 |
|
||||
| Interfacce e lingue | `ARC`, `API` | [architettura.md](../architettura.md) | 47, 48, 49, 50 |
|
||||
| Configurazione e ambiente | `ARC`, `OPS` | [architettura.md](../architettura.md), [operativita.md](../operativita.md) | 08, 09, 10, 11, 12, 13 |
|
||||
|
||||
## Le decisioni del 2026-09-28
|
||||
|
||||
| Decisione | ADR | B-nn |
|
||||
|---|---|---|
|
||||
| Estrazione con commit-reveal | [ADR-24](ADR-24-l-estrazione-commit-reveal-fra-server.md), [ADR-25](ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md) | B-73, B-81 |
|
||||
| Nel dubbio non si liberano mai i fondi | [ADR-51](ADR-51-dubbio-liberano-mai-fondi.md) | B-76, B-77, B-78 |
|
||||
| Un indirizzo del montepremi per ogni round | [ADR-03](ADR-03-ogni-round-ha-proprio-indirizzo-montepremi.md) | B-80 |
|
||||
| 6 conferme per i depositi, 1 per il resto | [ADR-06](ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md) | B-82 |
|
||||
| Le riorganizzazioni della catena si contengono | [ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md) | B-82, B-85 |
|
||||
@@ -0,0 +1,26 @@
|
||||
# ADR-NN — <la decisione, in una frase affermativa>
|
||||
|
||||
- **Stato:** Proposta | Accettata | Accettata — implementazione da completare (B-nn) | Sostituita da [ADR-MM](ADR-MM-slug.md) | Rifiutata
|
||||
- **Area:** <una delle aree di [README.md](README.md)>
|
||||
- **Funzionalità:** <prefisso/i dei requisiti coinvolti: REG, DEP, BET, WDR, RND, TX, ELC, AUD, …>
|
||||
|
||||
## Contesto
|
||||
|
||||
<Il problema e i vincoli. Che cosa succede oggi, perché non basta.>
|
||||
|
||||
## Decisione
|
||||
|
||||
**<La scelta, in grassetto, in una frase.>** <Dettagli indispensabili. Le regole
|
||||
verificabili NON si scrivono qui: stanno nel documento di dominio con il loro
|
||||
`PREFISSO-NN`; qui si rimanda a quegli ID.>
|
||||
|
||||
## Alternative scartate
|
||||
|
||||
- <alternativa> — <perché no>
|
||||
|
||||
## Conseguenze
|
||||
|
||||
- <effetti, rischi accettati, limiti `LIM-nn` che ne derivano, ADR collegati>
|
||||
- Requisiti che la attuano: <`RND-11`, `BET-04` …> (nei documenti di dominio)
|
||||
|
||||
*Codice:* `percorso:funzione` · *Test:* `tests/unit/…`
|
||||
+1
-1
@@ -862,7 +862,7 @@ dei parametri operativi in
|
||||
|
||||
- **[API-34]** Esporta la chiave privata WIF dell'indirizzo di un utente, per
|
||||
interventi manuali. È una scelta voluta (vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
- **Parametro:** `user_id` intero.
|
||||
- **Risposta `200`:** `{"address", "wif"}`.
|
||||
- **Errori:** `404 user_not_found` (oggi non strutturato, `"user not
|
||||
|
||||
@@ -565,7 +565,7 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
`drawing`; `tip_at_close` e i partecipanti vivono solo nella chiamata a
|
||||
`_close_and_draw`, quindi un'eccezione o un riavvio lasciano il round in
|
||||
`drawing` per sempre
|
||||
([LIM-01](decisioni-e-limiti.md#robustezza-del-ciclo-di-vita)).
|
||||
([LIM-01](limiti-noti.md#robustezza-del-ciclo-di-vita)).
|
||||
- **[ARC-28]** **`ConfirmationPoller`** (10s), **`RbfBumper`** (30s) e
|
||||
**`PendingTransactionReconciler`** (al primo giro subito, poi ogni 120s)
|
||||
lavorano sulla tabella `pending_transactions` (e, dal piano, sulla tabella
|
||||
@@ -635,7 +635,7 @@ La configurazione è divisa in due posti con regole diverse.
|
||||
la ripartizione 70/30, le conferme richieste per le transazioni della
|
||||
piattaforma (1) e per le uscite coinbase (120), i limiti di input e di
|
||||
partecipanti, le cadenze dei task. Si cambiano solo con una modifica al
|
||||
codice (vedi [decisioni-e-limiti.md](decisioni-e-limiti.md)). Le conferme
|
||||
codice (vedi [limiti-noti.md](limiti-noti.md)). Le conferme
|
||||
dei depositi sono invece configurabili ([ARC-31]).
|
||||
*Codice:* `app/rounds/service.py:winner_share` ·
|
||||
`app/wallet/psbt_builder.py` · `app/tx/confirmation.py`
|
||||
@@ -937,7 +937,7 @@ italiano.
|
||||
lingue di `i18n.js`; le migrazioni Alembic (lo schema dei test nasce da
|
||||
`Base.metadata.create_all`). Le verifiche dal vivo sono state manuali
|
||||
(`scripts/electrum_smoke_test.py` e transazioni reali su mainnet; prelievo e
|
||||
RBF mai trasmessi dal vivo, vedi [decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
RBF mai trasmessi dal vivo, vedi [limiti-noti.md](limiti-noti.md)).
|
||||
La revisione del 2026-09-28 ha osservato che nessuna delle sue falle
|
||||
critiche sarebbe stata trovata da questa suite, che simula tutto: da qui
|
||||
[ARC-62], [ARC-63] e [ARC-64].
|
||||
@@ -956,7 +956,7 @@ italiano.
|
||||
*Codice:* `app/static/i18n.js` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (senza B-nn: chiude
|
||||
[LIM-21](decisioni-e-limiti.md#prodotto-e-interfacce)) · *Oggi:* la parità è
|
||||
[LIM-21](limiti-noti.md#prodotto-e-interfacce)) · *Oggi:* la parità è
|
||||
verificata solo a mano.
|
||||
- **[ARC-64]** **Strato di test di guasto e di integrazione**, in
|
||||
`tests/integration/`, separato dalla suite unitaria: server Electrum
|
||||
@@ -994,7 +994,7 @@ italiano.
|
||||
l'invariante "al massimo un round attivo", garantita da un indice del
|
||||
database e quindi valida anche con più processi (vedi
|
||||
[modello-dati.md](modello-dati.md)). La scelta è discussa in
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md).
|
||||
[limiti-noti.md](limiti-noti.md).
|
||||
*Codice:* `app/rounds/events.py:RoundEventBroadcaster` ·
|
||||
`app/tx/locks.py:UserLocks` · `app/auth/rate_limit.py:AuthRateLimiters` ·
|
||||
`app/main.py:lifespan`
|
||||
+1
-1
@@ -234,7 +234,7 @@ flowchart LR
|
||||
riga è salvata **prima** che la risposta con la chiave venga restituita: se
|
||||
il salvataggio fallisce, la chiave non esce. Per un utente inesistente
|
||||
(404) non si scrive nulla. Questa registrazione non va mai rimossa (vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
*Codice:* `app/api/routes/admin.py:user_privkey` ·
|
||||
*Test:* nessun test verifica la riga di audit (`tests/unit/test_admin.py`
|
||||
copre solo l'esportazione).
|
||||
|
||||
@@ -439,9 +439,9 @@ qualunque nodo o explorer PLM, senza fidarsi del server:
|
||||
Il controllo non esclude un operatore che sia anche minatore: conoscendo il
|
||||
segreto potrebbe scegliere quale blocco pubblicare a `draw_height`. È un
|
||||
rischio accettato, descritto in
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md#round-ed-estrazione) (B-73).
|
||||
[limiti-noti.md](adr/README.md#round-ed-estrazione) (B-73).
|
||||
L'algoritmo resta un componente **sostituibile** (vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
|
||||
## 4 · Pagamento
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Come funziona PLM Lottery
|
||||
|
||||
Panoramica del funzionamento della piattaforma. È il punto di partenza: ogni
|
||||
Panoramica del funzionamento della piattaforma (l'indice completo della
|
||||
documentazione è [README.md](README.md)). È il punto di partenza: ogni
|
||||
sezione rimanda al documento che la approfondisce.
|
||||
|
||||
| Documento | Cosa spiega |
|
||||
@@ -16,7 +17,8 @@ sezione rimanda al documento che la approfondisce.
|
||||
| [sicurezza.md](sicurezza.md) | Modello di fiducia, chiavi e segreti, autenticazione, limiti sui tentativi, header HTTP |
|
||||
| [audit-log.md](audit-log.md) | Catalogo degli eventi del registro di audit |
|
||||
| [operativita.md](operativita.md) | Backup, riavvii, procedure per gli incidenti |
|
||||
| [decisioni-e-limiti.md](decisioni-e-limiti.md) | Decisioni di progetto con le loro motivazioni, limiti noti accettati |
|
||||
| [adr/](adr/README.md) | Le decisioni di progetto con le loro motivazioni, una per file (ADR) |
|
||||
| [limiti-noti.md](limiti-noti.md) | Limiti noti accettati |
|
||||
| [storico-bug.md](storico-bug.md) | Tutti i bug B-nn trovati e corretti, con commit e test |
|
||||
| [piano-correzioni.md](piano-correzioni.md) | Decisioni della revisione del 2026-09-28 e ordine in cui implementare le correzioni ancora aperte |
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ fosse ostile, il principale rischio per l'integrità.
|
||||
I requisiti usano il prefisso `ELC`, secondo la convenzione di
|
||||
[processo-di-modifica.md](processo-di-modifica.md). La configurazione
|
||||
(`.env`) è descritta in [setup.md](setup.md), i rischi accettati in
|
||||
[sicurezza.md](sicurezza.md) e [decisioni-e-limiti.md](decisioni-e-limiti.md).
|
||||
[sicurezza.md](sicurezza.md) e [limiti-noti.md](limiti-noti.md).
|
||||
Questo documento descrive il comportamento **atteso** dopo la revisione del
|
||||
2026-09-28; i requisiti che il codice non soddisfa ancora sono marcati
|
||||
*da implementare* (vedi [piano-correzioni.md](piano-correzioni.md)).
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -96,7 +96,7 @@ sorprenderli con un'interruzione improvvisa.
|
||||
Ogni pausa/ripresa viene registrata nell'audit log (`lottery_paused` /
|
||||
`lottery_resumed`), ma — come per il resto del pannello — non registra
|
||||
*quale* operatore l'ha premuta (token condiviso, vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
|
||||
Prima di un riavvio o di un deploy la pausa da sola non basta: bisogna anche
|
||||
aspettare che il round in corso sia `closed`, perché oggi un riavvio durante
|
||||
@@ -227,7 +227,7 @@ per l'utente: solo un admin col token può farlo.
|
||||
- Un riavvio mentre un round è in estrazione (`drawing`) lo blocca: prima
|
||||
di ogni riavvio mettere in pausa e aspettare `closed` (vedi
|
||||
"Manutenzione" sopra).
|
||||
- Elenco completo dei limiti accettati: [decisioni-e-limiti.md](decisioni-e-limiti.md).
|
||||
- Elenco completo dei limiti accettati: [limiti-noti.md](limiti-noti.md).
|
||||
|
||||
Per l'elenco completo dei gap noti vedi la sezione "Known gaps / TODO" in
|
||||
`CLAUDE.md`.
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
# 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]** **Senza server di riserva configurati la corroborazione passa
|
||||
sempre.** Con `ELECTRUM_FALLBACK_SERVERS` vuoto (il default di
|
||||
`.env.example`) il solo server primario decide estrazione, spese esterne e
|
||||
accrediti. Anche con server di riserva, la maggioranza è calcolata **su chi
|
||||
risponde**: un solo server di riserva raggiungibile decide da solo. I
|
||||
certificati TLS non sono verificati.
|
||||
- *Impatto:* chi controlla il server primario (o la maggioranza dei server
|
||||
raggiungibili) può scegliere il vincitore o alterare i saldi mostrati.
|
||||
- *Mitigazione attuale:* nessun server risponde → la corroborazione
|
||||
**fallisce** (una rete irraggiungibile non prova nulla).
|
||||
- *Stato:* da chiudere con B-84.
|
||||
- *Per chiuderlo:* almeno 2 server di riserva configurati (altrimenti il
|
||||
server rifiuta di partire, salvo `ALLOW_NO_QUORUM=true` in sviluppo) e
|
||||
almeno 2 risposte per ogni corroborazione ([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md)); pin facoltativo del
|
||||
certificato per endpoint (`host:porta:pin=<sha256>`). Resta accettato
|
||||
dopo il piano che i server del quorum siano scelti dall'operatore: più
|
||||
server controllati dalla stessa parte contano come voci indipendenti.
|
||||
|
||||
*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 sull'unico indirizzo del montepremi; il
|
||||
margine fra 400 partecipanti e 500 ingressi ([ADR-21](adr/ADR-21-tre-limiti-distinti-sul-numero-monete.md)) si consuma 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:* da chiudere con B-80.
|
||||
- *Per chiuderlo:* 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
|
||||
accettato dopo il piano: 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`
|
||||
@@ -782,7 +782,7 @@ Una scommessa: un utente in un round.
|
||||
### `round_config`
|
||||
|
||||
I parametri operativi, in **una sola riga**, modificabili a caldo da `/admin`
|
||||
senza riavviare (vedi [decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
|
||||
| Colonna | Tipo | Default | Limiti accettati da `PUT /admin/config` | Significato | Quando ha effetto |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -1532,7 +1532,7 @@ scrive la stringa vuota).
|
||||
background e le richieste HTTP di un singolo processo uvicorn ([DAT-52]). I
|
||||
lock per utente e il canale SSE sono comunque in memoria, quindi più
|
||||
processi non sono supportati in nessun caso (vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
- **Serializzazione degli scrittori.** SQLite ha un solo scrittore alla volta
|
||||
per file; un secondo scrittore aspetta il lock fino al `busy_timeout`
|
||||
([DAT-52]) e poi riceve `OperationalError: database is locked`. Non esistono
|
||||
|
||||
@@ -42,7 +42,7 @@ Il dettaglio tecnico di ogni voce, in inglese, è in [BUGS.md](../BUGS.md).
|
||||
**Limite accettato:** un attacco al 51% su PLM resta possibile e riguarda
|
||||
chiunque usi PLM, non solo la lotteria. Queste misure fanno sì che non
|
||||
convenga e che non blocchi la lotteria; non lo rendono impossibile. Vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md).
|
||||
[limiti-noti.md](limiti-noti.md).
|
||||
|
||||
### Decisioni confermate il 2026-09-29
|
||||
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Processo di modifica: dalla documentazione al codice
|
||||
|
||||
L'indice di tutta la documentazione è [README.md](README.md). Questo documento
|
||||
ne fissa le regole di lavoro.
|
||||
|
||||
## Principi: docs-first, SSOT, ADR
|
||||
|
||||
- **Docs-first.** Si cambia prima la documentazione, poi il codice; spec,
|
||||
codice e test stanno nello stesso commit.
|
||||
- **SSOT.** Ogni informazione ha un solo proprietario e gli altri documenti
|
||||
rimandano a lei per ID senza ripeterla:
|
||||
|
||||
| Informazione | Proprietario |
|
||||
|---|---|
|
||||
| Perché una scelta (contesto, alternative, conseguenze) | un ADR in [adr/](adr/README.md) |
|
||||
| Che cosa deve fare il software (regola verificabile) | il requisito `PREFISSO-NN` del documento di dominio |
|
||||
| Schema, stati, vincoli | `DAT` in [modello-dati.md](modello-dati.md) |
|
||||
| Soglie e rischi di sicurezza | `SEC` in [sicurezza.md](sicurezza.md) |
|
||||
| Eventi di audit | `AUD` in [audit-log.md](audit-log.md) |
|
||||
| Come lo fa | il codice (riferimento `percorso:funzione`) |
|
||||
| Limiti accettati | `LIM` in [limiti-noti.md](limiti-noti.md) |
|
||||
| Difetti trovati e corretti | [storico-bug.md](storico-bug.md) |
|
||||
|
||||
- **Un ADR per ogni funzionalità.** Una funzionalità nuova, o un cambio di
|
||||
scelta in una esistente, parte da un ADR scritto da [adr/TEMPLATE.md](adr/TEMPLATE.md):
|
||||
prossimo numero libero, stato *Proposta* finché non è approvato, poi
|
||||
*Accettata*. L'ADR elenca i requisiti che lo attuano; i requisiti citano
|
||||
l'ADR. Un ADR non si riscrive: se la scelta cambia se ne scrive uno nuovo e
|
||||
il vecchio passa a *Sostituita da ADR-MM*. Un nuovo ADR va aggiunto
|
||||
all'indice per area e alla tabella funzionalità → ADR di
|
||||
[adr/README.md](adr/README.md).
|
||||
|
||||
La cartella `docs/` è la **specifica** di PLM Lottery. Descrive cosa il
|
||||
software fa e perché; il codice è l'implementazione di questa specifica.
|
||||
Ogni modifica di comportamento parte da qui.
|
||||
@@ -82,15 +112,18 @@ fa il codice oggi:
|
||||
| `ELC` | [connessione-electrum.md](connessione-electrum.md) | Connessione alla rete |
|
||||
| `AUD` | [audit-log.md](audit-log.md) | Eventi del registro di audit |
|
||||
| `OPS` | [operativita.md](operativita.md) | Procedure operative |
|
||||
| `DEC` | [decisioni-e-limiti.md](decisioni-e-limiti.md) | Decisioni di progetto |
|
||||
| `LIM` | [decisioni-e-limiti.md](decisioni-e-limiti.md) | Limiti noti accettati |
|
||||
| `ADR` | [adr/](adr/README.md) | Decisioni di progetto (un file per decisione) |
|
||||
| `LIM` | [limiti-noti.md](limiti-noti.md) | Limiti noti accettati |
|
||||
|
||||
## Il ciclo di una modifica
|
||||
|
||||
0. **Scegliere.** Se la modifica è una funzionalità o una scelta nuova, si
|
||||
scrive o aggiorna l'ADR (e si approva) prima di toccare i requisiti.
|
||||
1. **Modificare la specifica.** Si cambia il testo del requisito in `docs/`
|
||||
(o se ne aggiunge uno nuovo con il prossimo numero libero). Se la modifica
|
||||
tocca una decisione (`DEC-*`) o un limite (`LIM-*`), si aggiorna anche
|
||||
quello.
|
||||
tocca una decisione (`ADR-*`) o un limite (`LIM-*`), si aggiorna anche
|
||||
quello; se introduce una funzionalità o una scelta nuova, si scrive prima
|
||||
il relativo ADR (vedi sotto).
|
||||
2. **Individuare l'impatto.** I riferimenti *Codice* e *Test* del requisito
|
||||
dicono dove intervenire. Si cercano anche le altre occorrenze
|
||||
dell'identificativo nel repository (`git grep RND-11`) e i requisiti di
|
||||
|
||||
@@ -193,7 +193,7 @@ transazioni della piattaforma — il resto di scommesse e prelievi, le
|
||||
vincite — arrivano allo stesso indirizzo e passano per lo stesso codice, ma
|
||||
con la soglia di conferme delle transazioni della piattaforma (1): il
|
||||
deposito è l'unico ingresso che un utente può spendere due volte
|
||||
([DEC-06](decisioni-e-limiti.md)).
|
||||
([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)).
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
|
||||
@@ -120,6 +120,6 @@ se crasha, riparte da solo. Questo però non è ancora sicuro in ogni caso: i
|
||||
round in `closing` e `paying_out` riprendono da soli al riavvio, ma se il
|
||||
crash avviene mentre un round è in `drawing`, lo scheduler non lo riprende e
|
||||
il round resta bloccato (limite noto, vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md); procedura in
|
||||
[limiti-noti.md](limiti-noti.md); procedura in
|
||||
[operativita.md](operativita.md)). Non trattare questo setup come
|
||||
"non supervisionato" finché quel limite non è risolto.
|
||||
@@ -380,7 +380,7 @@ una in comune. Questo è garantito da due meccanismi:
|
||||
|
||||
Il blocco per-utente vive nella memoria del processo: vale finché il server è
|
||||
un solo processo (vedi [architettura.md](architettura.md) e
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
|
||||
Cosa succede a una transazione dopo l'invio — conferma, RBF, abbandono — è
|
||||
descritto in [transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md).
|
||||
+27
-27
@@ -14,13 +14,13 @@ per l'estrazione [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#3--estr
|
||||
per gli endpoint e i codici di errore [api.md](api.md); per l'elenco degli
|
||||
eventi di audit [audit-log.md](audit-log.md); per backup e procedure
|
||||
operative [operativita.md](operativita.md); per le decisioni di progetto e i
|
||||
limiti accettati [decisioni-e-limiti.md](decisioni-e-limiti.md).
|
||||
limiti accettati [limiti-noti.md](limiti-noti.md).
|
||||
|
||||
## Modello di fiducia
|
||||
|
||||
La piattaforma è **custodiale**: tutte le chiavi private sono generate e
|
||||
conservate dal server. L'utente non controlla nulla finché non preleva
|
||||
([DEC-01](decisioni-e-limiti.md), [LIM-26](decisioni-e-limiti.md)).
|
||||
([ADR-01](adr/ADR-01-piattaforma-custodiale-tutte-chiavi-generate-tenute.md), [LIM-26](limiti-noti.md)).
|
||||
|
||||
| Soggetto | Fiducia | Perché |
|
||||
|---|---|---|
|
||||
@@ -51,7 +51,7 @@ ricompensa di quel blocco.
|
||||
vincere. Chi trasmette i blocchi ha una leva analoga: il server Electrum
|
||||
attivo può non annunciare un blocco e far estrarre dal successivo; la
|
||||
corroborazione verificava che il blocco esistesse, non che fosse il primo
|
||||
dopo la chiusura ([LIM-13](decisioni-e-limiti.md)).
|
||||
dopo la chiusura ([LIM-13](limiti-noti.md)).
|
||||
- **Come il commit-reveal toglie il movente al minatore.** All'apertura il
|
||||
server si impegna su un segreto pubblicandone solo l'impronta
|
||||
([RND-34](ciclo-di-vita-del-round.md#0--apertura)); all'ingresso in
|
||||
@@ -76,22 +76,22 @@ ricompensa di quel blocco.
|
||||
costa in media 2³² hash, pochi millisecondi a un ASIC: il controllo toglie
|
||||
la contraffazione gratuita, non quella economica. A proteggere l'estrazione
|
||||
sono il quorum sull'header di estrazione e il segreto del commit-reveal
|
||||
([LIM-14](decisioni-e-limiti.md)).
|
||||
([LIM-14](limiti-noti.md)).
|
||||
|
||||
Restano **accettati**, e descritti in
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md):
|
||||
[limiti-noti.md](limiti-noti.md):
|
||||
|
||||
- **Operatore d'accordo con un minatore** ([LIM-25](decisioni-e-limiti.md)).
|
||||
- **Operatore d'accordo con un minatore** ([LIM-25](limiti-noti.md)).
|
||||
L'operatore conosce il segreto prima della rivelazione e può calcolare
|
||||
l'esito di ogni blocco candidato; un minatore complice può scartare quelli
|
||||
sfavorevoli. È accettato perché l'operatore è già fidato come custode di
|
||||
tutti i fondi ([LIM-26](decisioni-e-limiti.md)): chi può leggere il segreto
|
||||
tutti i fondi ([LIM-26](limiti-noti.md)): chi può leggere il segreto
|
||||
dal database può già spendere ogni moneta.
|
||||
- **Un attacco al 51% su PLM** ([LIM-24](decisioni-e-limiti.md)) è
|
||||
- **Un attacco al 51% su PLM** ([LIM-24](limiti-noti.md)) è
|
||||
contenuto, non impedito: 6 conferme sui depositi
|
||||
([DEC-06](decisioni-e-limiti.md), B-82), rilevamento delle
|
||||
([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md), B-82), rilevamento delle
|
||||
riorganizzazioni con pausa sopra la profondità 1 e verifica delle monete
|
||||
del round prima del pagamento ([DEC-52](decisioni-e-limiti.md), B-82,
|
||||
del round prima del pagamento ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md), B-82,
|
||||
B-85). Il denaro che un attaccante riesce a sottrarre è perso per la
|
||||
piattaforma. *Oggi:* depositi a 1 conferma, nessuna riorganizzazione
|
||||
rilevata, nessuna verifica prima del pagamento.
|
||||
@@ -104,7 +104,7 @@ Restano **accettati**, e descritti in
|
||||
| Chiave di cifratura della master key (Fernet) | File `./secrets/xprv_encryption.key` (permessi `0600`, fuori da `./data/`), montato in sola lettura come secret di compose in `/run/secrets/xprv_encryption_key` e indicato da `XPRV_ENCRYPTION_KEY_FILE` ([SEC-53]) | `Fernet.generate_key()` (comando in `.env.example`) | Insieme al file cifrato: come sopra | Il file cifrato non è più leggibile: fondi persi senza backup della xprv | Manuale, con la procedura [OPS-22](operativita.md#segreto-compromesso). Non esiste uno script dedicato |
|
||||
| `JWT_SECRET` | `.env` (permessi `0600`) | `secrets.token_urlsafe(32)` | Chiunque può firmare un token per qualunque utente (con il `tv` corretto, che è un piccolo intero) e agire come lui, prelievi compresi | Nessuna perdita di dati: se ne genera uno nuovo | Cambiarlo e riavviare invalida **tutte** le sessioni |
|
||||
| `ADMIN_TOKEN` | `.env` (permessi `0600`); nel browser dell'operatore in `sessionStorage` (`plm_admin_token`) | `secrets.token_urlsafe(32)` | Accesso completo a `/admin/*`, compreso l'export delle chiavi private | Nessuna: se ne imposta uno nuovo | Cambiarlo e riavviare; l'operatore reinserisce il token |
|
||||
| Segreto del round (`server_seed`) | Tabella `rounds`, **cifrato** con la chiave della master xprv (`server_seed_enc`) fino alla rivelazione, poi in chiaro ([RND-34](ciclo-di-vita-del-round.md#0--apertura), [SEC-56]) | 32 byte da un generatore crittograficamente sicuro, all'apertura del round | Prima della rivelazione permette di calcolare il vincitore di ogni blocco candidato ([LIM-25](decisioni-e-limiti.md)) | L'estrazione di quel round non può concludersi | Uno nuovo per ogni round; pubblico dopo la rivelazione |
|
||||
| Segreto del round (`server_seed`) | Tabella `rounds`, **cifrato** con la chiave della master xprv (`server_seed_enc`) fino alla rivelazione, poi in chiaro ([RND-34](ciclo-di-vita-del-round.md#0--apertura), [SEC-56]) | 32 byte da un generatore crittograficamente sicuro, all'apertura del round | Prima della rivelazione permette di calcolare il vincitore di ogni blocco candidato ([LIM-25](limiti-noti.md)) | L'estrazione di quel round non può concludersi | Uno nuovo per ogni round; pubblico dopo la rivelazione |
|
||||
| Hash delle password | Tabella `users` (SQLite, `./data/db/`) | Argon2id | Attacco offline a dizionario, rallentato dai parametri Argon2 | — | Reset dall'operatore |
|
||||
| JWT dell'utente | Browser, `localStorage` (`plm_token`) | Emesso al login/registrazione | Sessione dell'utente fino alla scadenza (24h) o al cambio password | — | Cambio password o reset dall'operatore |
|
||||
|
||||
@@ -361,7 +361,7 @@ che ne è il proprietario; qui ci sono le regole di sicurezza.
|
||||
- ~~[SEC-18]~~ Assorbito da [REG-01](registrazione-e-depositi.md#registrazione)
|
||||
e [REG-02](registrazione-e-depositi.md#registrazione): formato dello
|
||||
username, confronto senza maiuscole e indice unico su `lower(username)` sono
|
||||
regole del dominio, con la motivazione in [DEC-38](decisioni-e-limiti.md);
|
||||
regole del dominio, con la motivazione in [ADR-38](adr/ADR-38-username-unico-spazio-nomi-senza-distinzione.md);
|
||||
qui non resta nulla di specifico della sicurezza.
|
||||
|
||||
- **[SEC-19]** Login con username inesistente e con password errata danno la
|
||||
@@ -377,7 +377,7 @@ che ne è il proprietario; qui ci sono le regole di sicurezza.
|
||||
- ~~[SEC-20]~~ Assorbito da [REG-12](registrazione-e-depositi.md#sessione-e-password)
|
||||
e [REG-13](registrazione-e-depositi.md#sessione-e-password): l'assenza di
|
||||
recupero, il cambio con la password attuale e il reset dall'operatore sono
|
||||
il flusso del dominio (motivazione in [DEC-37](decisioni-e-limiti.md)); la
|
||||
il flusso del dominio (motivazione in [ADR-37](adr/ADR-37-nessun-recupero-password-reset.md)); la
|
||||
parte di sicurezza, cioè il freno sui tentativi, è [SEC-54], e il reset è
|
||||
[SEC-27].
|
||||
|
||||
@@ -563,7 +563,7 @@ ogni riavvio e non funzionerebbero con più worker.
|
||||
gli altri endpoint di lettura. Scommesse e prelievi sono serializzati per
|
||||
utente dal blocco per-utente (vedi
|
||||
[scommesse-e-prelievi.md](scommesse-e-prelievi.md#scommessa-e-prelievo-insieme)),
|
||||
che non è un limite di frequenza ([LIM-07](decisioni-e-limiti.md)).
|
||||
che non è un limite di frequenza ([LIM-07](limiti-noti.md)).
|
||||
*Codice:* `app/api/routes/bets.py`, `app/api/routes/withdrawals.py`,
|
||||
`app/api/routes/admin.py:require_admin`
|
||||
*Stato:* da implementare (B-94) · *Oggi:* anche il cambio password e
|
||||
@@ -609,7 +609,7 @@ loro significato per la sicurezza.
|
||||
riserva raggiungibile — sceglierebbe da solo il blocco di estrazione, gli
|
||||
accrediti, le monete date per spese e le conferme. Una rete in cui
|
||||
rispondono meno di 2 server ritarda le decisioni, non le prende
|
||||
([DEC-07](decisioni-e-limiti.md)).
|
||||
([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md)).
|
||||
- **`ALLOW_NO_QUORUM=true`** riporta la regola ridotta (nessun altro server:
|
||||
la verifica passa; altrimenti basta una risposta). Rende fidato il server
|
||||
attivo ed è **solo per lo sviluppo**; ogni avvio lo scrive nel log come
|
||||
@@ -628,7 +628,7 @@ loro significato per la sicurezza.
|
||||
nel quorum solo come server indipendente.
|
||||
- **Resta accettato** che i server del quorum siano scelti dall'operatore:
|
||||
più server controllati dalla stessa parte contano come voci indipendenti
|
||||
([LIM-11](decisioni-e-limiti.md)).
|
||||
([LIM-11](limiti-noti.md)).
|
||||
|
||||
*Oggi* (B-84): un `ELECTRUM_FALLBACK_SERVERS` vuoto (il valore di
|
||||
`.env.example`) è accettato in silenzio e fa passare ogni corroborazione; con
|
||||
@@ -840,7 +840,7 @@ La struttura completa delle tabelle è in [modello-dati.md](modello-dati.md).
|
||||
database o di un backup, senza la chiave, non può calcolare in anticipo il
|
||||
vincitore di un round aperto, e quindi non può pilotarlo insieme a un
|
||||
minatore; per farlo servono database **e** chiave, cioè lo stesso livello
|
||||
di accesso che già consente di spendere i fondi ([LIM-25](decisioni-e-limiti.md)).
|
||||
di accesso che già consente di spendere i fondi ([LIM-25](limiti-noti.md)).
|
||||
Per questo la chiave va conservata separata dai backup del database
|
||||
([operativita.md](operativita.md) OPS-06). All'avvio la verifica di
|
||||
[SEC-49] decifra anche il segreto dei round non ancora rivelati: se non ci
|
||||
@@ -868,35 +868,35 @@ La struttura completa delle tabelle è in [modello-dati.md](modello-dati.md).
|
||||
## Rischi accettati
|
||||
|
||||
Limiti noti e voluti, descritti in dettaglio in
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md):
|
||||
[limiti-noti.md](limiti-noti.md):
|
||||
|
||||
- **Custodia totale** ([LIM-26](decisioni-e-limiti.md)). Chi controlla l'host
|
||||
- **Custodia totale** ([LIM-26](limiti-noti.md)). Chi controlla l'host
|
||||
o `ADMIN_TOKEN` controlla tutti i fondi; la cifratura a riposo e i permessi
|
||||
stretti non proteggono da chi ha accesso all'host.
|
||||
- **Operatore d'accordo con un minatore** ([LIM-25](decisioni-e-limiti.md)):
|
||||
- **Operatore d'accordo con un minatore** ([LIM-25](limiti-noti.md)):
|
||||
possono scegliere il vincitore, perché l'operatore conosce il segreto del
|
||||
round prima della rivelazione.
|
||||
- **Attacco al 51% su PLM** ([LIM-24](decisioni-e-limiti.md)): contenuto
|
||||
- **Attacco al 51% su PLM** ([LIM-24](limiti-noti.md)): contenuto
|
||||
(6 conferme sui depositi, riorganizzazioni rilevate e pausa, pagamento di
|
||||
ciò che è in catena), non impedito.
|
||||
- **Header a difficoltà minima** ([LIM-14](decisioni-e-limiti.md)): senza il
|
||||
- **Header a difficoltà minima** ([LIM-14](limiti-noti.md)): senza il
|
||||
ricalcolo della difficoltà, un server ostile può ancora produrre una breve
|
||||
catena falsa al powLimit; la neutralizzano il quorum e il commit-reveal.
|
||||
- **Server del quorum scelti dall'operatore** ([LIM-11](decisioni-e-limiti.md)):
|
||||
- **Server del quorum scelti dall'operatore** ([LIM-11](limiti-noti.md)):
|
||||
più server della stessa parte contano come voci indipendenti; con
|
||||
`ALLOW_NO_QUORUM=true` (solo sviluppo) il server attivo è fidato.
|
||||
- **Token admin unico e condiviso** ([LIM-06](decisioni-e-limiti.md)), senza
|
||||
- **Token admin unico e condiviso** ([LIM-06](limiti-noti.md)), senza
|
||||
identità per operatore né limite di tentativi: una fuga ha il raggio
|
||||
d'azione massimo.
|
||||
- **JWT in `localStorage`** ([LIM-09](decisioni-e-limiti.md)), quindi
|
||||
- **JWT in `localStorage`** ([LIM-09](limiti-noti.md)), quindi
|
||||
leggibile da un eventuale XSS; la CSP lo mitiga ma ammette script inline.
|
||||
- **Limiti di frequenza parziali** ([LIM-07](decisioni-e-limiti.md)): login,
|
||||
- **Limiti di frequenza parziali** ([LIM-07](limiti-noti.md)): login,
|
||||
registrazione, cambio password, segnalazioni di bug e SSE sì; scommesse,
|
||||
prelievi ed endpoint admin no. Tutti in memoria e per singolo processo.
|
||||
- **`JWT_SECRET` e `ADMIN_TOKEN` restano variabili d'ambiente**, visibili con
|
||||
`docker inspect` a chi ha accesso a Docker, che è già fidato come l'host.
|
||||
- **Una conferma per le transazioni della piattaforma**
|
||||
([DEC-06](decisioni-e-limiti.md)): scommesse, pagamenti e prelievi le
|
||||
([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)): scommesse, pagamenti e prelievi le
|
||||
firma solo il server, quindi un utente non può spenderne due volte gli
|
||||
ingressi; solo i depositi ne richiedono 6.
|
||||
- **La master key non è ruotabile**: gli indirizzi degli utenti sono
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ Questo file non contiene requisiti e quindi non ha identificativi propri.
|
||||
il requisito in `docs/`, come per qualunque altra modifica
|
||||
([processo-di-modifica.md](processo-di-modifica.md#il-ciclo-di-una-modifica)).
|
||||
- I limiti accettati **per scelta** non sono bug e non prendono un `B-nn`:
|
||||
stanno in [decisioni-e-limiti.md](decisioni-e-limiti.md).
|
||||
stanno in [limiti-noti.md](limiti-noti.md).
|
||||
|
||||
### Da dove vengono questi dati
|
||||
|
||||
@@ -104,7 +104,7 @@ Note:
|
||||
- **B-16** era stato dichiarato «parzialmente corretto per scelta»: la copia di
|
||||
`docs/` nell'immagine fu scritta e poi ritirata su richiesta del
|
||||
proprietario. Oggi `/guida` serve un segnaposto (vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
|
||||
---
|
||||
|
||||
@@ -170,7 +170,7 @@ restano aperte.
|
||||
|
||||
L'audit ricorda anche un problema **non numerato**, perché già elencato fra i
|
||||
limiti accettati: lo stato `drawing` non riprende dopo un riavvio (vedi
|
||||
[decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
[limiti-noti.md](limiti-noti.md)).
|
||||
|
||||
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
Reference in new issue
Block a user