Files
plm-lottery/docs/adr/ADR-24-l-estrazione-commit-reveal-fra-server.md
T
davideandClaude Sonnet 5.5 db24287ea5 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>
2026-10-14 16:29:09 +02:00

59 lines
3.8 KiB
Markdown

# ADR-24 — L'estrazione è un commit-reveal fra il server e un blocco futuro
- **Stato:** Accettata
- **Area:** Round ed estrazione
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
## Decisione
**L'estrazione è un commit-reveal fra il server e un blocco
futuro** (decisione del 2026-09-28). All'apertura del round il server genera
un segreto casuale di 32 byte (`server_seed`), lo salva e pubblica solo la
sua impronta `server_seed_hash = sha256(server_seed)`. All'ingresso in
`drawing` si fissano l'elenco dei partecipanti confermati, ordinato per txid
della scommessa crescente, la sua impronta `participants_hash` (sha256 delle
righe `txid:vout:indirizzo`) e l'altezza del blocco di estrazione
(`draw_height`, [ADR-25](ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md)). Il seme è
`sha256(server_seed || block_hash || participants_hash)` letto come intero,
l'indice del vincitore è seme modulo numero di partecipanti. Dopo il
salvataggio del vincitore il server rivela `server_seed` e pubblica la prova
completa (`GET /rounds/{id}/proof`), da cui chiunque ricalcola l'esito.
L'algoritmo resta un **componente sostituibile**: il resto del sistema non
va architettato attorno alla sua forma, ma ogni sostituto deve mantenere le
stesse quattro proprietà — impegno pubblicato prima delle scommesse, elenco
dei partecipanti fissato prima del blocco, esito ricalcolabile dai dati
pubblicati, esito non calcolabile da chi produce o trasmette i blocchi.
- *Motivazione:* il vincitore non deve essere calcolabile da chi controlla
la produzione dei blocchi: la potenza di calcolo di PLM (~22 TH/s) si può
affittare. Un minatore che non conosce `server_seed` non sa chi vincerebbe
con il suo blocco e quindi non ha motivo di scartarlo; l'operatore non
può cambiare `server_seed` dopo averne pubblicato l'impronta e non conosce
il blocco quando si impegna. L'ordine per txid e l'impronta dell'elenco
rendono l'esito riproducibile e impediscono di aggiungere o togliere
partecipanti dopo aver visto il blocco (B-73).
- *Alternative scartate:*
- *hash del solo blocco* (la forma precedente): un minatore con biglietti
in gioco trattiene i blocchi finché uno lo fa vincere, e il server
Electrum attivo sceglie quale blocco reale usare semplicemente non
annunciandone uno;
- *hash di più blocchi consecutivi*: chi produce l'ultimo blocco della
serie può ancora scartarlo e riprovare, quindi resta manipolabile;
- *segreti forniti dai giocatori* (commit-reveal fra partecipanti): chi non
rivela blocca o condiziona l'estrazione, e servirebbe un'azione in più a
ogni giocatore;
- *sorgente di casualità esterna* (beacon): una dipendenza di rete in più,
di cui fidarsi quanto del server.
- *Conseguenze:* l'operatore, che conosce `server_seed`, può calcolare
l'esito appena il blocco è noto: una collusione fra operatore e minatore
resta possibile ed è accettata ([LIM-25]), perché l'operatore è già
fidato come custode ([LIM-26]). `server_seed` non è esposto da nessun
endpoint prima della rivelazione, e nel database è cifrato con la chiave
della master xprv finché non viene rivelato
([sicurezza.md](../sicurezza.md#sec-56) SEC-56): una copia del database da
sola non basta a conoscerlo. Regole in
[ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#3--estrazione),
endpoint in [api.md](../api.md) (B-73).
*Codice:* `app/rounds/draw.py:draw_winner` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw` · `app/rounds/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`