Safe restore from a backup: START_PAUSED and the derivation-gap check (B-90, REG-15, OPS-27)
START_PAUSED=true pauses the lottery as the operator before the scheduler exists. On the first Electrum session the 20 derivation indexes past the highest assigned one are looked up on every server; if any has history, registration stays closed (503 registration_unavailable) and restore_gap_detected is written, so a restored database cannot hand a lost user's address to a new one. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
55d852fe08
commit
40ac490826
21 files changed
+281
-70
No files matched your search
+4
-5
@@ -305,12 +305,12 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
usato, **senza distinzione tra maiuscole e minuscole**, anche se
|
||||
l'altro account si è registrato nello stesso istante);
|
||||
`409 derivation_index_conflict` (dopo 5 tentativi non è stato possibile
|
||||
assegnare un indice di derivazione libero); `503 registration_suspended`
|
||||
assegnare un indice di derivazione libero); `503 registration_unavailable`
|
||||
(all'avvio sono stati trovati movimenti sulla catena per indici di
|
||||
derivazione successivi all'ultimo assegnato, cioè il database potrebbe
|
||||
essere un backup più vecchio della realtà; la registrazione resta
|
||||
sospesa finché l'operatore non riconcilia,
|
||||
[operativita.md](operativita.md) B-90 — *da implementare*).
|
||||
[operativita.md](operativita.md) B-90).
|
||||
- **Limite di frequenza:** al massimo 5 account creati per IP in una finestra
|
||||
mobile di un'ora; la quota è verificata **prima** dell'hash Argon2 e
|
||||
consumata solo da una registrazione riuscita (un nome già preso o un
|
||||
@@ -324,8 +324,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
[registrazione-e-depositi.md](registrazione-e-depositi.md).
|
||||
*Codice:* `app/auth/routes.py:register`, `app/auth/routes.py:RegisterRequest` ·
|
||||
*Test:* `tests/unit/test_auth.py`, `tests/unit/test_users.py`
|
||||
*Stato:* da implementare (B-92, B-90) · *Oggi:* non scrive audit, e non
|
||||
esiste la sospensione `registration_suspended`.
|
||||
*Stato:* da implementare (B-92) · *Oggi:* non scrive audit.
|
||||
|
||||
#### POST /auth/login
|
||||
|
||||
@@ -1053,7 +1052,7 @@ dei parametri operativi in
|
||||
| `internal_error` | 500 | `app/main.py:log_unhandled_exception` | — | eccezione non gestita | sì |
|
||||
| `withdrawal_to_platform_address` | 400 | `app/withdrawals/service.py:request_withdrawal` | — | destinazione uguale a un indirizzo di montepremi, di riserva o delle commissioni | sì |
|
||||
| `service_busy` | 503 | `app/bets/service.py:place_bet`, `app/withdrawals/service.py:request_withdrawal`, `app/auth/routes.py:register` | — | il database non ha concesso il lock di scrittura entro il timeout; nulla è stato trasmesso, si può riprovare | sì |
|
||||
| `registration_unavailable` | 503 | `app/auth/routes.py:register` | — | registrazione sospesa finché il controllo degli indici di derivazione dopo un ripristino non è concluso o risolto ([registrazione-e-depositi.md](registrazione-e-depositi.md), B-90) — *da implementare* | no — da aggiungere |
|
||||
| `registration_unavailable` | 503 | `app/auth/routes.py:register` | — | registrazione sospesa finché il controllo degli indici di derivazione dopo un ripristino non è concluso o risolto ([registrazione-e-depositi.md](registrazione-e-depositi.md), B-90) | sì |
|
||||
| `not_authenticated` | 401 | dipendenza Bearer di FastAPI | — | header `Authorization` assente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
|
||||
| `invalid_admin_token` | 403 | `app/api/routes/admin.py:require_admin` | — | token admin assente o errato — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
|
||||
| `user_not_found` | 404 | `app/api/routes/admin.py` | — | utente inesistente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
|
||||
|
||||
@@ -344,7 +344,7 @@ sequenceDiagram
|
||||
L->>L: parse_endpoints(...)
|
||||
L->>L: quorum minimo (validate_quorum_servers)
|
||||
L->>L: verifica della master key (B-89)
|
||||
L->>L: START_PAUSED (B-90, da implementare)
|
||||
L->>L: START_PAUSED (B-90)
|
||||
L->>L: app.state.electrum_listener, app.state.user_locks
|
||||
L->>L: create_task ×6
|
||||
Note over L: il server serve richieste<br/>(controllo dei buchi di indici alla prima connessione, B-90)
|
||||
@@ -448,8 +448,6 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
([operativita.md](operativita.md#ripristino), B-90).
|
||||
*Codice:* `app/main.py:lifespan` · `app/config.py:validate_runtime_secrets` · `app/config.py:validate_quorum_servers` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-90), per il controllo 3 · *Oggi:* il lifespan non
|
||||
ferma la registrazione né mette in pausa dopo un ripristino.
|
||||
- **[ARC-21]** Lo stato condiviso con le route vive su `app.state`:
|
||||
`electrum_listener` (l'`ElectrumListener`) e `user_locks` (un
|
||||
`UserLocks`), creati nel lifespan; `auth_rate_limiters` viene invece creato
|
||||
@@ -600,9 +598,8 @@ La configurazione è divisa in due posti con regole diverse.
|
||||
| `enable_api_docs` | `ENABLE_API_DOCS` | `False` | Espone Swagger/ReDoc/OpenAPI ([ARC-14]) |
|
||||
|
||||
*Codice:* `app/config.py:Settings` · *Test:* `tests/unit/test_config.py`
|
||||
*Stato:* da implementare (B-90, B-91) · *Oggi:* `START_PAUSED` e
|
||||
`XPRV_ENCRYPTION_KEY_FILE` non esistono, la chiave Fernet è la variabile
|
||||
`XPRV_ENCRYPTION_KEY`.
|
||||
*Stato:* da implementare (B-91), per `XPRV_ENCRYPTION_KEY_FILE` · *Oggi:* la
|
||||
chiave Fernet è la variabile `XPRV_ENCRYPTION_KEY`.
|
||||
- **[ARC-31]** **Parametri operativi e di business** (costo della scommessa,
|
||||
durata e pausa del round, animazione, commissione di rete, timeout RBF,
|
||||
indirizzo commissioni, pausa di manutenzione e suo motivo, conferme dei
|
||||
|
||||
+3
-8
@@ -44,7 +44,7 @@ ogni tipo di evento. Le colonne della tabella `audit_log` sono descritte in
|
||||
`app/tx/broadcast.py:RbfBumper._tick`, `app/main.py:lifespan`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg`
|
||||
*Stato:* da implementare (B-90, B-96), per due degli eventi
|
||||
*Stato:* da implementare (B-96), per uno degli eventi
|
||||
nuovi · *Oggi:* gli eventi osservativi sono i primi quattro,
|
||||
`chain_reorg_detected` e `rbf_bump_failed`.
|
||||
|
||||
@@ -213,11 +213,7 @@ flowchart LR
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout`,
|
||||
`app/tx/reconcile.py:reconcile_once` ·
|
||||
*Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-90) · *Oggi:* scritto da
|
||||
`POST /admin/pause` (`operator`), dal riconciliatore (`resurrected_tx`), dal
|
||||
listener (`reorg`) e dallo scheduler (`payout_blocked`); l'avvio con
|
||||
`START_PAUSED` non lo scrive ancora.
|
||||
*Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_electrum_listener.py`, `tests/unit/test_restore_gap.py`
|
||||
|
||||
- **[AUD-12]** `lottery_resumed` — `POST /admin/resume`. Payload
|
||||
`{"reason"}`: il `paused_reason` che la ripresa ha tolto (`null` se la
|
||||
@@ -294,8 +290,7 @@ flowchart LR
|
||||
ogni avvio in cui il controllo trova indirizzi usati; transazione propria
|
||||
([AUD-03]). La sospensione delle registrazioni che ne segue non dipende da
|
||||
questa riga. Procedura in [operativita.md](operativita.md) (OPS-28).
|
||||
*Codice:* `app/main.py:lifespan` · *Test:* —
|
||||
*Stato:* da implementare (B-90) · *Oggi:* nessun controllo e nessun evento.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._check_restore_gap` · *Test:* `tests/unit/test_restore_gap.py`
|
||||
|
||||
### Denaro degli utenti
|
||||
|
||||
|
||||
@@ -844,10 +844,6 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
`app/tx/reconcile.py:reconcile_once` · *Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-90) · *Oggi:* si scrivono
|
||||
`operator` (`POST /admin/pause`), `resurrected_tx` (il riconciliatore),
|
||||
`reorg` (il listener) e `payout_blocked` (lo scheduler); `START_PAUSED` non ha
|
||||
ancora chi lo scrive.
|
||||
|
||||
- **[DAT-31]** Con `fee_address` vuoto (o di soli spazi) non si apre nessun round
|
||||
(B-66). Una volta impostato, l'API non permette di svuotarlo di nuovo: la
|
||||
|
||||
+6
-12
@@ -162,7 +162,7 @@ denaro di oggi.
|
||||
docker compose run --rm --no-deps app python -c "import sqlite3; c=sqlite3.connect('/app/db_data/plm_lottery.db'); c.execute('UPDATE round_config SET paused=1'); c.commit(); print('pausa impostata')"
|
||||
```
|
||||
La modifica non lascia una riga di audit: annotarla altrove.
|
||||
Dopo B-90 si usa invece `START_PAUSED=true` in `.env` ([OPS-27]), che
|
||||
Si usa invece `START_PAUSED=true` in `.env` ([OPS-27]), che
|
||||
scrive anche il motivo della pausa e la riga di audit.
|
||||
5. **Elencare, sempre ad app ferma, i round non chiusi e le transazioni in
|
||||
volo** del backup:
|
||||
@@ -235,8 +235,7 @@ denaro di oggi.
|
||||
Gli utenti registrati dopo il backup spariscono con il ripristino, e i loro
|
||||
indici vengono riassegnati ai prossimi iscritti: il nuovo utente riceve lo
|
||||
**stesso indirizzo** di un utente perso, e con esso i fondi che quello vi
|
||||
aveva depositato. Oggi il codice non ha alcuna protezione: l'unica è la
|
||||
procedura [OPS-28]. Dopo B-90 l'app controlla da sola gli indici successivi
|
||||
aveva depositato. L'app lo previene: controlla da sola gli indici successivi
|
||||
e sospende le registrazioni
|
||||
([REG-15](registrazione-e-depositi.md#registrazione)); la procedura resta il
|
||||
modo di rimuovere la sospensione. Gli utenti persi non si possono
|
||||
@@ -253,13 +252,11 @@ denaro di oggi.
|
||||
("Riprendi lotteria"). Con il valore predefinito `false` l'avvio non tocca
|
||||
la pausa.
|
||||
*Codice:* `app/config.py:Settings`, `app/main.py:lifespan`,
|
||||
`app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-90) · *Oggi:* la variabile non esiste; la pausa
|
||||
si imposta a mano nel database (passo 4 di [OPS-07]).
|
||||
`app/rounds/pause.py:pause_by_operator` ·
|
||||
*Test:* `tests/unit/test_restore_gap.py`
|
||||
|
||||
- **[OPS-28]** **Occupare gli indici di utenti persi.** Procedura, ad app
|
||||
ferma, valida oggi e dopo B-90:
|
||||
ferma:
|
||||
1. Trovare il massimo indice assegnato e ricavare gli indirizzi dei 20
|
||||
indici successivi:
|
||||
```bash
|
||||
@@ -283,7 +280,7 @@ denaro di oggi.
|
||||
conoscere il proprio indirizzo, gli si consegna il segnaposto
|
||||
corrispondente con un reset della password ([OPS-19]).
|
||||
|
||||
Dopo B-90 il controllo automatico di
|
||||
Il controllo automatico di
|
||||
[REG-15](registrazione-e-depositi.md#registrazione), al riavvio successivo,
|
||||
non trova più indirizzi usati oltre i segnaposto e le registrazioni
|
||||
riprendono.
|
||||
@@ -789,9 +786,6 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
|
||||
`app/tx/confirmation.py:poll_once`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-90) · *Oggi:* il sistema mette in pausa la
|
||||
lotteria per `resurrected_tx` (B-78), `reorg` (B-85) e `payout_blocked` (B-82);
|
||||
l'avvio con `START_PAUSED=true` non esiste ancora.
|
||||
|
||||
### Intervento manuale sui fondi
|
||||
|
||||
|
||||
@@ -97,10 +97,7 @@ L'utente sceglie username e password (`POST /auth/register`). Il server:
|
||||
ricostruire gli utenti mancanti (procedura in
|
||||
[operativita.md](operativita.md#ripristino), B-90). La registrazione è
|
||||
rifiutata allo stesso modo finché il controllo non è stato completato.
|
||||
*Codice:* `app/main.py:lifespan`, `app/auth/routes.py:register`, `app/wallet/hd.py:derive_user_address` · *Test:* —
|
||||
*Stato:* da implementare (B-90) · *Oggi:* nessun controllo; dopo un
|
||||
ripristino il prossimo utente riceve il primo indice non presente nel
|
||||
database, anche se il suo indirizzo ha già ricevuto fondi.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._check_restore_gap`, `app/auth/routes.py:register`, `app/wallet/hd.py:derive_user_address` · *Test:* `tests/unit/test_restore_gap.py`
|
||||
- **[REG-16]** Ogni registrazione riuscita scrive l'evento `user_registered`
|
||||
con l'indice di derivazione e l'indirizzo assegnati, nella stessa
|
||||
transazione che crea l'utente.
|
||||
|
||||
+3
-5
@@ -131,8 +131,7 @@ l'host.
|
||||
riavvio. L'elenco completo è in
|
||||
[architettura.md](architettura.md#configurazione).
|
||||
*Codice:* `app/config.py:Settings`
|
||||
*Stato:* da implementare (B-90, B-91) · *Oggi:* `START_PAUSED` e
|
||||
`XPRV_ENCRYPTION_KEY_FILE` non esistono.
|
||||
*Stato:* da implementare (B-91), per `XPRV_ENCRYPTION_KEY_FILE`.
|
||||
|
||||
`.env.example` documenta le chiavi necessarie e il comando per generarle;
|
||||
non elenca `DATABASE_URL` né `MASTER_KEY_PATH`, che in Docker sono comunque
|
||||
@@ -778,9 +777,8 @@ sicurezza.
|
||||
successivi al più alto assegnato e, se uno ha cronologia in catena, la
|
||||
registrazione è rifiutata (`registration_unavailable`) e si scrive
|
||||
`restore_gap_detected`
|
||||
([REG-15](registrazione-e-depositi.md#registrazione), B-90). *Oggi:*
|
||||
nessun controllo; l'unica difesa è la procedura
|
||||
[OPS-28](operativita.md#ripristino).
|
||||
([REG-15](registrazione-e-depositi.md#registrazione), B-90); la procedura
|
||||
[OPS-28](operativita.md#ripristino) toglie la sospensione.
|
||||
- **Round rigiocati.** Con `START_PAUSED=true` il server parte con la
|
||||
lotteria in pausa ([OPS-27](operativita.md#ripristino), B-90), e ogni round
|
||||
ha il proprio indirizzo del montepremi, le cui monete sono verificate con
|
||||
|
||||
+2
-2
@@ -238,7 +238,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
| B-87 | L'RBF può lasciare un resto sotto la soglia di dust | Alta | *da registrare* | `test_rbf_dust.py`, `test_broadcast.py` | TX |
|
||||
| B-88 | Le transazioni SQLite non sono quelle che il codice presuppone; `synchronous=NORMAL` può perdere gli ultimi commit | Alta | *da registrare* | `test_sqlite_transactions.py`, `test_db_base.py` | DAT, ARC |
|
||||
| B-89 | Problemi della chiave rilevati solo al primo uso; gli script accettano qualunque xprv e scrivono file leggibili da tutti | Alta | *da registrare* | `test_master_key_check.py` | SEC, OPS |
|
||||
| B-90 | Il ripristino di un backup riapre round già pagati e riassegna indici di derivazione | Alta | **aperto** | — | OPS, REG |
|
||||
| B-90 | Il ripristino di un backup riapre round già pagati e riassegna indici di derivazione | Alta | *da registrare* | `test_restore_gap.py` | OPS, REG |
|
||||
| B-91 | Container eseguito come root, permessi larghi, dipendenze e immagini non fissate | Alta | **aperto** | — | SEC, OPS |
|
||||
| B-92 | Nessuna verifica di salute o prontezza, stato Electrum invisibile, audit non filtrabile, log incompleti | Media | **aperto** | — | OPS, AUD, API |
|
||||
| B-93 | Deploy senza arresto ordinato, rollback impossibile, `SITE_ADDRESS` che torna a `localhost` | Media | **aperto** | — | OPS |
|
||||
@@ -254,7 +254,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
|
||||
- **Corretti:** B-01 … B-70, tranne B-16, che è chiuso nella sostanza (il
|
||||
crash è eliminato e `/guida` serve un segnaposto) ma lascia la guida vera
|
||||
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83, B-74, B-85, B-86, B-73, B-81, B-79, B-82, B-87, B-89, B-95, B-98, B-99, B-100.
|
||||
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83, B-74, B-85, B-86, B-73, B-81, B-79, B-82, B-87, B-89, B-90, B-95, B-98, B-99, B-100.
|
||||
- **Aperti:** B-71, B-72 (gravità bassa, operativi o di documentazione) e
|
||||
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (0 critici,
|
||||
3 alti, 5 medi), da implementare nell'ordine di
|
||||
|
||||
Reference in new issue
Block a user