Snapshot the bet cost, fee address and pool cap onto the round at open, and cap the pool (B-95, RND-05, RND-06, RND-23, BET-20, BET-21, DAT-26, DAT-27, DAT-61, DAT-62, API-21, API-30, API-31, ADR-10, LIM-16)

Only the duration and the cooldown (and, since B-73, the draw confirmations) were copied onto
the round when it opened. The cost of a bet and the fee address were read live, so an edit from
/admin in the middle of a round gave the players of that round different stakes for the same
chance and redirected the commission of the round in progress.

- `rounds.bet_amount_sats`, `rounds.fee_address` and `rounds.max_round_pool_sats` are copied from
  round_config in the transaction that opens the round (Alembic a7d3e9f15b48: plain add_column,
  existing rounds backfilled with the current configuration, i.e. what they were using; the
  expression indexes untouched). `place_bet` charges the round's cost, `_trigger_payout` pays the
  round's fee address (an empty one on the row still stops the payout, as before),
  `GET /rounds/current` advertises the round's cost while one is active and the configured one
  otherwise. The minimum withdrawal and the fee rate stay live, by design (ADR-20, ADR-10).
- `round_config.max_round_pool_sats` (0 = none, otherwise >= the bet cost and <= 4e15, in
  /admin and PUT/GET /admin/config): a bet is refused with `round_full` (params also carry
  `max_round_pool_sats`) once the sum of the round's participant rows, in-flight ones included,
  has reached the round's cap, before anything moves; the pool can exceed it by one bet at most.
  A cap under the bet cost is a 422 `max_round_pool_below_bet_amount`.
- PUT /admin/config's payability check (RND-40) now also judges a fee-rate change against the
  bet cost the round in progress opened with, which can be lower than the configured one.
- `error.round_full` no longer says "maximum of N players" (it also fires for the pool cap): the
  seven translations are generic.

Ambiguity resolved: the spec said the cap is snapshotted (RND-05, BET-20, ARC) in some places and
"applies immediately, also to the round in progress" in the round_config table of modello-dati;
the snapshot wins, with a column on `rounds` (modello-dati/api/guide-admin brought in line).

