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:
davideandClaude Sonnet 5.5 committed 2026-10-14 12:05:57 +02:00
1 parent 7ba8e22bb4
commit b424f670c5
76 files changed
+2147 -1530

No files matched your search

+1 -1
View File
@@ -86,7 +86,7 @@ changed yet. Same convention as above: one commit per entry, with its own
regression test, spec + code + test together.
Design decisions taken with the owner on 2026-09-28 (they change existing
decisions, see `docs/decisioni-e-limiti.md`):
decisions, see `docs/limiti-noti.md`):
- **Draw by commit-reveal.** The winner must not be computable by anyone who
controls block production, since PLM's hashrate (~22 TH/s) is rentable.
+17 -1
View File
@@ -14,10 +14,26 @@ Verified on mainnet with real money: registration + address derivation, deposit
Two full-codebase audits — 2026-07-26 (24 findings, 5 critical) and 2026-07-27 (25 more, B-25 … B-49) — are **all fixed** as of 2026-07-27, each with its own regression test. A third audit (2026-08-03, B-52 … B-72) and a fourth, the 2026-09-28 specification review (B-73 … B-98: draw manipulation, ambiguous broadcasts, single-txid tracking, single-server abandonment, the 64 KiB Electrum reply limit, unbumpable payouts, 1-conf deposit reorgs, ops hazards), live in the tracked [BUGS.md](BUGS.md), which lists only what is still **open** (B-71 … B-98 at the time of writing). The spec in `docs/` already describes the *target* behaviour for all of them — every affected requirement is marked `*Stato:* da implementare (B-nn)` with an `*Oggi:*` line saying what the code does now, so read both before assuming a requirement is implemented; the design decisions and the implementation order are in [docs/piano-correzioni.md](docs/piano-correzioni.md); the full history of every B-nn, open and closed, is in [docs/storico-bug.md](docs/storico-bug.md). Closed findings leave the file, so the ~276 `B-nn` markers left in comments across the code are pointers into git history (`git log --all --grep 'B-nn'` finds the commit that fixed one, and `git show f1a1145:BUGS.md`-style the file as it stood). A closed list is not the same as no bugs: the suite is unit-only (`tests/integration/` is empty), and withdrawal and the RBF bump have never been live-broadcast. "Known gaps" at the end of this file is for limitations accepted **by design** instead. A new finding gets the next B-nn, in its own commit with its own regression test.
**[docs/](docs/) is the specification, and it is authoritative.** Every verifiable rule there has a stable requirement ID (`RND-04`, `BET-02`, …) with `*Codice:*`/`*Test:*` pointers to what implements it; the index is [docs/come-funziona.md](docs/come-funziona.md) and the ID convention and change workflow are in [docs/processo-di-modifica.md](docs/processo-di-modifica.md). A behaviour change starts in the spec: edit or add the requirement, implement it, cite the ID in the test docstring and the commit message, and ship spec + code + test in one commit. If this file and the spec disagree, the spec wins and this file is the one to fix. Before writing code, read the relevant spec documents and the "Architecture" section below.
**[docs/](docs/README.md) is the specification, and it is authoritative.** Every verifiable rule there has a stable requirement ID (`RND-04`, `BET-02`, …) with `*Codice:*`/`*Test:*` pointers to what implements it; the index is [docs/come-funziona.md](docs/come-funziona.md) and the ID convention and change workflow are in [docs/processo-di-modifica.md](docs/processo-di-modifica.md). A behaviour change starts in the spec: edit or add the requirement, implement it, cite the ID in the test docstring and the commit message, and ship spec + code + test in one commit. If this file and the spec disagree, the spec wins and this file is the one to fix. Before writing code, read the relevant spec documents and the "Architecture" section below.
Human-facing guides are in [docs/](docs/), in Italian by explicit request (an exception to the English-only rule): the specification starting at [come-funziona.md](docs/come-funziona.md), the operational guides [setup.md](docs/setup.md), [running-the-server.md](docs/running-the-server.md), [guida-utente.md](docs/guida-utente.md), [guida-admin.md](docs/guida-admin.md). README's Quick start and `docs/running-the-server.md` are Docker-only, matching this file — a bare `uvicorn --reload` workflow was removed from both (B-44).
## Documentation map (docs-first, SSOT, ADR)
**Entry point: [docs/README.md](docs/README.md)** — the index of everything under `docs/`. Three rules, all in [docs/processo-di-modifica.md](docs/processo-di-modifica.md):
1. **Docs-first.** Behaviour changes start in `docs/`, never in the code. Spec + code + test ship in one commit, with the requirement/ADR ID in the test docstring and the commit message.
2. **SSOT.** Every fact has one owner; everything else links to its ID instead of restating it. *Why* → an ADR ([docs/adr/](docs/adr/README.md)); *what* (verifiable rule) → a `PREFIX-NN` requirement in the domain document; *how* → the code; accepted limits → [limiti-noti.md](docs/limiti-noti.md) (`LIM-nn`); found/fixed defects → [storico-bug.md](docs/storico-bug.md) (`B-nn`, open ones in [BUGS.md](BUGS.md)). If two documents disagree, the spec wins and the other one gets fixed. This file included: it summarises, it does not own.
3. **One ADR per feature/decision.** A new feature, or a change of an existing choice, starts with an ADR from [docs/adr/TEMPLATE.md](docs/adr/TEMPLATE.md) (next free `ADR-NN`, never reused or rewritten — a superseded one becomes "Sostituita da ADR-MM"), then its requirements, then the code. Add it to the area index and the feature → ADR table in [docs/adr/README.md](docs/adr/README.md). Before touching a feature, read its ADRs there.
| Need | Go to |
|---|---|
| Why is it built this way / alternatives rejected | [docs/adr/README.md](docs/adr/README.md) (the old `DEC-nn` are now `ADR-nn`, same numbers) |
| What exactly must it do | domain docs listed in [docs/README.md](docs/README.md) (`RND`, `BET`, `WDR`, `DEP`, `REG`, `TX`, `ELC`, `AUD`, `ARC`, `DAT`, `API`, `SEC`, `OPS`) |
| Narrative overview | [docs/come-funziona.md](docs/come-funziona.md) |
| Accepted limits | [docs/limiti-noti.md](docs/limiti-noti.md) |
| Fix order for open B-nn | [docs/piano-correzioni.md](docs/piano-correzioni.md) |
## Commands
The server always runs via Docker, in dev and prod alike — there is no supported way to run `uvicorn` directly. The venv (`.venv/`) is only for local tooling: tests, Alembic migrations, and the one-time key/secret scripts.
+1
View File
@@ -41,6 +41,7 @@ domain).
## Documentation
- [CLAUDE.md](CLAUDE.md) — architecture, commands, domain decisions, known gaps (for anyone/anything working on the code)
- [docs/README.md](docs/README.md) — **index of all documentation** (docs-first, single source of truth, one ADR per decision: [docs/adr/](docs/adr/README.md))
- [docs/come-funziona.md](docs/come-funziona.md) — the specification (Italian): how the platform works, with diagrams and traceable requirement IDs; it indexes every per-topic document (flows, architecture, data model, API, security, audit log, operations, decisions and limits, bug history). [docs/processo-di-modifica.md](docs/processo-di-modifica.md) describes how a change goes from the spec to the code
- [docs/setup.md](docs/setup.md) — one-time setup (secrets, master key, migrations)
- [docs/running-the-server.md](docs/running-the-server.md) — how to launch it with Docker+Caddy (dev vs. production TLS)
+73
View File
@@ -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]).
+22
View File
@@ -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`
+132
View File
@@ -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 |
+26
View File
@@ -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
View File
@@ -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
+5 -5
View File
@@ -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
View File
@@ -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).
+2 -2
View File
@@ -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
+4 -2
View File
@@ -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 |
+1 -1
View File
@@ -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
View File
@@ -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`.
+517
View File
@@ -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`
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+37 -4
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 |
|---|---|---|---|---|---|