Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
The seed was the hash of the first block seen after the close, so a miner holding tickets
could withhold blocks until one made them win, the active Electrum server could pick which
real block was used by not announcing another (a failed corroboration even moved the draw to
the next block), and the participant order was the server-only `broadcast_at` with no
tie-break, so nobody could recompute the result.
- Open: every round gets a 32-byte `server_seed`, stored only Fernet-encrypted with the
master-xprv key (`server_seed_enc`), with its published `server_seed_hash`;
`draw_confirmations` is copied from round_config. The plaintext column is written only by
the reveal.
- Entering `drawing` (one transaction, `draw_committed` audit row): participants ordered by
bet txid, each `draw_position`, `participants_hash` (sha256 of `txid:vout:address\n`
lines; new `bet_vout`), `draw_height = tip_at_close + 1`. A tip of 0 keeps the round
`closing` (`draw_baseline_tip_unknown`, once per round). All persisted, so a resumed draw
reads rather than recomputes (the `_tick` branch itself is B-81).
- `_complete_draw` / `_wait_for_draw_block` replace `_wait_for_next_block`: wait for
`draw_confirmations` blocks from `draw_height`, then `ElectrumListener.agreed_block_hash`
asks every server, the active one included, for the header at that exact height (valid
work, >= 2 other answers, strict majority of all); no agreement means the same height is
asked again and `draw_header_corroboration_failed` is written once per distinct reported
hash. `draw_stalled` now counts from the last tip advance, or from when the quorum started
to fail. Seed = sha256(server_seed || block_hash || participants_hash), index = seed mod n.
- The winner, the seed and the reveal (`server_seed`, `winner_drawn` payload) are written in
one transaction; a round closing with no participants reveals too. A round opened before
this change (no commitment) is given a secret as it enters `drawing`.
- `GET /rounds/{id}/proof` (public, 404 `round_not_found`, translated in all 7 languages) and
the new fields on `GET /rounds/current`; `draw_confirmations` in `/admin/config` and the
/admin form; `server_seed_hash`/`draw_height` in `/admin/rounds`, never the secret.
- Alembic d3e8b15a7c42 (plain add_column / create_index, expression indexes untouched).
- Docs: the B-73 `*Stato:*`/`*Oggi:*` lines are gone, guides and CLAUDE.md describe the new
draw. Ambiguities resolved in api.md: the proof's participants also carry `amount_sats`
(RND-37 lists it); `seed` is decimal in the proof and hex in the audit row, as specified.
Tests changed because the spec changed: ElectrumListener.corroborate_header (asked only the
other servers about the block the active one announced) is replaced by agreed_block_hash and
its quorum tests ported; draw_winner takes the secret and the list fingerprint; the four
_wait_for_next_block scheduler tests are replaced by _wait_for_draw_block ones (fixed height,
confirmations, same-height retry, stall clock). New: test_draw_proof.py recomputes the winner
independently from the public proof, checks the plaintext secret is nowhere in the database,
the API or the log before the reveal, and that a second pass over a `drawing` round yields
the same winner. Mutation-checked: ordering by broadcast_at, an early reveal and asking the
tip instead of the draw height each fail tests.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82d078e2d6
commit
db24287ea5
42 files changed
+1715
-620
No files matched your search
@@ -31,5 +31,5 @@ di 2 server, la corroborazione fallisce.
|
||||
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/electrum/listener.py:ElectrumListener.corroborate_utxo_reinstatement` · `app/electrum/listener.py:ElectrumListener.confirmation_proven` · `app/main.py:lifespan` ·
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority` · `app/electrum/listener.py:ElectrumListener.agreed_block_hash` · `app/electrum/listener.py:ElectrumListener.corroborate_utxo_spent` · `app/electrum/listener.py:ElectrumListener.corroborate_utxo_credit` · `app/electrum/listener.py:ElectrumListener.corroborate_utxo_reinstatement` · `app/electrum/listener.py:ElectrumListener.confirmation_proven` · `app/main.py:lifespan` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_confirmation_proof.py`
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-24 — L'estrazione è un commit-reveal fra il server e un blocco futuro
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-73)
|
||||
- **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*.
|
||||
|
||||
@@ -54,9 +54,5 @@ pubblicati, esito non calcolabile da chi produce o trasmette i blocchi.
|
||||
[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.
|
||||
*Codice:* `app/rounds/draw.py:draw_winner` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` · `app/rounds/scheduler.py:RoundScheduler._complete_draw` · `app/rounds/service.py:open_new_round_if_needed` · `app/api/routes/rounds.py:round_proof` ·
|
||||
*Test:* `tests/unit/test_draw.py`, `tests/unit/test_draw_proof.py`
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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)
|
||||
- **Stato:** Accettata — implementazione da completare (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*.
|
||||
|
||||
@@ -30,9 +30,8 @@ chiusura significa «sconosciuta»: la prima altezza appresa fa da base.
|
||||
- *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`.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` · `app/rounds/scheduler.py:RoundScheduler._tick` ·
|
||||
*Test:* `tests/unit/test_scheduler.py::test_a_block_relayer_cannot_choose_the_block_that_seeds_the_draw`, `tests/unit/test_scheduler.py::test_a_disagreement_asks_the_same_height_again_and_is_audited_once_per_hash`
|
||||
*Stato:* da implementare (B-81) · *Oggi:* `draw_height`, `participants_hash` e le
|
||||
posizioni sono salvati, ma `_tick` salta i round in `drawing`: dopo un errore
|
||||
o un riavvio il round non riparte.
|
||||
+20
-23
@@ -34,7 +34,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `POST` | `/withdrawals` | Bearer | [API-24](#post-withdrawals) |
|
||||
| `GET` | `/rounds/current` | facoltativa | [API-25](#get-roundscurrent) |
|
||||
| `GET` | `/rounds/stream` | nessuna | [API-26](#get-roundsstream) |
|
||||
| `GET` | `/rounds/{round_id}/proof` | nessuna | [API-45](#get-roundsround_idproof) — *da implementare (B-73)* |
|
||||
| `GET` | `/rounds/{round_id}/proof` | nessuna | [API-45](#get-roundsround_idproof) |
|
||||
| `GET` | `/qr/{address}` | nessuna | [API-27](#get-qraddress) |
|
||||
| `POST` | `/bug-reports` | facoltativa | [API-28](#post-bug-reports) |
|
||||
| `GET` | `/bug-reports/mine` | Bearer | [API-29](#get-bug-reportsmine) |
|
||||
@@ -548,10 +548,10 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `draw_block_height` | intero o `null` | altezza del blocco usato per l'estrazione |
|
||||
| `draw_block_hash` | stringa o `null` | hash di quel blocco; è **uno** degli ingressi del seme, non il seme (vedi [API-45](#get-roundsround_idproof)) |
|
||||
| `draw_waiting_since` | stringa ISO o `null` | inizio dell'attesa del blocco; valorizzato **solo** mentre `status` è `drawing` |
|
||||
| `server_seed_hash` | stringa o `null` | impegno del server sul segreto del round, pubblicato dall'apertura; `null` per un round aperto prima di B-73 — *da implementare (B-73)* |
|
||||
| `participants_hash` | stringa o `null` | impronta dell'elenco ordinato dei partecipanti, valorizzata dall'ingresso in `drawing` — *da implementare (B-73)* |
|
||||
| `draw_height` | intero o `null` | altezza del blocco di estrazione, fissata all'ingresso in `drawing` — *da implementare (B-73)* |
|
||||
| `draw_confirmations` | intero o `null` | conferme richieste sul blocco di estrazione, fissate all'apertura del round — *da implementare (B-73, B-95)* |
|
||||
| `server_seed_hash` | stringa o `null` | impegno del server sul segreto del round, pubblicato dall'apertura; `null` per un round aperto prima di B-73 |
|
||||
| `participants_hash` | stringa o `null` | impronta dell'elenco ordinato dei partecipanti, valorizzata dall'ingresso in `drawing` |
|
||||
| `draw_height` | intero o `null` | altezza del blocco di estrazione, fissata all'ingresso in `drawing` |
|
||||
| `draw_confirmations` | intero o `null` | conferme richieste sul blocco di estrazione, fissate all'apertura del round |
|
||||
| `pool_address` | stringa o `null` | indirizzo del montepremi del round, verificabile su un esploratore |
|
||||
| `chain_tip_height` | intero o `null` | ultima altezza nota della catena; `null` finché non è nota |
|
||||
| `lottery_paused` | booleano | lotteria in pausa, per qualunque motivo |
|
||||
@@ -577,9 +577,8 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
`app/rounds/service.py:round_deadline`, `app/rounds/service.py:winner_share`,
|
||||
`app/rounds/service.py:rounds_can_open` ·
|
||||
*Test:* `tests/unit/test_rounds_route.py`
|
||||
*Stato:* da implementare (B-73, B-78, B-82, B-85, B-95), per i campi
|
||||
marcati · *Oggi:* quei campi non esistono, `bet_amount_sats` è sempre il
|
||||
costo configurato, e `draw_block_hash` è il seme dell'estrazione.
|
||||
*Stato:* da implementare (B-82, B-95), per i campi marcati · *Oggi:*
|
||||
`bet_amount_sats` è sempre il costo configurato.
|
||||
|
||||
#### GET /rounds/stream
|
||||
|
||||
@@ -641,7 +640,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
| `server_seed` | stringa o `null` | **solo dopo** che il vincitore è stato scritto (`paying_out` e `closed`) | il segreto del round, 32 byte in esadecimale |
|
||||
| `draw_height` | intero o `null` | ingresso in `drawing` | altezza del blocco di estrazione |
|
||||
| `draw_confirmations` | intero | apertura | conferme richieste su quel blocco prima di leggerne l'hash |
|
||||
| `participants` | lista | ingresso in `drawing` | elenco in ordine di `draw_position`: `{"draw_position", "txid", "vout", "address", "status"}`, dove `txid:vout` è l'output della scommessa verso `pool_address`, `address` l'indirizzo del partecipante (dove andrebbe la vincita) e `status` `confirmed` o `reorged_out` |
|
||||
| `participants` | lista | ingresso in `drawing` | elenco in ordine di `draw_position`: `{"draw_position", "txid", "vout", "address", "amount_sats", "status"}`, dove `txid:vout` è l'output della scommessa verso `pool_address`, `address` l'indirizzo del partecipante (dove andrebbe la vincita), `amount_sats` l'importo netto della scommessa e `status` `confirmed` o `reorged_out` |
|
||||
| `participants_hash` | stringa o `null` | ingresso in `drawing` | impronta dell'elenco `participants` |
|
||||
| `draw_block_hash` | stringa o `null` | estrazione | hash del blocco a `draw_height` |
|
||||
| `seed` | stringa o `null` | estrazione | il seme in decimale (`rounds.seed_int`) |
|
||||
@@ -675,11 +674,9 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
|
||||
`pool_address`.
|
||||
- **Errori:** `404 round_not_found`; `422`.
|
||||
- **Effetti:** nessuno.
|
||||
*Codice:* `app/api/routes/rounds.py` (endpoint da scrivere),
|
||||
`app/rounds/draw.py:draw_winner` · *Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* l'endpoint non esiste; l'unico
|
||||
dato pubblico dell'estrazione è `draw_block_hash` in `GET /rounds/current`,
|
||||
finché il round è attivo.
|
||||
*Codice:* `app/api/routes/rounds.py:round_proof`,
|
||||
`app/rounds/draw.py:draw_winner` ·
|
||||
*Test:* `tests/unit/test_draw_proof.py`
|
||||
|
||||
### QR
|
||||
|
||||
@@ -763,9 +760,8 @@ dei parametri operativi in
|
||||
"draw_confirmations", "max_round_pool_sats", "paused", "paused_reason"}`.
|
||||
*Codice:* `app/api/routes/admin.py:read_config` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-82, B-85, B-95) · *Oggi:* mancano
|
||||
`deposit_confirmations`, `draw_confirmations`, `max_round_pool_sats` e
|
||||
`paused_reason`.
|
||||
*Stato:* da implementare (B-82, B-95) · *Oggi:* mancano
|
||||
`deposit_confirmations` e `max_round_pool_sats`.
|
||||
|
||||
#### PUT /admin/config
|
||||
|
||||
@@ -783,7 +779,7 @@ dei parametri operativi in
|
||||
| `rbf_timeout_seconds` | 60 – 604 800 |
|
||||
| `draw_animation_seconds` | 0 – 600 |
|
||||
| `deposit_confirmations` | 1 – 100 — *da implementare (B-82)* |
|
||||
| `draw_confirmations` | 1 – 20 — *da implementare (B-73)* |
|
||||
| `draw_confirmations` | 1 – 20 |
|
||||
| `max_round_pool_sats` | `0` (disattivato), oppure da `bet_amount_sats` a 4 000 000 000 000 000 — *da implementare (B-95)* |
|
||||
|
||||
I limiti sono quelli di [modello-dati.md](modello-dati.md) (tabella
|
||||
@@ -802,8 +798,9 @@ dei parametri operativi in
|
||||
*Codice:* `app/api/routes/admin.py:update_config`,
|
||||
`app/api/routes/admin.py:RoundConfigUpdate` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-82, B-95), per i tre campi nuovi e la
|
||||
copia all'apertura · *Oggi:* sono accettati solo i primi sette campi.
|
||||
*Stato:* da implementare (B-82, B-95), per i due campi nuovi e la copia
|
||||
all'apertura di costo ed `fee_address` · *Oggi:* sono accettati i primi
|
||||
otto campi.
|
||||
|
||||
#### POST /admin/pause e POST /admin/resume
|
||||
|
||||
@@ -891,8 +888,8 @@ dei parametri operativi in
|
||||
completa di un round è in [API-45](#get-roundsround_idproof).
|
||||
*Codice:* `app/api/routes/admin.py:list_rounds` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-79, B-82), per gli ultimi quattro
|
||||
campi · *Oggi:* la lista si ferma a `pool_address`.
|
||||
*Stato:* da implementare (B-79, B-82), per gli ultimi due campi · *Oggi:*
|
||||
la lista si ferma a `draw_height`.
|
||||
|
||||
#### GET /admin/audit-log
|
||||
|
||||
@@ -1065,7 +1062,7 @@ dei parametri operativi in
|
||||
| `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 |
|
||||
| `bug_report_not_found` | 404 | `app/api/routes/admin.py` | — | segnalazione inesistente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
|
||||
| `round_not_found` | 404 | `app/api/routes/rounds.py` (`GET /rounds/{id}/proof`) | — | round inesistente — *da implementare (B-73)* | no — da aggiungere |
|
||||
| `round_not_found` | 404 | `app/api/routes/rounds.py` (`GET /rounds/{id}/proof`) | — | round inesistente | sì |
|
||||
| `not_found` | 404 | gestore delle eccezioni HTTP in `app/main.py` | — | percorso inesistente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
|
||||
| `method_not_allowed` | 405 | gestore delle eccezioni HTTP in `app/main.py` | — | metodo non ammesso — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
|
||||
| `too_many_streams` | 503 | `app/api/routes/rounds.py` (SSE) | — | tetto globale degli stream raggiunto — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
|
||||
|
||||
@@ -216,7 +216,7 @@ sono vuoti e non sono elencati.
|
||||
| `app/withdrawals/confirmation.py` | Gestore di conferma per il tipo `withdrawal` |
|
||||
| `app/rounds/service.py` | Round attivo, scadenza, accettazione scommesse, `rounds_can_open`, apertura del round, quota del vincitore |
|
||||
| `app/rounds/scheduler.py` | `RoundScheduler`: chiusura, estrazione, pagamento e suoi tentativi |
|
||||
| `app/rounds/draw.py` | Matematica degli header (hash, prev-hash, target) e scelta del vincitore. *Da implementare:* seme commit-reveal e impronta dei partecipanti (B-73) |
|
||||
| `app/rounds/draw.py` | Matematica degli header (hash, prev-hash, target) e scelta del vincitore. Seme commit-reveal e impronta dei partecipanti |
|
||||
| `app/rounds/config.py` | `get_round_config`: la riga unica di `round_config`, creata se manca |
|
||||
| `app/rounds/confirmation.py` | Gestore di conferma per il tipo `payout` |
|
||||
| `app/rounds/events.py` | `RoundEventBroadcaster` e il singleton `broadcaster` (SSE) |
|
||||
@@ -553,7 +553,7 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
round in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#3--estrazione).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._tick` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
*Stato:* da implementare (B-81) · *Oggi:* `_tick` salta i round in
|
||||
`drawing`; `tip_at_close` e i partecipanti vivono solo nella chiamata a
|
||||
@@ -621,9 +621,8 @@ La configurazione è divisa in due posti con regole diverse.
|
||||
effetti in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md).
|
||||
*Codice:* `app/rounds/config.py:get_round_config` ·
|
||||
`app/db/models.py:RoundConfig`
|
||||
*Stato:* da implementare (B-73, B-82, B-85, B-95) · *Oggi:*
|
||||
`deposit_confirmations`, `draw_confirmations`, `max_round_pool_sats` e il
|
||||
motivo della pausa non esistono.
|
||||
*Stato:* da implementare (B-82, B-95) · *Oggi:* `deposit_confirmations` e
|
||||
`max_round_pool_sats` non esistono.
|
||||
- **[ARC-32]** Alcuni valori sono costanti nel codice e non configurabili:
|
||||
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
|
||||
|
||||
+19
-38
@@ -38,8 +38,8 @@ ogni tipo di evento. Le colonne della tabella `audit_log` sono descritte in
|
||||
transazione). Per `payout_failed` la scrittura è anche *best effort*: se
|
||||
fallisce, l'errore viene registrato nel log applicativo
|
||||
(`could not record the payout failure of round …`) e ignorato.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._adopt_baseline_tip`,
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._close_and_draw`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._log_payout_failure`,
|
||||
`app/tx/broadcast.py:RbfBumper._tick`, `app/main.py:lifespan`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
@@ -115,8 +115,9 @@ ogni tipo di evento. Le colonne della tabella `audit_log` sono descritte in
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo `payout_failed`)
|
||||
*Stato:* da implementare (B-87, B-96, B-73), per le ultime tre letture ·
|
||||
*Oggi:* l'unica lettura è quella di `payout_failed`.
|
||||
*Stato:* da implementare (B-87, B-96), per le ultime due letture ·
|
||||
*Oggi:* le letture sono quelle di `payout_failed` e di
|
||||
`draw_baseline_tip_unknown`.
|
||||
|
||||
## Catalogo degli eventi
|
||||
|
||||
@@ -186,8 +187,8 @@ flowchart LR
|
||||
riga. `user_id` no, `round_id` no. Stessa transazione della modifica.
|
||||
*Codice:* `app/api/routes/admin.py:update_config` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-82, B-95), per gli ultimi tre campi ·
|
||||
*Oggi:* i campi sono i primi sette.
|
||||
*Stato:* da implementare (B-82, B-95), per gli ultimi due campi ·
|
||||
*Oggi:* i campi sono i primi otto.
|
||||
|
||||
- **[AUD-11]** `lottery_paused` — la lotteria è stata messa in pausa, da
|
||||
chiunque. Payload `{"reason"}`, più `"note"` quando l'operatore l'ha
|
||||
@@ -413,9 +414,7 @@ flowchart LR
|
||||
logica rilegge la riga: la ripresa dell'estrazione (RND-36) legge i valori
|
||||
dal round.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* l'ingresso in `drawing` non
|
||||
scrive alcun evento.
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
- **[AUD-24]** `winner_drawn` — il vincitore è stato estratto e salvato; in
|
||||
questa riga il segreto del round viene **rivelato**. Payload
|
||||
@@ -434,11 +433,8 @@ flowchart LR
|
||||
round e lo porta a `paying_out` ([RND-18](ciclo-di-vita-del-round.md#3--estrazione)):
|
||||
`server_seed` non può quindi comparire nel registro prima che il vincitore
|
||||
sia salvato.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
*Stato:* da implementare (B-73), per le ultime quattro chiavi · *Oggi:* il
|
||||
payload ha solo le prime cinque; il round non ha segreto né impronta dei
|
||||
partecipanti.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._complete_draw` ·
|
||||
*Test:* `tests/unit/test_draw_proof.py`
|
||||
|
||||
- **[AUD-25]** `draw_baseline_tip_unknown` — quando il round sarebbe pronto
|
||||
per `drawing`, l'altezza della catena non era nota: il round resta in
|
||||
@@ -447,13 +443,8 @@ flowchart LR
|
||||
Una sola riga per round ([AUD-09]). `user_id` no, `round_id` sì.
|
||||
Transazione propria ([AUD-03]). Dovrebbe essere irraggiungibile (lo
|
||||
scheduler non lavora senza un'altezza nota).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._adopt_baseline_tip` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* il round entra comunque in
|
||||
`drawing`, il primo blocco ricevuto è preso come base e l'estrazione
|
||||
aspetta il blocco successivo; payload `{"baseline_height"}` con l'altezza
|
||||
adottata.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
- **[AUD-26]** `draw_header_corroboration_failed` — raggiunte le conferme
|
||||
richieste, il quorum dei server non concorda sull'header all'altezza
|
||||
@@ -463,11 +454,8 @@ flowchart LR
|
||||
`{"height", "reported_hash"}`: `height` è sempre `draw_height`,
|
||||
`reported_hash` l'hash riportato dal server attivo. `user_id` no,
|
||||
`round_id` sì. Transazione propria ([AUD-03]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* una riga per ogni blocco
|
||||
candidato rifiutato, con `height` l'altezza di quel candidato; l'estrazione
|
||||
aspetta poi un blocco ulteriore invece di richiedere la stessa altezza.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
- **[AUD-27]** `draw_stalled` — l'estrazione è in stallo secondo la regola di
|
||||
[RND-20](ciclo-di-vita-del-round.md#3--estrazione), che ne è il
|
||||
@@ -476,14 +464,8 @@ flowchart LR
|
||||
"elapsed_seconds"}`: altezza del blocco di estrazione, altezza della catena
|
||||
nota in quel momento, secondi da quando dura la condizione di RND-20.
|
||||
`user_id` no, `round_id` sì. Transazione propria ([AUD-03]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* scritto quando l'attesa di un
|
||||
nuovo blocco supera 360 secondi contati da `drawing_started_at`, e mai
|
||||
durante una corroborazione; payload `{"tip_at_close",
|
||||
"current_tip_height", "elapsed_seconds"}`, dove `tip_at_close` è la base
|
||||
*attuale* dell'attesa, che dopo un `draw_header_corroboration_failed`
|
||||
diventa l'altezza del candidato rifiutato.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
- **[AUD-28]** `payout_failed` — un tentativo di pagamento non è partito.
|
||||
Payload `{"round_id", "reason"}`. `user_id` dipende: il vincitore, tranne
|
||||
@@ -660,10 +642,9 @@ flowchart LR
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/rounds/confirmation.py:_on_payout_confirmed`,
|
||||
`app/tx/confirmation.py:poll_once`, `app/tx/broadcast.py:bump_fee`
|
||||
*Stato:* da implementare (B-73, B-87, B-92, B-94), per i quattro fatti che
|
||||
escono dall'elenco · *Oggi:* anche l'ingresso in `drawing`, gli RBF
|
||||
impossibili, la registrazione e il cambio password riuscito non producono
|
||||
alcuna riga.
|
||||
*Stato:* da implementare (B-87, B-92, B-94), per i tre fatti che escono
|
||||
dall'elenco · *Oggi:* gli RBF impossibili, la registrazione e il cambio
|
||||
password riuscito non producono alcuna riga.
|
||||
|
||||
## Log applicativo
|
||||
|
||||
|
||||
@@ -111,10 +111,9 @@ richiesta di scommessa (vedi
|
||||
round successivo, mai per quello in corso.
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/rounds/service.py:round_deadline` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py`, `tests/unit/test_scheduler.py` (solo durata e pausa)
|
||||
*Stato:* da implementare (B-95, B-73) · *Oggi:* sono fotografate solo durata
|
||||
e pausa; costo della scommessa e `fee_address` sono letti dalla
|
||||
configurazione a ogni uso, `draw_confirmations` e `max_round_pool_sats` non
|
||||
esistono.
|
||||
*Stato:* da implementare (B-95) · *Oggi:* sono fotografate durata, pausa e
|
||||
`draw_confirmations`; costo della scommessa e `fee_address` sono letti dalla
|
||||
configurazione a ogni uso, `max_round_pool_sats` non esiste.
|
||||
- **[RND-06]** Pause e configurazione non pagabile bloccano solo l'apertura
|
||||
del *prossimo* round: un round già in corso prosegue fino al pagamento con i
|
||||
valori fotografati all'apertura ([RND-05]), quindi una modifica di
|
||||
@@ -143,10 +142,8 @@ richiesta di scommessa (vedi
|
||||
`GET /rounds/current` e dalla prova pubblica ([RND-37]) fin dall'apertura.
|
||||
`server_seed` non è esposto da nessun endpoint, `/admin` compreso, finché il
|
||||
vincitore non è salvato ([RND-18]).
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/api/routes/rounds.py:current_round` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* il round non ha alcun segreto;
|
||||
il seme dipende solo dall'hash del blocco.
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/rounds/draw.py:new_server_seed`, `app/wallet/keystore.py:encrypt_round_seed`, `app/api/routes/rounds.py:current_round` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py`, `tests/unit/test_draw_proof.py`
|
||||
- **[RND-35]** **Un indirizzo del montepremi per round**: all'apertura il
|
||||
round riceve un proprio indirizzo `m/84'/746'/0'/2/<round_id>`, salvato in
|
||||
`rounds.pool_address` ed esposto da `GET /rounds/current`. Le scommesse del
|
||||
@@ -241,20 +238,14 @@ giocata, ma il montepremi pubblicizzato non supera mai quello che verrà pagato.
|
||||
|
||||
Dopo il commit si invia una notifica SSE; da quel momento `participants_hash`
|
||||
e `draw_height` sono pubblici ([RND-37]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* — (nessun test porta `_close_and_draw` con partecipanti confermati fino a `drawing`)
|
||||
*Stato:* da implementare (B-73, B-81) · *Oggi:* l'elenco è ordinato per
|
||||
`broadcast_at` (che il pubblico non vede e che non ha spareggio) e resta
|
||||
solo in memoria; non esistono `draw_position` né `participants_hash`.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw`, `app/rounds/draw.py:participants_hash` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_draw.py`, `tests/unit/test_draw_proof.py`
|
||||
- **[RND-33]** L'altezza del blocco di estrazione è fissata all'ingresso in
|
||||
`drawing` come `draw_height = tip_at_close + 1`, dove `tip_at_close` è
|
||||
l'altezza della catena nota in quell'istante, e non cambia più: né per una
|
||||
conferma mancata, né per un errore, né per un riavvio.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* si usa la punta vista al primo
|
||||
controllo utile, che può essere un blocco successivo; dopo una
|
||||
corroborazione fallita il riferimento avanza all'altezza rifiutata.
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
## 3 · Estrazione
|
||||
|
||||
@@ -290,12 +281,11 @@ flowchart TB
|
||||
`draw_height + draw_confirmations − 1`. La condizione si controlla a ogni
|
||||
passo dello scheduler, rileggendo dal database tutto ciò che serve
|
||||
([RND-36]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73, B-81) · *Oggi:* si attende che la punta
|
||||
superi l'altezza di riferimento e si usa la punta stessa, con una sola
|
||||
conferma; per tutta l'attesa lo scheduler resta bloccato dentro
|
||||
`_close_and_draw` e non esegue altri passi.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
*Stato:* da implementare (B-81) · *Oggi:* la condizione si controlla dentro
|
||||
`_close_and_draw`, che resta in attesa per tutta la durata senza che
|
||||
`_tick` la riprenda: dopo un errore o un riavvio non riparte.
|
||||
- **[RND-16]** **Header concordato**: raggiunte le conferme, l'header
|
||||
all'altezza `draw_height` (e non la punta corrente) viene chiesto a tutti i
|
||||
server configurati, attivo compreso, e lo si usa solo se il quorum concorda
|
||||
@@ -308,12 +298,8 @@ flowchart TB
|
||||
riportato dal server attivo per quell'altezza, non a ogni richiesta, così
|
||||
un disaccordo che dura non scrive una riga ogni 5 secondi; la durata
|
||||
dell'attesa resta visibile tramite `draw_stalled` ([RND-20]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block`, `app/electrum/listener.py:ElectrumListener.corroborate_header` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* si usa l'header della punta
|
||||
ricevuto dal server attivo, chiedendo agli altri solo di confermarlo; se la
|
||||
conferma manca, si attende un blocco ulteriore e si usa quello, così il
|
||||
server attivo può scegliere quale blocco reale seminare tacendone uno.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block`, `app/electrum/listener.py:ElectrumListener.agreed_block_hash` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_electrum_listener.py`
|
||||
- **[RND-17]** **Algoritmo**: seme = sha256 della concatenazione di
|
||||
`server_seed` (32 byte), hash del blocco a `draw_height` (32 byte,
|
||||
nell'ordine mostrato dagli explorer) e `participants_hash` (32 byte),
|
||||
@@ -321,10 +307,8 @@ flowchart TB
|
||||
partecipanti; vincitore = il partecipante con `draw_position` uguale
|
||||
all'indice. **Probabilità uguale per tutti**: ogni partecipante ha una e
|
||||
una sola posizione nell'elenco, indipendentemente dall'importo.
|
||||
*Codice:* `app/rounds/draw.py:draw_winner`, `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_draw.py` (solo `draw_winner`; nessun test lato scheduler)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* seme = hash del blocco letto come
|
||||
numero esadecimale, applicato all'elenco ordinato per `broadcast_at`.
|
||||
*Codice:* `app/rounds/draw.py:draw_seed`, `app/rounds/draw.py:draw_index`, `app/rounds/draw.py:draw_winner`, `app/rounds/scheduler.py:RoundScheduler._complete_draw` ·
|
||||
*Test:* `tests/unit/test_draw.py`, `tests/unit/test_draw_proof.py`
|
||||
- **[RND-18]** **Verificabile**: in un'unica transazione del database il
|
||||
round salva altezza e hash del blocco (`draw_block_height` = `draw_height`,
|
||||
`draw_block_hash`), il seme (`seed_int`), il vincitore e il montepremi
|
||||
@@ -339,11 +323,8 @@ flowchart TB
|
||||
e ottenere lo stesso vincitore (vedi
|
||||
[Come si verifica un'estrazione](#come-si-verifica-unestrazione)). Il primo
|
||||
tentativo di pagamento parte subito dopo, nello stesso passo.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* — (nessun test porta `_close_and_draw` con partecipanti confermati fino a `paying_out`)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* il calcolo non è ripetibile da
|
||||
terzi: l'ordine dei partecipanti è per `broadcast_at`, noto solo al server
|
||||
e senza spareggio fra valori uguali.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._complete_draw` ·
|
||||
*Test:* `tests/unit/test_draw_proof.py`
|
||||
- **[RND-19]** **Altezza sconosciuta alla chiusura**: se, quando il round
|
||||
sarebbe pronto per `drawing`, l'altezza della catena non è nota (valore 0),
|
||||
il round resta `closing` e il passaggio viene ritentato al passo
|
||||
@@ -351,11 +332,8 @@ flowchart TB
|
||||
`draw_height` si fissa solo su un'altezza realmente nota ([RND-33]). In
|
||||
condizioni normali non accade, perché la connessione è resa disponibile solo
|
||||
con l'altezza già nota ([ELC-07](connessione-electrum.md#3-caduta-rilevata)).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw`, `app/rounds/scheduler.py:RoundScheduler._adopt_baseline_tip` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* il round entra comunque in
|
||||
`drawing`, attende il primo header, ne adotta l'altezza come riferimento,
|
||||
scrive `draw_baseline_tip_unknown` e aspetta un blocco successivo.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
- **[RND-20]** **Attesa lunga ma visibile**: l'attesa non ha un limite,
|
||||
perché dipende davvero dall'arrivo dei blocchi. Ogni 60 secondi
|
||||
(`_DRAW_PROGRESS_LOG_INTERVAL_SECONDS`) se ne registra l'avanzamento nei
|
||||
@@ -365,11 +343,8 @@ flowchart TB
|
||||
secondi finché la situazione dura. Durante `drawing`, `GET /rounds/current`
|
||||
espone `draw_waiting_since` (l'orario di inizio dell'attesa); negli altri
|
||||
stati il campo è assente.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block`, `app/api/routes/rounds.py:current_round` ·
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block`, `app/api/routes/rounds.py:current_round` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_rounds_route.py`
|
||||
*Stato:* da implementare (B-73) · *Oggi:* i 360 secondi si contano
|
||||
dall'inizio dell'attesa; con 3 conferme richieste l'evento scatterebbe in
|
||||
quasi ogni round.
|
||||
- **[RND-36]** **Estrazione ripresa**: tutto ciò che l'estrazione usa
|
||||
(`server_seed`, elenco con `draw_position`, `participants_hash`,
|
||||
`draw_height`, `draw_confirmations`) è nel database dall'ingresso in
|
||||
@@ -378,12 +353,11 @@ flowchart TB
|
||||
precedente o un riavvio del server. La ripresa è idempotente: l'estrazione
|
||||
ripresa dà sempre lo stesso vincitore, e un round già passato a
|
||||
`paying_out` non viene estratto di nuovo.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._tick`, `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-81) · *Oggi:* elenco dei partecipanti e altezza
|
||||
di riferimento esistono solo in memoria e `_tick` salta i round in
|
||||
`drawing`: dopo un errore o un riavvio il round resta in `drawing` per
|
||||
sempre.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._tick`, `app/rounds/scheduler.py:RoundScheduler._complete_draw` ·
|
||||
*Test:* `tests/unit/test_draw_proof.py` (solo `_complete_draw`)
|
||||
*Stato:* da implementare (B-81) · *Oggi:* tutto ciò che serve è nel database
|
||||
e `_complete_draw` è idempotente, ma `_tick` salta i round in `drawing`:
|
||||
dopo un errore o un riavvio il round resta in `drawing` per sempre.
|
||||
- **[RND-37]** **Prova pubblica**: `GET /rounds/{id}/proof`, senza
|
||||
autenticazione, restituisce per un round: `server_seed_hash` e
|
||||
`pool_address` (dall'apertura); dall'ingresso in `drawing` l'elenco dei
|
||||
@@ -392,11 +366,8 @@ flowchart TB
|
||||
del vincitore `server_seed`, `draw_block_hash`, il seme, l'indice, il
|
||||
vincitore e, quando esiste, il txid del pagamento. Un campo non ancora
|
||||
disponibile è `null`. Il formato della risposta è in [api.md](api.md) (B-73).
|
||||
*Codice:* `app/api/routes/rounds.py` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* non esiste alcun endpoint di
|
||||
prova; `GET /rounds/current` non espone partecipanti né dati
|
||||
dell'estrazione, e i round chiusi non sono consultabili pubblicamente.
|
||||
*Codice:* `app/api/routes/rounds.py:round_proof` ·
|
||||
*Test:* `tests/unit/test_draw_proof.py`
|
||||
|
||||
### Come si verifica un'estrazione
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ solo polling.
|
||||
| Partecipanti massimi per round | 400 | no, è nel codice |
|
||||
| Tetto al montepremi del round (`max_round_pool_sats`) | 0 = nessun tetto (B-95) | da `/admin`, vale dal round successivo |
|
||||
| Conferme per i depositi (`deposit_confirmations`) | 6; 120 per le uscite coinbase (B-82) | da `/admin`; le 120 coinbase no, sono nel codice |
|
||||
| Conferme sul blocco di estrazione (`draw_confirmations`) | 3 (B-73) | da `/admin`, vale dal round successivo |
|
||||
| Conferme sul blocco di estrazione (`draw_confirmations`) | 3 | da `/admin`, vale dal round successivo |
|
||||
| Conferme per scommesse, pagamenti, prelievi | 1 | no, è nel codice |
|
||||
| Indirizzo del montepremi | uno per round, `m/84'/746'/0'/2/<id_round>` (B-80) | no, è nel codice |
|
||||
| Riserva per l'RBF sul pagamento | `max(294 sat, dimensione stimata × 10 sat/vB)`, tolta dalla quota della piattaforma, verso `m/84'/746'/0'/1/0` (B-79) | no, è nel codice |
|
||||
|
||||
@@ -275,7 +275,7 @@ server attivo, ed è la riga [ELC-27]. Le conferme delle transazioni
|
||||
|
||||
| ID | Prima di… | Si verifica che… |
|
||||
|---|---|---|
|
||||
| **[ELC-15]** | usare un blocco per l'estrazione | l'header che ciascun server restituisce per l'altezza di estrazione (`blockchain.block.header`) abbia lo stesso hash; regole complete in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#3--estrazione) (B-73) |
|
||||
| **[ELC-15]** | usare un blocco per l'estrazione | l'header che ciascun server, **attivo compreso**, restituisce per l'altezza di estrazione (`blockchain.block.header`, 80 byte con lavoro valido) abbia lo stesso hash, con almeno 2 risposte dagli altri server e la maggioranza stretta di chi ha risposto; regole complete in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#3--estrazione) (B-73) |
|
||||
| **[ELC-16]** | togliere dal saldo una moneta sparita | l'altro server non elenchi più la moneta tra quelle non spese |
|
||||
| **[ELC-17]** | accreditare un deposito | l'altro server elenchi la moneta tra quelle non spese, con lo stesso importo, a un'altezza che le dà almeno le conferme richieste ([DEP-03](registrazione-e-depositi.md#deposito)) contate sulla nostra punta |
|
||||
| **[ELC-25]** | reintegrare una moneta data per spesa fuori | l'altro server elenchi la moneta tra quelle non spese, con lo stesso importo e confermata (altezza maggiore di 0), come per un accredito |
|
||||
@@ -283,7 +283,7 @@ server attivo, ed è la riga [ELC-27]. Le conferme delle transazioni
|
||||
| **[ELC-27]** | costruire il pagamento di un round | l'altro server elenchi come non spese le monete delle scommesse del round all'indirizzo del suo montepremi; regole in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#4--pagamento) (B-82) |
|
||||
| **[ELC-28]** | abbandonare una transazione | l'altro server riporti un suo ingresso speso in un blocco da un txid diverso, oppure non conosca nessuno dei suoi txid; regole in [transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md#riconciliazione) (B-78) |
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener.corroborate_header`,
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener.agreed_block_hash`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_spent`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_credit`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_reinstatement`,
|
||||
@@ -293,10 +293,6 @@ server attivo, ed è la riga [ELC-27]. Le conferme delle transazioni
|
||||
|
||||
Stato delle righe della tabella:
|
||||
|
||||
- [ELC-15] — *Stato:* da implementare (B-73) · *Oggi:* si corrobora l'header
|
||||
del primo blocco che il server attivo ha annunciato dopo la chiusura,
|
||||
chiedendo solo agli altri server; se la verifica fallisce si passa al blocco
|
||||
successivo invece di richiedere la stessa altezza.
|
||||
- [ELC-17] — *Stato:* da implementare (B-82) · *Oggi:* basta un'altezza
|
||||
maggiore di 0 (1 conferma).
|
||||
- [ELC-27] — *Stato:* da implementare (B-82) · *Oggi:* il pagamento sceglie
|
||||
|
||||
+2
-1
@@ -51,7 +51,8 @@ business — quelli si toccano solo da qui.
|
||||
| **Bet amount (PLM)** | Il costo fisso d'ingresso per round. È anche l'importo minimo prelevabile: un prelievo sotto questa soglia viene rifiutato (i depositi non hanno un controllo minimo lato server). |
|
||||
| **Durata round (secondi)** | Quanto resta aperto un round prima di chiudersi ed estrarre il vincitore. Il taglio per le nuove giocate scatta esattamente allo scadere di questo tempo (verificato ad ogni bet, non dipende dal ciclo dello scheduler) — è un "semaforo giallo": nessuna nuova entrata, ma le bet già trasmesse prima dello scadere hanno comunque tempo di confermarsi prima che il round chiuda ed estragga. |
|
||||
| **Pausa tra un round e il successivo (secondi)** | Cooldown dopo la chiusura di un round, prima che il successivo si apra — dà tempo ai giocatori di vedere l'esito. |
|
||||
| **Durata animazione estrazione (secondi)** | Tempo minimo per cui la dashboard di ogni utente mostra l'animazione "Estrazione in corso" dopo la chiusura del round, prima di rivelare il vincitore. È solo un minimo: il processo reale aspetta fino a 3 blocchi in sequenza (conferma dell'ultima bet in sospeso, blocco dell'estrazione, conferma del payout — un blocco ogni 2 minuti in media), quindi l'animazione può durare più a lungo di questo valore, mai meno. |
|
||||
| **Durata animazione estrazione (secondi)** | Tempo minimo per cui la dashboard di ogni utente mostra l'animazione "Estrazione in corso" dopo la chiusura del round, prima di rivelare il vincitore. È solo un minimo: il processo reale aspetta più blocchi in sequenza (conferma dell'ultima bet in sospeso, il blocco dell'estrazione con le sue conferme, conferma del payout — un blocco ogni 2 minuti in media), quindi l'animazione può durare più a lungo di questo valore, mai meno. |
|
||||
| **Conferme sul blocco di estrazione** | Da 1 a 20 (predefinito 3): quanti blocchi deve avere la catena a partire dal blocco di estrazione prima che il suo hash scelga il vincitore. Più conferme rendono l'estrazione più robusta a una riorganizzazione della catena e allungano l'attesa di circa 2 minuti per blocco. Si applica dal round successivo. |
|
||||
| **Fee rate di rete (sat/vB)** | Fee per byte usata per costruire bet, payout e prelievi. |
|
||||
| **Timeout prima del fee-bump RBF (secondi)** | Dopo quanto tempo senza conferma una transazione viene ritrasmessa con fee più alta. |
|
||||
|
||||
|
||||
@@ -48,17 +48,19 @@ chiuso — attesa conferma puntate…", poi "Estrazione in corso…", poi
|
||||
compare nella dashboard di **ogni** utente, anche di chi non ha giocato in
|
||||
questo round. Questo messaggio resta visibile per l'intera durata della fase
|
||||
(chiusura → estrazione → pagamento), perché sotto la copertina servono
|
||||
**fino a tre conferme sulla rete PLM in sequenza**, una diversa dall'altra:
|
||||
**diverse conferme sulla rete PLM in sequenza**, una diversa dall'altra:
|
||||
|
||||
1. conferma dell'ultima giocata rimasta in sospeso (se ce n'era una proprio
|
||||
allo scadere del timer — altrimenti questo passo è già superato);
|
||||
2. un nuovo blocco dopo la chiusura, il cui hash serve a scegliere il
|
||||
vincitore;
|
||||
2. un blocco di estrazione, fissato alla chiusura (il primo dopo di essa), che
|
||||
deve avere in tutto **3 conferme** (il valore è configurabile dall'operatore)
|
||||
prima che il suo hash, insieme a un segreto del server impegnato
|
||||
all'apertura del round, scelga il vincitore;
|
||||
3. la conferma della transazione che paga effettivamente la vincita.
|
||||
|
||||
Con un blocco PLM ogni ~2 minuti, il tempo reale dall'azzeramento del
|
||||
timer all'accredito della vincita è quindi in media **4-6 minuti** (se
|
||||
c'era una giocata da confermare all'ultimo istante) o **2-4 minuti** (se
|
||||
timer all'accredito della vincita è quindi in media **8-10 minuti** (se
|
||||
c'era una giocata da confermare all'ultimo istante) o **6-8 minuti** (se
|
||||
tutte le giocate erano già confermate prima dello zero) — non pochi
|
||||
secondi, ed è normale.
|
||||
|
||||
|
||||
+6
-6
@@ -47,9 +47,9 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
|
||||
- **[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
|
||||
e salta `drawing`; l'attesa del blocco (`_wait_for_draw_block`) vive solo
|
||||
dentro la chiamata a `_close_and_draw` che l'ha avviata (altezza, elenco e
|
||||
segreto sono ormai salvati nel database, ma nulla li rilegge). 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
|
||||
@@ -69,7 +69,7 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
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`
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._tick` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` · `app/rounds/scheduler.py:RoundScheduler._wait_for_draw_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'è
|
||||
@@ -314,8 +314,8 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
- *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
|
||||
- *Stato:* chiuso da B-73.
|
||||
- *Come è chiuso:* 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
|
||||
|
||||
+31
-36
@@ -431,19 +431,19 @@ restano nella tabella come storico.
|
||||
| `cooldown_seconds` | Integer | no | `30` (anche `server_default`) | Pausa dopo **questo** round, copiata da `round_config` all'apertura | `open_new_round_if_needed` |
|
||||
| `bet_amount_sats` | BigInteger | no | backfill da `round_config` nella migrazione | Costo della scommessa di **questo** round, copiato all'apertura ([DAT-61]) — *da implementare (B-95)* | `open_new_round_if_needed` |
|
||||
| `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` |
|
||||
| `draw_confirmations` | Integer | no | `3` (`server_default`) | Conferme richieste sul blocco di estrazione di **questo** round, copiate all'apertura ([DAT-61]) | `open_new_round_if_needed` |
|
||||
| `pool_address` | String(128) | no | `""` solo fino alla fine della transazione di apertura; backfill con l'indirizzo di riserva per i round esistenti | Indirizzo del montepremi di **questo** round ([DAT-60]) | `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` |
|
||||
| `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]) | `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]) | `RoundScheduler._complete_draw`; `RoundScheduler._close_and_draw` (round senza partecipanti) |
|
||||
| `server_seed_hash` | String(64) | sì | `NULL` | `sha256(server_seed)` in esadecimale, pubblicato dall'apertura ([DAT-64]) | `open_new_round_if_needed` |
|
||||
| `participants_hash` | String(64) | sì | `NULL` | Impronta dell'elenco ordinato dei partecipanti ([DAT-65]) | `RoundScheduler._close_and_draw` |
|
||||
| `draw_height` | Integer | sì | `NULL` | Altezza del blocco di estrazione, fissata all'ingresso in `drawing` ([DAT-65]) | `_close_and_draw` |
|
||||
| `drawing_started_at` | DateTime | sì | `NULL` | Istante del passaggio a `drawing` | `RoundScheduler._close_and_draw` |
|
||||
| `draw_block_height` | Integer | sì | `NULL` | Altezza del blocco usato per l'estrazione; nello schema atteso è sempre uguale a `draw_height` | `_close_and_draw` |
|
||||
| `draw_block_hash` | String(64) | sì | `NULL` | Hash (esadecimale) di quel blocco | `_close_and_draw` |
|
||||
| `seed_int` | String(128) | sì | `NULL` | Il seme in decimale (fino a 78 cifre, per questo è una stringa). Atteso: il valore intero di `sha256(server_seed ‖ block_hash ‖ participants_hash)`, con la codifica definita in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) (B-73); oggi `int(draw_block_hash, 16)` | `_close_and_draw` |
|
||||
| `winner_user_id` | FK `users.id` | sì | `NULL` | Il vincitore | `_close_and_draw` |
|
||||
| `pool_amount_sats` | BigInteger | sì | `NULL` | Montepremi: somma delle scommesse confermate e ancora sulla catena ([DAT-21], [DAT-67]) | `_close_and_draw`; `RoundScheduler._trigger_payout` (atteso, B-82) |
|
||||
| `draw_block_height` | Integer | sì | `NULL` | Altezza del blocco usato per l'estrazione; è sempre uguale a `draw_height` | `_complete_draw` |
|
||||
| `draw_block_hash` | String(64) | sì | `NULL` | Hash (esadecimale) di quel blocco | `_complete_draw` |
|
||||
| `seed_int` | String(128) | sì | `NULL` | Il seme in decimale (fino a 78 cifre, per questo è una stringa). Il valore intero di `sha256(server_seed ‖ block_hash ‖ participants_hash)`, con la codifica definita in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) (RND-17) | `_complete_draw` |
|
||||
| `winner_user_id` | FK `users.id` | sì | `NULL` | Il vincitore | `_complete_draw` |
|
||||
| `pool_amount_sats` | BigInteger | sì | `NULL` | Montepremi: somma delle scommesse confermate e ancora sulla catena ([DAT-21], [DAT-67]) | `_close_and_draw`, `_complete_draw`; `RoundScheduler._trigger_payout` (atteso, B-82) |
|
||||
| `winner_amount_sats` | BigInteger | sì | `NULL` | Quanto riceve il vincitore (70% meno la commissione di rete) | `RoundScheduler._trigger_payout` |
|
||||
| `fee_amount_sats` | BigInteger | sì | `NULL` | Quanto va a `fee_address`: 30% più il resto dell'arrotondamento, meno `bump_reserve_sats` nello schema atteso | `_trigger_payout` |
|
||||
| `bump_reserve_sats` | BigInteger | sì | `NULL` | Output di riserva per l'RBF del pagamento, verso l'indirizzo di riserva ([DAT-73]) — *da implementare (B-79)* | `_trigger_payout`; `app/tx/broadcast.py:bump_fee` |
|
||||
@@ -575,7 +575,8 @@ restano nella tabella come storico.
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
`app/bets/service.py:place_bet` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` · *Test:* —
|
||||
*Stato:* da implementare (B-95) · *Oggi:* `place_bet` e `_trigger_payout`
|
||||
*Stato:* da implementare (B-95) · *Oggi:* `draw_confirmations` è già copiato
|
||||
all'apertura (B-73); `place_bet` e `_trigger_payout`
|
||||
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.
|
||||
|
||||
@@ -598,10 +599,9 @@ restano nella tabella come storico.
|
||||
([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.
|
||||
`app/rounds/scheduler.py:RoundScheduler._complete_draw` ·
|
||||
`app/api/routes/rounds.py:round_proof` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py`, `tests/unit/test_draw_proof.py`
|
||||
|
||||
- **[DAT-65]** All'ingresso in `drawing`, nella **stessa transazione** che
|
||||
cambia lo stato, si scrivono `draw_height`, `participants_hash` e il
|
||||
@@ -612,9 +612,8 @@ restano nella tabella come storico.
|
||||
dei tre cambia più. Sono ciò che permette di riprendere l'estrazione dopo un
|
||||
errore o un riavvio ottenendo lo stesso vincitore (B-81): la ripresa legge
|
||||
questi valori dal database e non ricalcola nulla.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` · *Test:* —
|
||||
*Stato:* da implementare (B-73, B-81) · *Oggi:* le colonne non esistono;
|
||||
l'elenco dei partecipanti e la punta alla chiusura restano solo in memoria.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
- **[DAT-67]** Prima di costruire il pagamento, gli outpoint delle scommesse
|
||||
vengono verificati sulla rete; i partecipanti la cui scommessa non c'è più
|
||||
@@ -662,10 +661,10 @@ Una scommessa: un utente in un round.
|
||||
| `user_id` | FK `users.id`, indicizzata | no | — | Chi scommette | `place_bet` |
|
||||
| `bet_amount_sats` | BigInteger | no | — | Quanto arriva davvero al pool: costo della scommessa **meno** la commissione di rete | `place_bet` |
|
||||
| `bet_txid` | String(64) | no | — | Txid corrente della scommessa; dopo la conferma, il txid della versione confermata ([DAT-14]) | `place_bet`; `app/tx/broadcast.py:_retarget_txid_references` |
|
||||
| `bet_vout` | Integer | no | `0` (`server_default`, backfill `0`) | Posizione dell'output verso il montepremi nella transazione della scommessa ([DAT-63]) — *da implementare (B-73)* | `place_bet` |
|
||||
| `bet_vout` | Integer | no | `0` (`server_default`, backfill `0`) | Posizione dell'output verso il montepremi nella transazione della scommessa ([DAT-63]) | `place_bet` |
|
||||
| `broadcast_at` | DateTime | no | `utcnow` | Istante della fase 1 di `place_bet`, vedi [DAT-25] | `place_bet` |
|
||||
| `confirmed_at` | DateTime | sì | `NULL` | Istante in cui la scommessa è risultata confermata | `app/bets/confirmation.py:_on_bet_confirmed` |
|
||||
| `draw_position` | Integer | sì | `NULL` | Posizione (da 0) nell'elenco ordinato dell'estrazione; valorizzata solo per i partecipanti `confirmed` all'ingresso in `drawing` ([DAT-66]) — *da implementare (B-81)* | `app/rounds/scheduler.py:RoundScheduler._close_and_draw` |
|
||||
| `draw_position` | Integer | sì | `NULL` | Posizione (da 0) nell'elenco ordinato dell'estrazione; valorizzata solo per i partecipanti `confirmed` all'ingresso in `drawing` ([DAT-66]) | `app/rounds/scheduler.py:RoundScheduler._close_and_draw` |
|
||||
| `status` | String(16) | no | `"broadcast"` (mai usato: si scrive sempre esplicitamente) | Vedi [DAT-24] | vedi diagramma |
|
||||
|
||||
- **[DAT-23]** Al massimo una scommessa per utente per round: vincolo unico su
|
||||
@@ -727,8 +726,6 @@ Una scommessa: un utente in un round.
|
||||
*Codice:* `app/bets/service.py:place_bet` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
*Stato:* da implementare (B-73) · *Oggi:* l'estrazione ordina i partecipanti
|
||||
confermati per `broadcast_at` crescente, senza criterio di parità.
|
||||
|
||||
- **[DAT-63]** `bet_vout` è l'indice dell'output della scommessa che paga
|
||||
`rounds.pool_address`; con il costruttore attuale è sempre `0`
|
||||
@@ -736,9 +733,7 @@ Una scommessa: un utente in un round.
|
||||
l'outpoint che l'elenco dell'estrazione pubblica e che il pagamento spende
|
||||
([DAT-11]).
|
||||
*Codice:* `app/bets/service.py:place_bet` ·
|
||||
`app/wallet/psbt_builder.py:build_signed_transaction` · *Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* la colonna non esiste;
|
||||
l'outpoint della scommessa non è registrato.
|
||||
`app/wallet/psbt_builder.py:RECIPIENT_VOUT` · *Test:* `tests/unit/test_bets.py`
|
||||
|
||||
- **[DAT-66]** `draw_position` si scrive una sola volta, all'ingresso in
|
||||
`drawing` ([DAT-65]), per ogni partecipante `confirmed` del round: i
|
||||
@@ -749,9 +744,8 @@ Una scommessa: un utente in un round.
|
||||
dal vincolo. `draw_position` non cambia nemmeno quando la riga passa a
|
||||
`reorged_out`.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
`app/db/models.py:RoundParticipant` · *Test:* —
|
||||
*Stato:* da implementare (B-73, B-81) · *Oggi:* la colonna non esiste;
|
||||
l'ordine esiste solo in memoria durante l'attesa del blocco.
|
||||
`app/db/models.py:RoundParticipant` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_migrations.py`
|
||||
|
||||
- **[DAT-26]** `bet_amount_sats` è l'importo del primo output della scommessa
|
||||
(`recipient_sats`), cioè il costo della scommessa del round
|
||||
@@ -793,7 +787,7 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
| `fee_rate_sat_vb` | Integer | `1` | 1 – `MAX_FEE_RATE_SAT_VB` (10 000) | Commissione di rete delle nuove transazioni | Subito, per le transazioni costruite dopo; quelle in volo tengono il proprio `fee_rate_sat_vb` |
|
||||
| `rbf_timeout_seconds` | Integer | `900` | 60 s – 7 giorni | Dopo quanto senza conferme si fa un bump RBF | Subito, anche per le transazioni già in volo (il bumper lo rilegge a ogni giro) |
|
||||
| `deposit_confirmations` | Integer | `6` | 1 – 100 | Conferme richieste per accreditare un deposito ([DAT-62]) — *da implementare (B-82)* | Subito, per gli accrediti successivi; gli output già accreditati restano |
|
||||
| `draw_confirmations` | Integer | `3` | 1 – 20 | Conferme richieste sul blocco di estrazione ([DAT-62]) — *da implementare (B-73)* | Dal round **successivo**, copiato all'apertura ([DAT-61]) |
|
||||
| `draw_confirmations` | Integer | `3` | 1 – 20 | Conferme richieste sul blocco di estrazione ([DAT-62]) | Dal round **successivo**, copiato all'apertura ([DAT-61]) |
|
||||
| `max_round_pool_sats` | BigInteger | `0` (tetto disattivato) | `0`, oppure `≥ bet_amount_sats` e `≤ 4 000 000 000 000 000` (400 × 100 000 PLM) | Tetto alla somma delle scommesse di un round ([DAT-27], [DAT-62]) — *da implementare (B-95)* | Subito, anche per il round in corso: vale per le scommesse successive |
|
||||
| `paused` | Boolean | `False` | non modificabile da `PUT /admin/config` | Pausa della lotteria | Subito per l'apertura del round successivo; il round in corso non è toccato |
|
||||
| `paused_reason` | String(16) | `NULL` | non modificabile da `PUT /admin/config` | Perché la lotteria è in pausa ([DAT-69]) | Insieme a `paused` |
|
||||
@@ -817,8 +811,8 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
valore uguale a quello attuale non conta come modifica.
|
||||
*Codice:* `app/api/routes/admin.py:update_config` ·
|
||||
`app/api/routes/admin.py:RoundConfigUpdate` · *Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-73, B-82, B-95), per le ultime tre colonne ·
|
||||
*Oggi:* le colonne modificabili sono le prime sette.
|
||||
*Stato:* da implementare (B-82, B-95), per le ultime due colonne ·
|
||||
*Oggi:* le colonne modificabili sono le prime otto.
|
||||
|
||||
- **[DAT-30]** `paused` e `paused_reason` si modificano solo con
|
||||
`POST /admin/pause` e `POST /admin/resume`, che scrivono `lottery_paused` e
|
||||
@@ -841,9 +835,10 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
[scommesse-e-prelievi.md](scommesse-e-prelievi.md) (B-95). La migrazione le
|
||||
aggiunge con un `server_default` temporaneo pari al predefinito ([DAT-55]).
|
||||
*Codice:* `app/db/models.py:RoundConfig` · `app/rounds/config.py:get_round_config` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73, B-82, B-95) · *Oggi:* le colonne non
|
||||
esistono; depositi ed estrazione usano 1 conferma e non c'è tetto.
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-82, B-95) · *Oggi:* `deposit_confirmations` e
|
||||
`max_round_pool_sats` non esistono: i depositi usano 1 conferma e non c'è
|
||||
tetto; `draw_confirmations` esiste.
|
||||
|
||||
- **[DAT-69]** `paused_reason` è `NULL` esattamente quando `paused` è falso;
|
||||
con `paused` vero vale uno di questi valori, e nessun altro:
|
||||
@@ -1214,7 +1209,7 @@ Tabella di servizio di Alembic: una riga con la revisione applicata
|
||||
| `ix_utxo_events_user_id` | `utxo_events` | indice su `user_id` | calcolo del saldo e selezione degli UTXO per utente |
|
||||
| `ix_rounds_single_active` | `rounds` | `UNIQUE ((1)) WHERE status IN ('open','closing','drawing','paying_out')` | al massimo un round attivo, [DAT-17] (B-09) |
|
||||
| unico (`round_id`, `user_id`) | `round_participants` | `UNIQUE` | una scommessa per utente per round, [DAT-23] |
|
||||
| unico (`round_id`, `draw_position`) | `round_participants` | `UNIQUE` (i `NULL` non collidono) | una posizione di estrazione per partecipante, [DAT-66] — *da implementare (B-81)* |
|
||||
| unico (`round_id`, `draw_position`) | `round_participants` | `UNIQUE` (i `NULL` non collidono) | una posizione di estrazione per partecipante, [DAT-66]; creato come indice (`ux_round_participants_round_draw_position`) |
|
||||
| unico `txid` | `pending_transaction_txids` | `UNIQUE` | un txid appartiene a una sola transazione, [DAT-70] |
|
||||
| `ix_pending_transaction_txids_pending_transaction_id` | `pending_transaction_txids` | indice | versioni di una transazione |
|
||||
| `ix_round_participants_round_id` | `round_participants` | indice | partecipanti di un round |
|
||||
|
||||
+1
-1
@@ -521,7 +521,7 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
|
||||
`open`, quindi nessuna scommessa nuova può entrare.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._close_and_draw`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._wait_for_next_block`
|
||||
`app/rounds/scheduler.py:RoundScheduler._wait_for_draw_block`
|
||||
|
||||
### Round bloccato in `closing`
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ fa il codice oggi:
|
||||
`drawing` come `tip_at_close + 1` e non cambia più.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* si usa la punta vista al primo
|
||||
*Stato:* da implementare (B-nn) · *Oggi:* si usa la punta vista al primo
|
||||
controllo utile, che può essere un blocco successivo.
|
||||
```
|
||||
|
||||
|
||||
+4
-7
@@ -64,8 +64,6 @@ ricompensa di quel blocco.
|
||||
motivo di scartarlo; un server che trasmette non può scegliere il blocco,
|
||||
perché l'altezza è fissata e l'header a quell'altezza deve essere
|
||||
confermato dal quorum ([ELC-15](connessione-electrum.md#5-il-quorum)).
|
||||
*Oggi* (B-73): il seme è l'hash del primo blocco visto dopo la chiusura,
|
||||
senza segreto né impegno, e l'estrazione è manipolabile da chi mina.
|
||||
- **Header falsi.** Il bersaglio che un header dichiara nel campo `bits` lo sceglie chi
|
||||
fabbrica l'header (con `bits = 0x207fffff` un header mai minato passava con
|
||||
probabilità 1/2): per questo dal B-74 il
|
||||
@@ -107,10 +105,10 @@ Restano **accettati**, e descritti in
|
||||
| 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 |
|
||||
|
||||
*Oggi* (B-91, B-73): la chiave di cifratura è la variabile d'ambiente
|
||||
*Oggi* (B-91): la chiave di cifratura è la variabile d'ambiente
|
||||
`XPRV_ENCRYPTION_KEY` in `.env`, visibile a chiunque esegua `docker inspect`
|
||||
sul container; `.env` e i file di chiave hanno i permessi predefiniti del
|
||||
sistema (di norma `0644`, leggibili da tutti); il round non ha alcun segreto.
|
||||
sistema (di norma `0644`, leggibili da tutti).
|
||||
`JWT_SECRET` e `ADMIN_TOKEN` restano variabili d'ambiente anche dopo B-91: le
|
||||
vede con `docker inspect` solo chi ha accesso a Docker, che è già fidato come
|
||||
l'host.
|
||||
@@ -834,9 +832,8 @@ La struttura completa delle tabelle è in [modello-dati.md](modello-dati.md).
|
||||
([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
|
||||
riesce, il server non parte.
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/wallet/hd.py` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* il segreto non esiste.
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/wallet/keystore.py:encrypt_round_seed`, `app/wallet/keystore.py:decrypt_round_seed` ·
|
||||
*Test:* `tests/unit/test_rounds_service.py`, `tests/unit/test_draw_proof.py`
|
||||
- **[SEC-48]** Nessun segreto finisce nei log o nell'audit log: né password
|
||||
(nemmeno quella generata dal reset), né WIF, né token, né la master key o
|
||||
la sua chiave di cifratura, né `server_seed` prima della rivelazione. Il
|
||||
|
||||
+3
-3
@@ -221,7 +221,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
|
||||
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
||||
|---|---|---|---|---|---|
|
||||
| B-73 | L'estrazione dipende solo dall'hash del blocco: un minatore con biglietti o il server Electrum attivo possono pilotarla; ordine dei partecipanti non verificabile | Critica | **aperto** | — | RND, ELC |
|
||||
| B-73 | L'estrazione dipende solo dall'hash del blocco: un minatore con biglietti o il server Electrum attivo possono pilotarla; ordine dei partecipanti non verificabile | Critica | *da registrare* | `test_draw.py`, `test_draw_proof.py`, `test_scheduler.py`, `test_electrum_listener.py`, `test_rounds_service.py`, `test_migrations.py` | RND, ELC |
|
||||
| B-74 | Header accettati senza controllo della difficoltà minima; un salto di più blocchi non viene collegato; una riorganizzazione di un blocco chiude la sessione | Critica | *da registrare* | `test_electrum_listener.py`, `test_draw.py` | ELC |
|
||||
| B-75 | Una risposta Electrum oltre 64 KiB chiude la connessione, e la cronologia dell'indirizzo del montepremi la supera dopo ~800 transazioni | Critica | *da registrare* | `test_electrum_client.py`, `test_deposits.py`, `test_psbt_builder.py` | ELC, TX |
|
||||
| B-76 | Un invio ambiguo (timeout, connessione caduta) viene annullato come se fosse rifiutato | Critica | *da registrare* | `test_broadcast_outcome.py`, `test_electrum_client.py` | TX, BET, WDR |
|
||||
@@ -252,9 +252,9 @@ 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.
|
||||
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.
|
||||
- **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 (4 critici,
|
||||
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (3 critici,
|
||||
4 alti, 6 medi), da implementare nell'ordine di
|
||||
[piano-correzioni.md](piano-correzioni.md).
|
||||
- **Prossimo numero libero:** `B-99`.
|
||||
Reference in new issue
Block a user