Tests changed because the spec changed: the payout fixtures seed the round's own fee address, and
the old "fee address missing" payout test now empties the round's address (clearing it in the
configuration mid-round must not stop the payout). New test_round_snapshot.py: the copy at open,
edits reaching only the next round, every bet of a round paying the same stake whether the cost
is raised or lowered meanwhile, the payout following the round's address (and surviving it being
cleared in the configuration), the cap (exactly at the cap is full, in-flight bets count, no cap,
a mid-round edit of the cap does not apply, the refused player's coins untouched), plus the route,
the admin bounds and the two 422s, the fee-rate check against the round's own cost, and the
migration backfill. Mutation-checked: charging the live cost, paying the live address, not
copying the cap and `>` instead of `>=` each fail tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-16 15:12:00 +02:00
1 parent ccd15807ed
commit 8b182ac8b6
28 files changed
+520 -121

No files matched your search

@@ -1,6 +1,6 @@
# ADR-10 — I parametri del round sono fotografati all'apertura
- **Stato:** Accettata — implementazione da completare (B-95)
- **Stato:** Accettata
- **Area:** Configurazione e ambiente
- **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*.
@@ -8,7 +8,7 @@
**I parametri del round sono fotografati all'apertura.**
Durata, pausa successiva, costo della scommessa (`bet_amount_sats`),
`fee_address` e `draw_confirmations` vengono copiati sulla riga del round
`fee_address`, `draw_confirmations` e tetto al montepremi (`max_round_pool_sats`) vengono copiati sulla riga del round
quando si apre; una modifica vale dal round successivo, mai per quello in
corso. La pausa fra due round è quella annunciata dal round appena chiuso.
Il fee rate resta letto dal vivo: riguarda la singola transazione, non le
@@ -22,6 +22,4 @@ regole del round.
[ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#0--apertura) (B-95).
*Codice:* `app/rounds/service.py:open_new_round_if_needed` · `app/rounds/service.py:round_deadline` ·
*Test:* `tests/unit/test_rounds_service.py::test_a_new_round_snapshots_the_current_config_timing`, `tests/unit/test_scheduler.py::test_tick_ignores_a_config_duration_edited_mid_round`, `tests/unit/test_rounds_service.py::test_cooldown_comes_from_the_round_that_closed`
*Stato:* da implementare (B-95) · *Oggi:* sono fotografati solo durata e
pausa; costo, `fee_address` e fee rate sono letti dal vivo ([LIM-16]).
*Test:* `tests/unit/test_round_snapshot.py::test_a_round_copies_cost_fee_address_and_cap_when_it_opens`, `tests/unit/test_round_snapshot.py::test_an_edit_from_admin_never_reaches_the_round_in_progress_but_the_next_one`, `tests/unit/test_rounds_service.py::test_a_new_round_snapshots_the_current_config_timing`, `tests/unit/test_scheduler.py::test_tick_ignores_a_config_duration_edited_mid_round`, `tests/unit/test_rounds_service.py::test_cooldown_comes_from_the_round_that_closed`
@@ -11,8 +11,7 @@ probabilità uguale per tutti** indipendentemente dall'importo effettivo.
Nessun deposito minimo.
- *Conseguenze:* l'estrazione sceglie fra partecipanti, non pesa gli
importi. Il costo è fotografato all'apertura del round ([ADR-10](ADR-10-parametri-round-fotografati-all-apertura.md)), così
tutti i partecipanti di un round pagano la stessa posta (B-95; oggi è letto
dal vivo, [LIM-16]).
tutti i partecipanti di un round pagano la stessa posta (B-95).
*Codice:* `app/bets/service.py:place_bet` · `app/rounds/draw.py:draw_winner` ·
*Test:* `tests/unit/test_bets.py::test_place_bet_rejects_second_bet_same_round`
+11 -15
View File
@@ -467,10 +467,9 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
casi (successo, rifiuto, esito ambiguo) e all'apertura del round.
*Codice:* `app/api/routes/bets.py:create_bet`,
`app/bets/service.py:place_bet` ·
*Test:* `tests/unit/test_bets.py`
*Stato:* da implementare (B-95, B-96) · *Oggi:* un costo coperto solo da resto non confermato
risponde `insufficient_balance`; non c'è tetto al montepremi; il costo è
quello configurato al momento della scommessa, non quello del round.
*Test:* `tests/unit/test_bets.py`, `tests/unit/test_round_snapshot.py`
*Stato:* da implementare (B-96) · *Oggi:* un costo coperto solo da resto non
confermato risponde `insufficient_balance`.
#### POST /withdrawals
@@ -537,7 +536,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
| `opened_at` | stringa ISO o `null` | apertura del round |
| `closes_at` | stringa ISO o `null` | scadenza per le scommesse: `opened_at` + la durata **fissata all'apertura del round** (non quella attuale di configurazione) |
| `participant_count` | intero | partecipanti **confermati**, cioè quelli tra cui si estrae |
| `bet_amount_sats` | intero | costo che pagherebbe una nuova scommessa: con un round attivo il costo **fissato all'apertura del round** (`rounds.bet_amount_sats`), senza round attivo quello configurato — *da implementare (B-95); oggi sempre quello configurato* |
| `bet_amount_sats` | intero | costo che pagherebbe una nuova scommessa: con un round attivo il costo **fissato all'apertura del round** (`rounds.bet_amount_sats`), senza round attivo quello configurato |
| `jackpot_sats` | intero | 70% (arrotondato per difetto) della somma degli importi effettivi delle scommesse confermate; limite superiore, perché la commissione del pagamento è ancora da sottrarre |
| `pending_participant_count` | intero | tutti i partecipanti, confermati **e** in volo (totale, non differenza) |
| `pending_jackpot_sats` | intero | 70% della somma di tutte le scommesse, confermate e in volo |
@@ -577,8 +576,6 @@ 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-95), per i campi marcati · *Oggi:*
`bet_amount_sats` è sempre il costo configurato.
#### GET /rounds/stream
@@ -760,7 +757,6 @@ 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-95) · *Oggi:* manca `max_round_pool_sats`.
#### PUT /admin/config
@@ -779,7 +775,7 @@ dei parametri operativi in
| `draw_animation_seconds` | 0 – 600 |
| `deposit_confirmations` | 1 – 100 |
| `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)* |
| `max_round_pool_sats` | `0` (disattivato), oppure da `bet_amount_sats` a 4 000 000 000 000 000 |
I limiti sono quelli di [modello-dati.md](modello-dati.md) (tabella
`round_config`), che ne è il proprietario. `paused` e `paused_reason`
@@ -793,18 +789,18 @@ dei parametri operativi in
dà `422` con `detail = {"code": "config_not_payable", "message", "params":
{"reason"}}`, dove `reason` è `winner_share_below_network_fee` o
`commission_below_dust_limit`; il controllo usa i valori risultanti dopo
la modifica.
la modifica e, se cambia `fee_rate_sat_vb`, anche il costo con cui si è
aperto il round in corso. Un tetto `max_round_pool_sats` diverso da 0 e
minore del costo della scommessa (dopo la modifica) dà `422` con
`detail.code = "max_round_pool_below_bet_amount"`.
- **Effetti:** scrive `config_updated` con i valori prima/dopo dei soli
campi davvero cambiati; nessuna scrittura se nulla cambia. Durata, pausa
tra round, costo della scommessa, `fee_address` e `draw_confirmations` si
applicano dal round successivo (per costo e `fee_address`: *da
implementare*, B-95; oggi subito). Nessuna notifica SSE.
applicano dal round successivo, come il tetto `max_round_pool_sats`. Nessuna
notifica SSE.
*Codice:* `app/api/routes/admin.py:update_config`,
`app/api/routes/admin.py:RoundConfigUpdate` ·
*Test:* `tests/unit/test_admin.py`
*Stato:* da implementare (B-95), per `max_round_pool_sats` e la copia
all'apertura di costo ed `fee_address` · *Oggi:* sono accettati i primi
nove campi.
#### POST /admin/pause e POST /admin/resume
-1
View File
@@ -616,7 +616,6 @@ 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-95) · *Oggi:* `max_round_pool_sats` non esiste.
- **[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
-2
View File
@@ -187,8 +187,6 @@ 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-95), per `max_round_pool_sats` ·
*Oggi:* i campi sono i primi nove.
- **[AUD-11]** `lottery_paused` — la lotteria è stata messa in pausa, da
chiunque. Payload `{"reason"}`, più `"note"` quando l'operatore l'ha
+4 -13
View File
@@ -105,10 +105,7 @@ richiesta di scommessa (vedi
Il round usa sempre i valori fotografati: una modifica da `/admin` vale dal
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) · *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.
*Test:* `tests/unit/test_rounds_service.py`, `tests/unit/test_scheduler.py` (durata e pausa), `tests/unit/test_round_snapshot.py`
- **[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
@@ -119,12 +116,9 @@ richiesta di scommessa (vedi
`lottery_configured`) perché `/` mostri avvisi diversi. Le procedure sono in
[guida-admin.md](guida-admin.md).
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/api/routes/rounds.py:current_round`, `app/api/routes/admin.py:RoundConfigUpdate` ·
*Test:* `tests/unit/test_rounds_service.py`, `tests/unit/test_rounds_route.py`
*Stato:* da implementare (B-95) · *Oggi:* il pagamento legge `fee_address`
dalla configurazione a ogni tentativo: una modifica durante il round
sposta la commissione del round in corso, e un `fee_address` vuoto (solo
modificando il database a mano) blocca il pagamento con
`no fee_address configured`.
*Test:* `tests/unit/test_rounds_service.py`, `tests/unit/test_rounds_route.py`, `tests/unit/test_round_snapshot.py::test_the_payout_pays_the_fee_address_the_round_opened_with`, `tests/unit/test_round_snapshot.py::test_clearing_the_fee_address_mid_round_does_not_stop_its_payout`
Un `fee_address` vuoto sulla riga del round (solo modificando il database a
mano) blocca il pagamento con `no fee_address configured`.
- **[RND-07]** L'apertura di un round invia una notifica SSE.
*Codice:* `app/rounds/service.py:open_new_round_if_needed`
- **[RND-34]** **Impegno sul segreto**: all'apertura il server genera un
@@ -438,9 +432,6 @@ del montepremi del round.
`commission_below_dust_limit`).
*Codice:* `app/wallet/psbt_builder.py:build_payout_transaction` ·
*Test:* `tests/unit/test_payout_builder.py::test_payout_deducts_fee_only_from_winner_share`, `tests/unit/test_payout_builder.py::test_payout_folds_excess_input_value_into_the_reserve_output`, `tests/unit/test_payout_builder.py::test_payout_leaves_a_dust_sized_excess_to_the_network_fee`, `tests/unit/test_payout_builder.py::test_payout_with_one_participant_at_the_smallest_viable_pool`
*Stato:* da implementare (B-95) · *Oggi:* `fee_address` è letto dalla
configurazione corrente al momento del pagamento, non fotografato
all'apertura.
- **[RND-24]** **Prima si scrive, poi si invia**: prima dell'invio il round
registra importo del vincitore (`winner_amount_sats`), commissioni
(`fee_amount_sats`), riserva per l'RBF (`bump_reserve_sats`, [RND-38]) e
+3 -2
View File
@@ -47,9 +47,10 @@ business — quelli si toccano solo da qui.
| Campo | Significato |
|---|---|
| **Fee address** | L'indirizzo PLM su cui finisce il 30% di ogni round (fee). Obbligatorio: su un'istanza nuova è vuoto, e finché resta vuoto **non si apre nessun round** (il payout non sarebbe costruibile, quindi il round accetterebbe scommesse per poi restare bloccato con i soldi già nel montepremi). Il pannello lo segnala con un avviso in cima ai Parametri, e la pagina utente mostra un banner "lotteria non ancora pronta". Una volta impostato non si può più svuotare dal pannello: un valore vuoto non è un indirizzo `plm1...` valido e viene rifiutato. **Attenzione:** oggi il payout legge questo campo al momento in cui viene costruito, quindi cambiarlo mentre un round è in corso sposta anche la commissione di quel round sul nuovo indirizzo. |
| **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). |
| **Fee address** | L'indirizzo PLM su cui finisce il 30% di ogni round (fee); una modifica vale dal round successivo, mai per quello in corso. Obbligatorio: su un'istanza nuova è vuoto, e finché resta vuoto **non si apre nessun round** (il payout non sarebbe costruibile, quindi il round accetterebbe scommesse per poi restare bloccato con i soldi già nel montepremi). Il pannello lo segnala con un avviso in cima ai Parametri, e la pagina utente mostra un banner "lotteria non ancora pronta". Una volta impostato non si può più svuotare dal pannello: un valore vuoto non è un indirizzo `plm1...` valido e viene rifiutato. **Attenzione:** oggi il payout legge questo campo al momento in cui viene costruito, quindi cambiarlo mentre un round è in corso sposta anche la commissione di quel round sul nuovo indirizzo. |
| **Bet amount (PLM)** | Il costo fisso d'ingresso per round, **dal round successivo**: il round in corso tiene il costo con cui si è aperto, così tutti i suoi giocatori pagano la stessa posta. È anche l'importo minimo prelevabile, e questo vale subito: un prelievo sotto questa soglia viene rifiutato (i depositi non hanno un controllo minimo lato server). Un valore con cui un round non si potrebbe pagare (commissione di rete, riserva e soglia di polvere non coperte) viene rifiutato. |
| **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. |
| **Tetto al montepremi di un round (PLM)** | 0 = nessun tetto. Con un tetto, una scommessa è rifiutata (`round_full`) quando la somma delle scommesse del round lo ha raggiunto; il montepremi può quindi superarlo al massimo di una scommessa. Non può essere minore del costo della scommessa. Vale dal round successivo. |
| **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 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 per accreditare un deposito** | Da 1 a 100 (predefinito 6): dopo quante conferme un deposito arrivato all'indirizzo di un utente viene accreditato e diventa spendibile. Un deposito è l'unico ingresso che un utente può spendere due volte con una catena privata, perciò il valore non va abbassato senza motivo. Vale subito per gli accrediti successivi; le uscite di mining (coinbase) ne richiedono comunque 120, e le transazioni costruite dalla piattaforma (resto di una bet, vincite) una sola. |
+4 -13
View File
@@ -350,19 +350,10 @@ implementato il [piano delle correzioni](piano-correzioni.md).
### Configurazione ed economia
- **[LIM-16]** **Solo i tempi del round sono fotografati all'apertura.** Costo
della scommessa, fee rate e `fee_address` sono letti dal vivo.
- *Impatto:* cambiare il costo a metà round fa pagare poste diverse a
giocatori con la stessa probabilità, e cambia subito il prelievo minimo;
cambiare `fee_address` a metà round devia la commissione del round in
corso (la modifica è comunque registrata in audit).
- *Stato:* da chiudere con B-95.
- *Per chiuderlo:* fotografare costo, `fee_address` e `draw_confirmations`
sulla riga del round, come già si fa per durata e pausa ([ADR-10](adr/ADR-10-parametri-round-fotografati-all-apertura.md)). Il fee
rate resta letto dal vivo per scelta, e il prelievo minimo segue il costo
corrente della configurazione ([ADR-20](adr/ADR-20-prelievo-minimo-costo-corrente-scommessa.md)).
*Codice:* `app/bets/service.py:place_bet` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/withdrawals/service.py:request_withdrawal`
- ~~[LIM-16]~~ Rimosso: chiuso da B-95. Costo della scommessa, `fee_address` e
tetto al montepremi sono fotografati sulla riga del round all'apertura, come
durata e pausa ([ADR-10](adr/ADR-10-parametri-round-fotografati-all-apertura.md)); il fee rate resta letto dal vivo per scelta e il prelievo
minimo segue il costo corrente della configurazione ([ADR-20](adr/ADR-20-prelievo-minimo-costo-corrente-scommessa.md)).
- **[LIM-17]** **Nessun consolidamento automatico del montepremi.** Monete
avanzate e resti si accumulano sugli indirizzi del montepremi (fino a B-80
+25 -31
View File
@@ -98,9 +98,10 @@ erDiagram
datetime closed_at
int duration_seconds "snapshot"
int cooldown_seconds "snapshot"
bigint bet_amount_sats "snapshot (da implementare)"
string fee_address "snapshot (da implementare)"
int draw_confirmations "snapshot (da implementare)"
bigint bet_amount_sats "snapshot"
string fee_address "snapshot"
bigint max_round_pool_sats "snapshot"
int draw_confirmations "snapshot"
string pool_address
string server_seed_enc "cifrato fino alla rivelazione (da implementare)"
string server_seed "in chiaro solo dopo la rivelazione (da implementare)"
@@ -140,9 +141,9 @@ erDiagram
int draw_animation_seconds
int fee_rate_sat_vb
int rbf_timeout_seconds
int deposit_confirmations "da implementare"
int deposit_confirmations
int draw_confirmations "da implementare"
bigint max_round_pool_sats "da implementare"
bigint max_round_pool_sats
bool paused
string paused_reason "da implementare"
datetime updated_at
@@ -426,8 +427,9 @@ restano nella tabella come storico.
| `closed_at` | DateTime | sì | `NULL` | Chiusura delle scommesse, poi conferma del pagamento ([RND-28](ciclo-di-vita-del-round.md)) | `app/rounds/scheduler.py:RoundScheduler._tick`; `app/rounds/confirmation.py:_on_payout_confirmed` |
| `duration_seconds` | Integer | no | `600` (anche `server_default`) | Durata di **questo** round, copiata da `round_config` all'apertura | `open_new_round_if_needed` |
| `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` |
| `bet_amount_sats` | BigInteger | no | backfill da `round_config` nella migrazione | Costo della scommessa di **questo** round, copiato all'apertura ([DAT-61]) | `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]) | `open_new_round_if_needed` |
| `max_round_pool_sats` | BigInteger | no | `0` (`server_default`); backfill `0` | Tetto alla somma delle scommesse di **questo** round, copiato all'apertura ([DAT-61], [DAT-27]); `0` = nessun tetto | `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]) | `open_new_round_if_needed` |
@@ -560,22 +562,20 @@ restano nella tabella come storico.
tabella con righe; il codice lo riscrive prima del commit e ogni consumatore
rifiuta un indirizzo vuoto.
- **[DAT-61]** `bet_amount_sats`, `fee_address` e `draw_confirmations` di
`rounds` sono una **copia** dei valori di `round_config` presa quando il
- **[DAT-61]** `bet_amount_sats`, `fee_address`, `max_round_pool_sats` e
`draw_confirmations` di `rounds` sono una **copia** dei valori di `round_config` presa quando il
round si apre, nella stessa transazione dell'inserimento, e non cambiano
più. Le scommesse del round costano `rounds.bet_amount_sats`, il suo
pagamento paga la commissione a `rounds.fee_address`, la sua estrazione
attende `rounds.draw_confirmations` conferme. La regola di dominio è in
attende `rounds.draw_confirmations` conferme, le scommesse oltre il tetto
`rounds.max_round_pool_sats` sono rifiutate. La regola di dominio è in
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) (B-95). La
migrazione riempie le righe esistenti con i valori attuali di
`round_config` ([DAT-55]).
*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:* `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.
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
*Test:* `tests/unit/test_round_snapshot.py`, `tests/unit/test_rounds_service.py`, `tests/unit/test_migrations.py`
- **[DAT-64]** Il segreto del round sono 32 byte casuali generati con
`secrets.token_bytes(32)`. All'inserimento del round si scrivono insieme
@@ -748,21 +748,18 @@ Una scommessa: un utente in un round.
moltiplicando il numero di partecipanti per il costo (B-11).
*Codice:* `app/bets/service.py:place_bet` ·
`app/wallet/psbt_builder.py:build_signed_transaction` ·
`app/api/routes/rounds.py:current_round` · *Test:* `tests/unit/test_bets.py`
*Stato:* da implementare (B-95) · *Oggi:* il costo è
`round_config.bet_amount_sats` letto al momento della scommessa.
`app/api/routes/rounds.py:current_round` · *Test:* `tests/unit/test_bets.py`, `tests/unit/test_round_snapshot.py`
- **[DAT-27]** Il numero di righe di un round, contando tutti gli stati, non
supera `MAX_PARTICIPANTS_PER_ROUND` (400): la 401ª scommessa è rifiutata con
`round_full` prima che si muova denaro (B-52). Con
`round_config.max_round_pool_sats` diverso da 0 ([DAT-62]), una scommessa è
rifiutata con `round_full` anche quando la somma dei `bet_amount_sats` del
round, contando tutti gli stati, ha già raggiunto quel tetto; la regola di
`rounds.max_round_pool_sats` (copiato da `round_config` all'apertura,
[DAT-61], [DAT-62]) diverso da 0, una scommessa è rifiutata con `round_full`
anche quando la somma dei `bet_amount_sats` del round, contando tutti gli
stati, ha già raggiunto quel tetto; la regola di
dominio è in [scommesse-e-prelievi.md](scommesse-e-prelievi.md) (B-95).
*Codice:* `app/bets/service.py:place_bet` · `app/wallet/psbt_builder.py` ·
*Test:* `tests/unit/test_bets.py`
*Stato:* da implementare (B-95), solo per il tetto sul montepremi · *Oggi:*
esiste solo il limite dei 400 partecipanti.
*Test:* `tests/unit/test_bets.py`, `tests/unit/test_round_snapshot.py`
### `round_config`
@@ -772,8 +769,8 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
| Colonna | Tipo | Default | Limiti accettati da `PUT /admin/config` | Significato | Quando ha effetto |
|---|---|---|---|---|---|
| `id` | Integer PK | autoincrement | — | — | — |
| `fee_address` | String(128), non null | nessun default nel modello; la riga nasce con `""` | indirizzo PLM bech32 valido | Dove va il 30% di ogni round | Atteso: dal round **successivo**, copiato all'apertura ([DAT-61]). Oggi: subito, si legge a ogni costruzione del pagamento, quindi vale anche per il round in corso se il suo pagamento non è ancora stato costruito |
| `bet_amount_sats` | BigInteger | `1_000_000_000` (10 PLM) | `> 0`, `≤ 100 000 PLM` | Costo della scommessa; anche il prelievo minimo | Atteso: per le scommesse dal round **successivo** ([DAT-61]), per il prelievo minimo subito. Oggi: subito, per le scommesse e i prelievi successivi; le scommesse già piazzate tengono il proprio importo ([DAT-26]) |
| `fee_address` | String(128), non null | nessun default nel modello; la riga nasce con `""` | indirizzo PLM bech32 valido | Dove va il 30% di ogni round | Dal round **successivo**, copiato all'apertura ([DAT-61]); il round in corso paga la commissione all'indirizzo con cui si è aperto |
| `bet_amount_sats` | BigInteger | `1_000_000_000` (10 PLM) | `> 0`, `≤ 100 000 PLM` | Costo della scommessa; anche il prelievo minimo | Per le scommesse, dal round **successivo** ([DAT-61]); per il prelievo minimo, subito ([ADR-20](adr/ADR-20-prelievo-minimo-costo-corrente-scommessa.md)). Le scommesse già piazzate tengono il proprio importo ([DAT-26]) |
| `round_duration_seconds` | Integer | `600` | 30 s – 7 giorni | Durata del round | Dal round **successivo** ([RND-05](ciclo-di-vita-del-round.md)) |
| `round_cooldown_seconds` | Integer | `30` | 0 s – 24 h | Pausa dopo la chiusura | Dal round **successivo**: la pausa dopo il round in corso è quella copiata alla sua apertura ([RND-05](ciclo-di-vita-del-round.md)) |
| `draw_animation_seconds` | Integer | `20` | 0 – 600 s | Durata minima dell'animazione dell'estrazione nel frontend; non condiziona l'estrazione vera | Subito (letto da `GET /rounds/current`) |
@@ -781,7 +778,7 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
| `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]) | Subito, per gli accrediti successivi; gli output già accreditati restano |
| `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 |
| `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]) | Dal round **successivo**, copiato all'apertura ([DAT-61]): un round tiene il tetto con cui si è aperto |
| `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` |
| `updated_at` | DateTime | `utcnow`, aggiornato a ogni modifica (`onupdate`) | — | Ultima modifica della riga | ORM |
@@ -804,8 +801,7 @@ 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-95), per l'ultima colonna ·
*Oggi:* le colonne modificabili sono le prime nove.
- **[DAT-30]** `paused` e `paused_reason` si modificano solo con
`POST /admin/pause` e `POST /admin/resume`, che scrivono `lottery_paused` e
@@ -827,8 +823,6 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
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:* `tests/unit/test_admin.py`, `tests/unit/test_migrations.py`
*Stato:* da implementare (B-95) · *Oggi:* `max_round_pool_sats` non esiste:
non c'è tetto.
- **[DAT-69]** `paused_reason` è `NULL` esattamente quando `paused` è falso;
con `paused` vero vale uno di questi valori, e nessun altro:
+2 -5
View File
@@ -53,7 +53,7 @@ cifra. Il **costo** è quello del round ([BET-21]):
| **[BET-04]** | Il round è in stato `open` e la sua **scadenza** — apertura più la durata fotografata all'apertura (`opened_at + duration_seconds`), non lo stato nel database — non è ancora passata | `round_closing` |
| **[BET-05]** | L'utente non ha già una scommessa in questo round (in qualunque stato) | `already_betting` |
| **[BET-06]** | Il round ha meno di 400 partecipanti (`MAX_PARTICIPANTS_PER_ROUND`), contando anche quelli non ancora confermati | `round_full` (parametro `max_participants`) |
| **[BET-20]** | Se il round ha un tetto al montepremi (`max_round_pool_sats` fotografato all'apertura, [RND-05](ciclo-di-vita-del-round.md#0--apertura)) diverso da 0, la somma degli importi netti dei partecipanti del round, contando anche quelli non ancora confermati, è minore del tetto. *Stato:* da implementare (B-95) · *Oggi:* il tetto non esiste | `round_full` |
| **[BET-20]** | Se il round ha un tetto al montepremi (`max_round_pool_sats` fotografato all'apertura, [RND-05](ciclo-di-vita-del-round.md#0--apertura)) diverso da 0, la somma degli importi netti dei partecipanti del round, contando anche quelli non ancora confermati, è minore del tetto. | `round_full` |
| **[BET-07]** | La somma delle monete confermate e non spese copre il costo della scommessa | `balance_pending_confirmation` se lo coprirebbe il saldo comprensivo di quanto è in corso (parametro `pending_sats`), altrimenti `insufficient_balance` (parametro `required_sats`), come per il prelievo ([WDR-05](#prelievo)). *Stato:* da implementare (B-96) · *Oggi:* sempre `insufficient_balance`, anche quando il saldo mostrato all'utente copre la scommessa |
| **[BET-08]** | Il costo si copre con al massimo 50 monete (`MAX_TX_INPUTS`), scelte dalla più grande fino a coprirlo | `too_many_inputs` (parametro `max_inputs`) |
| **[BET-09]** | Le monete scelte ([BET-08]) superano il costo di almeno 294 sat (`DUST_LIMIT_SATS`), che restano come resto | `balance_leaves_no_change` (parametro `required_extra_sats`) |
@@ -88,10 +88,7 @@ ancora `open` oltre la scadenza.
([RND-05](ciclo-di-vita-del-round.md#0--apertura)), non il valore corrente
di `round_config`: una modifica da `/admin` vale dal round successivo.
*Codice:* `app/bets/service.py:place_bet` ·
*Test:* —
*Stato:* da implementare (B-95) · *Oggi:* il costo è letto da
`round_config` a ogni scommessa, quindi una modifica vale subito anche per
il round in corso.
*Test:* `tests/unit/test_round_snapshot.py::test_every_bet_of_a_round_pays_the_roundd_cost_whatever_is_edited_meanwhile`
- **[BET-11]** La scadenza viene verificata due volte: all'arrivo della
richiesta ([BET-04]) e di nuovo dopo aver costruito e firmato la
transazione. L'iscrizione al round viene poi salvata nella stessa
+3 -3
View File
@@ -243,7 +243,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
| B-92 | Nessuna verifica di salute o prontezza, stato Electrum invisibile, audit non filtrabile, log incompleti | Media | **aperto** | — | OPS, AUD, API |
| B-93 | Deploy senza arresto ordinato, rollback impossibile, `SITE_ADDRESS` che torna a `localhost` | Media | **aperto** | — | OPS |
| B-94 | Cambio password, login e bug report senza limiti adeguati; errori non strutturati in alcune risposte | Media | **aperto** | — | SEC, API |
| B-95 | Costo della scommessa e `fee_address` letti dalla configurazione corrente invece che fotografati all'apertura | Media | **aperto** | — | RND, BET |
| B-95 | Costo della scommessa e `fee_address` letti dalla configurazione corrente invece che fotografati all'apertura | Media | *da registrare* | `test_round_snapshot.py`, `test_admin.py`, `test_rounds_route.py`, `test_scheduler.py` | RND, BET |
| B-96 | Incoerenze minori di stato (prelievo ridotto sotto il minimo, importi di un pagamento abbandonato, audit mancanti, attesa in `closing` senza segnali, …) | Media | **aperto** | — | TX, BET, WDR, DEP |
| B-97 | Le migrazioni possono eliminare in silenzio gli indici su espressione | Media | *da registrare* | `test_migrations.py` | DAT |
| B-98 | La selezione delle monete si ferma alla prima che copre l'importo e rifiuta una scommessa pagabile | Media | **aperto** | — | 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, B-73, B-81, B-79, B-82, B-87.
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, B-81, B-79, B-82, B-87, B-95.
- **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 (0 critici,
3 alti, 6 medi), da implementare nell'ordine di
3 alti, 5 medi), da implementare nell'ordine di
[piano-correzioni.md](piano-correzioni.md).
- **Prossimo numero libero:** `B-99`.