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