Complete the target spec for the 2026-09-28 review (B-73..B-98)
Finishes what 88221b0 started: sicurezza.md, audit-log.md and storico-bug.md
now describe the target behaviour too, the operator guides are corrected
against today's code, and api.md gains the missing /health/ready and
/admin/electrum sections plus every new error code in its catalog.
Harmonised names and owners across documents (the `resurrected_tx` pause
reason, the reconciler as the one that watches failed rows, the full
`winner_drawn` payload, a reason for an unanswered pre-payout pool check,
one corroboration-failure row per hash instead of one per tick).
Adds B-98 (coin selection stops at the first covering UTXO and refuses a
payable bet), tightens RND-40 to check both 1 and 400 participants and to
guard PUT /admin/config, and gives a stalled `closing` round a signal
(TX-29, closing_stalled).
Records three decisions confirmed on 2026-09-29: the draw's server_seed is
stored Fernet-encrypted until revealed (SEC-56, DAT-64), the chosen abuse
and key-check thresholds (SEC-49, SEC-54, SEC-55), and a broadcast
rejection is verified against every server before any coin is released,
so matching "already known" text is a shortcut, not a safety condition
(TX-04).
Spec only; no code changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88221b0dbd
commit
4be58709d1
16 files changed
+1377
-323
No files matched your search
+29
-16
@@ -103,7 +103,8 @@ erDiagram
|
||||
string fee_address "snapshot (da implementare)"
|
||||
int draw_confirmations "snapshot (da implementare)"
|
||||
string pool_address "da implementare"
|
||||
string server_seed "segreto fino al vincitore (da implementare)"
|
||||
string server_seed_enc "cifrato fino alla rivelazione (da implementare)"
|
||||
string server_seed "in chiaro solo dopo la rivelazione (da implementare)"
|
||||
string server_seed_hash "da implementare"
|
||||
string participants_hash "da implementare"
|
||||
int draw_height "da implementare"
|
||||
@@ -439,7 +440,8 @@ restano nella tabella come storico.
|
||||
| `fee_address` | String(128) | no | backfill da `round_config` nella migrazione | Destinatario della commissione di **questo** round, copiato all'apertura ([DAT-61]) — *da implementare (B-95)* | `open_new_round_if_needed` |
|
||||
| `draw_confirmations` | Integer | no | `3` (`server_default`) | Conferme richieste sul blocco di estrazione di **questo** round, copiate all'apertura ([DAT-61]) — *da implementare (B-73, B-95)* | `open_new_round_if_needed` |
|
||||
| `pool_address` | String(128) | no | backfill con l'indirizzo di riserva per i round esistenti | Indirizzo del montepremi di **questo** round ([DAT-60]) — *da implementare (B-80)* | `open_new_round_if_needed` |
|
||||
| `server_seed` | String(64) | sì | `NULL` | Segreto di 32 byte in esadecimale, generato all'apertura ([DAT-64]) — *da implementare (B-73)* | `open_new_round_if_needed` |
|
||||
| `server_seed_enc` | Text | sì | `NULL` | Il segreto del round cifrato con Fernet, con la stessa chiave della master xprv; scritto all'apertura ([DAT-64]) — *da implementare (B-73)* | `open_new_round_if_needed` |
|
||||
| `server_seed` | String(64) | sì | `NULL` | Il segreto in chiaro (32 byte in esadecimale), scritto **solo** alla rivelazione ([DAT-64]) — *da implementare (B-73)* | `RoundScheduler._close_and_draw` |
|
||||
| `server_seed_hash` | String(64) | sì | `NULL` | `sha256(server_seed)` in esadecimale, pubblicato dall'apertura ([DAT-64]) — *da implementare (B-73)* | `open_new_round_if_needed` |
|
||||
| `participants_hash` | String(64) | sì | `NULL` | Impronta dell'elenco ordinato dei partecipanti ([DAT-65]) — *da implementare (B-73, B-81)* | `RoundScheduler._close_and_draw` |
|
||||
| `draw_height` | Integer | sì | `NULL` | Altezza del blocco di estrazione, fissata all'ingresso in `drawing` ([DAT-65]) — *da implementare (B-73, B-81)* | `_close_and_draw` |
|
||||
@@ -582,16 +584,26 @@ restano nella tabella come storico.
|
||||
leggono `bet_amount_sats` e `fee_address` da `round_config` al momento
|
||||
dell'uso, quindi una modifica a metà round cambia il round in corso.
|
||||
|
||||
- **[DAT-64]** `server_seed` e `server_seed_hash` si scrivono insieme,
|
||||
all'inserimento del round: `server_seed` sono 32 byte casuali generati con
|
||||
`secrets.token_bytes(32)`, in esadecimale minuscolo (64 caratteri);
|
||||
`server_seed_hash` è `sha256` di quei 32 byte, in esadecimale minuscolo.
|
||||
Nessuno dei due cambia più. `server_seed` è **segreto** finché
|
||||
`winner_user_id` non è stato scritto: nessun endpoint lo restituisce prima,
|
||||
e nessuna voce del registro di audit lo contiene prima
|
||||
([api.md](api.md), `GET /rounds/{id}/proof`). I round aperti prima della
|
||||
migrazione hanno entrambe le colonne a `NULL`.
|
||||
- **[DAT-64]** Il segreto del round sono 32 byte casuali generati con
|
||||
`secrets.token_bytes(32)`. All'inserimento del round si scrivono insieme
|
||||
`server_seed_hash` (`sha256` di quei 32 byte, esadecimale minuscolo) e
|
||||
`server_seed_enc`: il segreto **cifrato con Fernet** usando la stessa chiave
|
||||
che cifra la master xprv (`XPRV_ENCRYPTION_KEY`, dopo B-91 il file di
|
||||
[sicurezza.md](sicurezza.md#sec-53) SEC-53). Il segreto in chiaro non viene
|
||||
mai scritto nel database prima della rivelazione: `server_seed` resta
|
||||
`NULL` e si valorizza (esadecimale minuscolo, 64 caratteri) nella stessa
|
||||
transazione che scrive `winner_user_id`, oppure alla chiusura di un round
|
||||
senza partecipanti. Così una copia del database o un suo backup, senza la
|
||||
chiave, non rivela il segreto di un round ancora aperto. Nessun endpoint e
|
||||
nessuna voce del registro di audit contengono il segreto prima della
|
||||
rivelazione ([api.md](api.md), `GET /rounds/{id}/proof`). Nessuna colonna
|
||||
cambia più dopo la rivelazione; `server_seed_enc` resta com'è. Se
|
||||
all'avvio la chiave non decifra il `server_seed_enc` di un round non
|
||||
ancora rivelato, il server rifiuta di partire
|
||||
([sicurezza.md](sicurezza.md#sec-49) SEC-49). I round aperti prima della
|
||||
migrazione hanno le tre colonne a `NULL`.
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
`app/api/routes/rounds.py:current_round` · *Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* le colonne non esistono; il seme
|
||||
è l'hash del blocco da solo.
|
||||
@@ -846,7 +858,7 @@ senza riavviare (vedi [decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
| `operator` | `app/api/routes/admin.py:pause_lottery`; l'avvio con `START_PAUSED=true` ([operativita.md](operativita.md) B-90) | pausa decisa dall'operatore |
|
||||
| `reorg` | il listener Electrum | riorganizzazione di profondità ≥ 2 ([connessione-electrum.md](connessione-electrum.md) B-85) |
|
||||
| `payout_blocked` | `app/rounds/scheduler.py:RoundScheduler._trigger_payout` | la scommessa del vincitore non è più sulla catena ([DAT-68], B-82) |
|
||||
| `resurrected_tx` | `app/tx/confirmation.py:poll_once` | una riga `failed` risulta confermata ([DAT-72], B-78) |
|
||||
| `resurrected_tx` | `app/tx/reconcile.py:reconcile_once` | una riga `failed` risulta confermata ([DAT-72], B-78) |
|
||||
|
||||
Una pausa automatica (`reorg`, `payout_blocked`, `resurrected_tx`) scrive
|
||||
sempre il proprio motivo, anche sopra una pausa già attiva; una pausa
|
||||
@@ -859,7 +871,7 @@ senza riavviare (vedi [decisioni-e-limiti.md](decisioni-e-limiti.md)).
|
||||
`app/api/routes/admin.py:resume_lottery` ·
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
`app/tx/confirmation.py:poll_once` · *Test:* —
|
||||
`app/tx/reconcile.py:reconcile_once` · *Test:* —
|
||||
*Stato:* da implementare (B-78, B-82, B-85, B-90) · *Oggi:* esiste solo
|
||||
`paused`, scritto solo dai due endpoint admin.
|
||||
|
||||
@@ -952,7 +964,7 @@ Le funzioni citate sono `app/bets/service.py:place_bet`,
|
||||
| `building`, `pending` | `failed` | `app/tx/reconcile.py:_abandon`, solo con una prova ([transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md) B-78) |
|
||||
| `building` | `failed` | `app/withdrawals/service.py:_release_failed_withdrawal`, solo per un rifiuto esplicito |
|
||||
| `building` | cancellata | `app/bets/service.py:_release_failed_bet`, solo per un rifiuto esplicito |
|
||||
| `failed` | `confirmed` | `app/tx/confirmation.py:poll_once` ([DAT-72]) |
|
||||
| `failed` | `confirmed` | `app/tx/reconcile.py:reconcile_once` ([DAT-72]) |
|
||||
|
||||
Un broadcast dall'esito ambiguo lascia la riga in `building` per il
|
||||
riconciliatore, per tutti e tre i `kind` (B-76). Il poller delle conferme
|
||||
@@ -1016,7 +1028,8 @@ Le funzioni citate sono `app/bets/service.py:place_bet`,
|
||||
`"not found on chain"` e `failed_at` non esiste.
|
||||
|
||||
- **[DAT-72]** Una riga `failed` con `failed_at` negli ultimi 14 giorni resta
|
||||
tra quelle che il poller delle conferme controlla, su tutti i txid di
|
||||
tra quelle che il riconciliatore controlla a ogni giro (120 s,
|
||||
[transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md) TX-23), su tutti i txid di
|
||||
`pending_transaction_txids`. Se una versione risulta confermata, la riga
|
||||
passa a `confirmed`, si scrive `pending_tx_resurrected` e la lotteria va in
|
||||
pausa con `paused_reason = 'resurrected_tx'` ([DAT-69]), tutto nella stessa
|
||||
@@ -1024,7 +1037,7 @@ Le funzioni citate sono `app/bets/service.py:place_bet`,
|
||||
(partecipante cancellato, prelievo `failed`, input liberati) **non** vengono
|
||||
ricostruite automaticamente: le riallinea l'operatore
|
||||
([operativita.md](operativita.md) B-78).
|
||||
*Codice:* `app/tx/confirmation.py:poll_once` · `app/tx/reconcile.py:_abandon` ·
|
||||
*Codice:* `app/tx/reconcile.py:reconcile_once` · `app/tx/reconcile.py:_abandon` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-78) · *Oggi:* una riga `failed` non viene mai
|
||||
più letta.
|
||||
|
||||
Reference in new issue
Block a user