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
+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
|
||||
|
||||
|
||||
Reference in new issue
Block a user