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>
11 KiB
Come funziona PLM Lottery
Panoramica del funzionamento della piattaforma (l'indice completo della documentazione è README.md). È il punto di partenza: ogni sezione rimanda al documento che la approfondisce.
| Documento | Cosa spiega |
|---|---|
| registrazione-e-depositi.md | Come nasce un account, come arrivano i fondi, come si calcola il saldo mostrato |
| scommesse-e-prelievi.md | Come si piazza una scommessa e come si ritirano i fondi |
| ciclo-di-vita-del-round.md | Apertura, chiusura, estrazione del vincitore, pagamento |
| transazioni-e-riconciliazione.md | Come ogni movimento di denaro viene registrato, inviato, accelerato (RBF) e riconciliato con la catena |
| connessione-electrum.md | Come il server parla con la rete PLM e perché non si fida di un solo server |
| architettura.md | Struttura del codice, processi in background, configurazione, frontend, traduzioni, test |
| modello-dati.md | Tabelle del database, campi, stati, vincoli, migrazioni |
| api.md | Tutti gli endpoint HTTP, formato delle risposte, catalogo dei codici di errore |
| sicurezza.md | Modello di fiducia, chiavi e segreti, autenticazione, limiti sui tentativi, header HTTP |
| audit-log.md | Catalogo degli eventi del registro di audit |
| operativita.md | Backup, riavvii, procedure per gli incidenti |
| adr/ | Le decisioni di progetto con le loro motivazioni, una per file (ADR) |
| limiti-noti.md | Limiti noti accettati |
| storico-bug.md | Tutti i bug B-nn trovati e corretti, con commit e test |
| piano-correzioni.md | Decisioni della revisione del 2026-09-28 e ordine in cui implementare le correzioni ancora aperte |
Questi documenti sono la specifica del software: ogni regola ha un
identificativo stabile (ad esempio RND-11) con il riferimento al codice che
la implementa. Come si usano per modificare il software è spiegato in
processo-di-modifica.md.
Dalla revisione del 2026-09-28 la specifica descrive il comportamento
atteso, che in alcuni punti è più avanti del codice. I requisiti che il
codice non soddisfa ancora sono marcati *Stato:* da implementare (B-nn) e
dicono in una riga cosa fa il codice oggi (git grep "da implementare" li
elenca tutti); anche questa panoramica descrive il comportamento atteso. Le
decisioni prese e l'ordine di implementazione sono in
piano-correzioni.md, il dettaglio tecnico di ogni voce
in BUGS.md.
Per installazione e avvio vedi setup.md e running-the-server.md; per l'uso delle interfacce guida-utente.md e guida-admin.md.
In una frase
Gli utenti depositano PLM su un indirizzo personale custodito dal server, comprano un biglietto a costo fisso per il round in corso, e allo scadere del round un seme che combina un segreto su cui il server si è impegnato all'apertura, l'hash di un blocco successivo alla chiusura e l'elenco dei partecipanti sceglie un vincitore, che riceve il 70% del montepremi; il 30% va alla piattaforma.
Scelte di fondo
- Custodiale. Le chiavi sono generate e tenute dal server (una master xprv cifrata a riposo). L'utente non controlla nulla finché non preleva.
- Un indirizzo per utente, per sempre. Lo stesso indirizzo serve per i depositi, riceve le vincite e riceve il resto di ogni scommessa e prelievo.
- Nessun nodo completo. Tutto l'accesso alla rete passa dal protocollo Electrum (vedi connessione-electrum.md).
- 6 conferme per i depositi, 1 per tutto il resto. Il deposito è l'unico ingresso che un utente può spendere due volte, perché scommesse, pagamenti e prelievi li firma solo il server; le uscite coinbase richiedono 120 conferme (vedi registrazione-e-depositi.md).
- Estrazione con commit-reveal. All'apertura il server si impegna su un segreto pubblicandone l'impronta; alla chiusura si fissano elenco dei partecipanti e altezza del blocco; il segreto viene rivelato dopo l'estrazione. Chi produce i blocchi non può calcolare il vincitore e l'operatore non può cambiare il segreto; chiunque può verificare il risultato con la prova pubblica (vedi ciclo-di-vita-del-round.md).
- Un indirizzo del montepremi per ogni round, così soldi e cronologia di un round restano separati da quelli degli altri.
- Nel dubbio non si liberano i fondi. Una transazione viene annullata solo con una prova, mai per un timeout o per il silenzio di un solo server (vedi transazioni-e-riconciliazione.md).
- Parametri operativi nel database, modificabili a caldo da
/admin(costo della scommessa, durata del round, commissioni…). Nel file.envrestano solo segreti e collegamenti infrastrutturali.
Le cinque fasi
Diagramma del comportamento atteso:
flowchart LR
subgraph REG["1 · Registrazione"]
direction TB
A1["Username + password"] --> A2["Il server deriva un indirizzo<br/>dedicato e permanente"]
end
subgraph DEP["2 · Deposito"]
direction TB
B1["L'utente invia PLM<br/>al proprio indirizzo"] --> B2{"6 conferme e<br/>corroborato dagli<br/>altri server?"}
B2 -- "No" --> B1b["Si riprova al prossimo<br/>aggiornamento"] --> B2
B2 -- "Sì" --> B3["Saldo accreditato"]
end
subgraph PLAY["3 · Scommessa"]
direction TB
C1{"Saldo sufficiente,<br/>nessuna scommessa attiva,<br/>round aperto e non pieno?"}
C1 -- "Sì" --> C2["Transazione: indirizzo utente →<br/>montepremi del round, resto all'utente"]
C2 --> C3{"Confermata?"}
C3 -- "No, troppo tempo" --> C4["Commissione aumentata<br/>(RBF) e reinvio"] --> C3
C3 -- "Sì" --> C5["Partecipante del round"]
end
subgraph DRAW["4 · Round ed estrazione"]
direction TB
D0["Apertura: indirizzo del montepremi<br/>del round, impronta del segreto pubblicata"] --> D1["Scadenza del round"]
D1 --> D2["Si attendono le scommesse<br/>già in volo"]
D2 --> D3["Si fissano partecipanti<br/>e altezza del blocco"]
D3 --> D3b["Blocco con 3 conferme, header<br/>concordato dal quorum:<br/>segreto + blocco + partecipanti<br/>scelgono il vincitore"]
D3b --> D3c["Segreto rivelato,<br/>prova pubblica"]
D3c --> D3d{"Scommesse ancora<br/>sulla catena?"}
D3d -- "Sì (quelle sparite<br/>sono escluse)" --> D4["70% al vincitore, 30% alla piattaforma<br/>meno la riserva per l'RBF"]
D3d -- "Sparita quella<br/>del vincitore" --> D5["Pagamento bloccato,<br/>lotteria in pausa"]
end
subgraph WITHDRAW["5 · Prelievo"]
direction TB
E1["Indirizzo esterno + importo"] --> E2["Transazione: indirizzo utente →<br/>esterno, resto all'utente"]
E2 --> E3{"Confermata?"}
E3 -- "No, troppo tempo" --> E4["RBF e reinvio"] --> E3
E3 -- "Sì" --> E5["Saldo aggiornato"]
end
A2 --> B1
B3 --> C1
B3 --> E1
C5 --> D1
D4 -.->|"round successivo"| C1
- Registrazione — registrazione-e-depositi.md
- Deposito — registrazione-e-depositi.md
- Scommessa — scommesse-e-prelievi.md
- Round ed estrazione — ciclo-di-vita-del-round.md
- Prelievo — scommesse-e-prelievi.md
Componenti
Un solo processo Python (FastAPI) dietro Caddy, con un database SQLite. Dentro il processo girano, oltre all'API HTTP, sei attività in background avviate all'avvio del server:
| Attività | Ogni | Cosa fa |
|---|---|---|
ElectrumListener |
connessione continua, ping ogni 60s | L'unica connessione alla rete: riceve i nuovi blocchi, osserva gli indirizzi di tutti gli utenti, accredita i depositi |
RoundScheduler |
5s | Apre e chiude i round, esegue l'estrazione, avvia e riprova i pagamenti |
ConfirmationPoller |
10s | Porta le transazioni da "in attesa" a "confermate" |
RbfBumper |
30s | Aumenta la commissione di ciò che non conferma entro il tempo limite |
PendingTransactionReconciler |
all'avvio, poi 120s | Confronta con la catena ogni transazione rimasta a metà |
DepositReconciler |
300s | Ricontrolla tutti gli indirizzi, nel caso una notifica sia andata persa |
La cadenza di queste attività è ciò che determina quanto in fretta il sistema si ripara da solo dopo un problema.
Le due interfacce web sono pagine statiche servite dallo stesso server, senza build né framework:
/— l'interfaccia per i giocatori (registrazione, deposito, scommessa, prelievo, profilo, stato del round), tradotta in 7 lingue./admin— il pannello operatore, protetto da un token condiviso, in italiano.
Entrambe interrogano periodicamente l'API e in più ascoltano un canale
SSE (GET /rounds/stream): un semplice segnale "è cambiato qualcosa,
ricarica" inviato a ogni evento rilevante (round aperto o cambiato di stato,
scommessa o prelievo inviato, transazione confermata, deposito accreditato,
nuovo blocco). Se il canale cade, le pagine continuano a funzionare con il
solo polling.
Parametri principali
| Parametro | Valore predefinito | Modificabile |
|---|---|---|
| Costo della scommessa | 10 PLM | da /admin, vale dal round successivo (B-95) |
| Ripartizione del montepremi | 70% vincitore / 30% piattaforma | no, è nel codice |
| Durata del round | 600s | da /admin, vale dal round successivo |
| Pausa tra un round e l'altro | 30s | da /admin, vale dal round successivo |
| Commissione di rete | 1 sat/vB | da /admin |
| Tempo prima di un RBF | 900s | da /admin |
| Prelievo minimo | pari al costo della scommessa | segue il costo |
| Deposito minimo | nessuno | — |
| 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 | 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 |
Un B-nn nella tabella indica un parametro del comportamento atteso non ancora implementato (vedi piano-correzioni.md).