Give every round its own pool address (B-80, RND-35, DAT-60, DAT-11, REG-14, SEC-08, BET-12, RND-22, WDR-14, SEC-43, API-23, API-36)

All bets of all rounds paid one address (m/84'/746'/0'/1/0), so its history
grew forever, other rounds' money and stray coins mixed with a round's own, and
the payout picked from everything it held.

- hd.py: derive_pool_key/derive_pool_address(round_id) on branch 2
  (m/84'/746'/0'/2/<round_id>); no argument is the reserve address 1/0.
  derive_round_pool_key checks the key against the address stored on the round,
  so a payout can never be signed for money its key does not control.
- rounds.pool_address (migration 477bddae1a84, existing rounds backfilled with
  the reserve address since that is where their bets went; add_column, not a
  batch alter, so the expression indexes stay): written in the transaction that
  inserts the round, once its id exists.
- place_bet pays round.pool_address; the payout reads, signs and takes change at
  the round's own address; own_address_for and bump_fee's _signing_context follow
  the round (confirmation poller and reconciler pass round_id).
- Withdrawals to any round pool address, the reserve address or fee_address are
  refused (withdrawal_to_platform_address, 400, all seven languages).
- GET /rounds/current and GET /admin/rounds expose pool_address.
- RND-22/DAT-11 stay "da implementare" for B-82: the payout still spends every
  confirmed coin at the round's address, not only the participants' outpoints
  (participants carry no bet_vout yet).
- tests/unit/conftest.py: every test now gets a throwaway master key. Opening a
  round derives an address, and tests that merely opened a round were silently
  reading the real key from the local .env.
- Existing tests changed because the spec changed: payout fixtures give their
  round a pool_address; the reconciler's address-cache test uses two payouts of
  one round (rows of different rounds no longer share an address);
  own_address_for now takes the round id.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-14 13:52:32 +02:00
1 parent cc63753439
commit fd6a91e93d
37 files changed
+673 -144

No files matched your search

@@ -1,6 +1,6 @@
# ADR-03 — Ogni round ha il proprio indirizzo del montepremi
- **Stato:** Accettata — implementazione da completare (B-80)
- **Stato:** Accettata
- **Area:** Custodia, chiavi e indirizzi
- **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,10 +30,7 @@ commissioni è rifiutato. Il ramo `0` resta riservato agli utenti.
lì, visibile ma fuori da ogni pagamento; il margine fra partecipanti e
ingressi del pagamento ([ADR-21](ADR-21-tre-limiti-distinti-sul-numero-monete.md)) non si consuma più nel tempo ([LIM-17]).
Regole in [ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#4--pagamento)
e [scommesse-e-prelievi.md](../scommesse-e-prelievi.md#prelievo) (B-80).
e [scommesse-e-prelievi.md](../scommesse-e-prelievi.md#prelievo).
*Codice:* `app/wallet/hd.py:derive_pool_key` · `app/wallet/hd.py:derive_pool_address` · `app/rounds/service.py:open_new_round_if_needed` ·
*Test:* `tests/unit/test_pending_address.py`
*Stato:* da implementare (B-80) · *Oggi:* un unico indirizzo del montepremi
(`1/0`) riceve le scommesse di tutti i round e il pagamento sceglie fra
tutte le sue monete.
*Codice:* `app/wallet/hd.py:derive_pool_key` · `app/wallet/hd.py:derive_pool_address` · `app/wallet/hd.py:derive_round_pool_key` · `app/rounds/service.py:open_new_round_if_needed` ·
*Test:* `tests/unit/test_round_pool_address.py`, `tests/unit/test_pending_address.py`
+8 -9
View File
@@ -504,7 +504,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
finché la rete non torna); poi `400`: `invalid_address`,
`withdrawal_to_own_address`, `withdrawal_to_platform_address`
(destinazione uguale a un indirizzo di montepremi, di riserva o delle
commissioni, B-80), `amount_below_minimum` (`params.minimum_sats` = costo
commissioni), `amount_below_minimum` (`params.minimum_sats` = costo
attuale della scommessa; verificato anche sull'importo ridotto di un
prelievo dell'intero saldo, B-96), `balance_pending_confirmation`
(`params.pending_sats`), `insufficient_balance` (`params.required_sats`),
@@ -521,10 +521,9 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
*Codice:* `app/api/routes/withdrawals.py:create_withdrawal`,
`app/withdrawals/service.py:request_withdrawal` ·
*Test:* `tests/unit/test_withdrawals.py`
*Stato:* da implementare (B-76, B-80, B-96) · *Oggi:* ogni esito diverso
*Stato:* da implementare (B-76, B-96) · *Oggi:* ogni esito diverso
dal successo è `502 broadcast_failed` con il prelievo `failed`; la
trasmissione va solo al server attivo; un prelievo verso l'indirizzo del
pool o delle commissioni è accettato; l'importo ridotto di un prelievo
trasmissione va solo al server attivo; l'importo ridotto di un prelievo
dell'intero saldo non è riconfrontato con il minimo.
### Round
@@ -560,7 +559,7 @@ esiste una API separata per l'amministrazione, solo un'autenticazione diversa.
| `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)* |
| `pool_address` | stringa o `null` | indirizzo del montepremi del round, verificabile su un esploratore — *da implementare (B-80)* |
| `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 |
| `paused_reason` | stringa o `null` | `operator`, `reorg`, `payout_blocked`, `resurrected_tx`, oppure `null` se non in pausa ([modello-dati.md](modello-dati.md), DAT-69) — *da implementare (B-78, B-82, B-85)* |
@@ -585,7 +584,7 @@ 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-80, B-82, B-85, B-95), per i campi
*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.
@@ -898,8 +897,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-80, B-82), per gli ultimi cinque
campi · *Oggi:* la lista si ferma a `payout_txid`.
*Stato:* da implementare (B-73, B-79, B-82), per gli ultimi quattro
campi · *Oggi:* la lista si ferma a `pool_address`.
#### GET /admin/audit-log
@@ -1065,7 +1064,7 @@ dei parametri operativi in
| `withdrawal_to_own_address` | 400 | `app/withdrawals/service.py:request_withdrawal` | — | destinazione uguale all'indirizzo di deposito dell'utente | sì |
| `amount_below_minimum` | 400 | `app/withdrawals/service.py:request_withdrawal` | `minimum_sats` | prelievo sotto il costo attuale della scommessa | sì |
| `internal_error` | 500 | `app/main.py:log_unhandled_exception` | — | eccezione non gestita | sì |
| `withdrawal_to_platform_address` | 400 | `app/withdrawals/service.py:request_withdrawal` | — | destinazione uguale a un indirizzo di montepremi, di riserva o delle commissioni — *da implementare (B-80)* | no — da aggiungere |
| `withdrawal_to_platform_address` | 400 | `app/withdrawals/service.py:request_withdrawal` | — | destinazione uguale a un indirizzo di montepremi, di riserva o delle commissioni | sì |
| `service_busy` | 503 | `app/bets/service.py:place_bet`, `app/withdrawals/service.py:request_withdrawal`, `app/auth/routes.py:register` | — | il database non ha concesso il lock di scrittura entro il timeout; nulla è stato trasmesso, si può riprovare | sì |
| `registration_unavailable` | 503 | `app/auth/routes.py:register` | — | registrazione sospesa finché il controllo degli indici di derivazione dopo un ripristino non è concluso o risolto ([registrazione-e-depositi.md](registrazione-e-depositi.md), B-90) — *da implementare* | no — da aggiungere |
| `not_authenticated` | 401 | dipendenza Bearer di FastAPI | — | header `Authorization` assente — oggi `detail` stringa — *da implementare (B-94)* | no — da aggiungere |
+1 -1
View File
@@ -191,7 +191,7 @@ sono vuoti e non sono elencati.
|---|---|
| `app/wallet/plm_network.py` | Parametri della rete PLM per embit (`PLM_MAINNET`), coin type 746, `ACCOUNT_PATH` |
| `app/wallet/keystore.py` | Cifratura/decifratura Fernet della master xprv |
| `app/wallet/hd.py` | Generazione della master key, derivazione di indirizzi/chiavi utente (`0/i`) e del pool (`1/0`), export WIF; la chiave d'account decifrata resta in cache nel processo. *Da implementare:* indirizzo del montepremi per round (`2/<round_id>`), con `1/0` come indirizzo di riserva (B-80) |
| `app/wallet/hd.py` | Generazione della master key, derivazione di indirizzi/chiavi utente (`0/i`), del montepremi di ogni round (`2/<round_id>`, B-80) e dell'indirizzo di riserva (`1/0`), `derive_round_pool_key` che sceglie la chiave che controlla l'indirizzo salvato sul round, export WIF; la chiave d'account decifrata resta in cache nel processo |
| `app/wallet/address.py` | `is_valid_plm_address`: checksum bech32 + HRP `plm` |
| `app/wallet/balance.py` | `recompute_balance` (saldo confermato), `compute_pending_balance` (saldo mostrato) — vedi [registrazione-e-depositi.md](registrazione-e-depositi.md) |
| `app/wallet/psbt_builder.py` | Selezione UTXO, costruzione e firma di scommessa, prelievo e pagamento; limiti `MAX_TX_INPUTS`, `MAX_PAYOUT_TX_INPUTS`, `MAX_PARTICIPANTS_PER_ROUND`, `DUST_LIMIT_SATS`. *Da implementare:* uscita di riserva per l'RBF sul pagamento (B-79) |
+10 -10
View File
@@ -152,16 +152,14 @@ richiesta di scommessa (vedi
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
round pagano quell'indirizzo
([scommesse-e-prelievi.md](scommesse-e-prelievi.md#cosa-succede) (B-80)) e il
([scommesse-e-prelievi.md](scommesse-e-prelievi.md#cosa-succede)) e il
pagamento spende solo monete di quell'indirizzo ([RND-22]). I round aperti
prima di questa modifica mantengono l'indirizzo condiviso
`m/84'/746'/0'/1/0`, che diventa l'**indirizzo di riserva** ([RND-38]).
Il rifiuto dei prelievi verso indirizzi di montepremi, riserva o
commissioni è in [scommesse-e-prelievi.md](scommesse-e-prelievi.md#prelievo) (B-80).
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/wallet/hd.py:derive_pool_key` ·
*Test:* —
*Stato:* da implementare (B-80) · *Oggi:* tutti i round usano lo stesso
indirizzo `m/84'/746'/0'/1/0` (`derive_pool_key`).
commissioni è in [scommesse-e-prelievi.md](scommesse-e-prelievi.md#prelievo).
*Codice:* `app/rounds/service.py:open_new_round_if_needed`, `app/wallet/hd.py:derive_pool_key`, `app/wallet/hd.py:derive_round_pool_key` ·
*Test:* `tests/unit/test_round_pool_address.py`
## 1 · Round aperto
@@ -472,10 +470,12 @@ del montepremi del round.
dalla più grande fino a coprire il montepremi; non abbastanza monete dà
`insufficient pool UTXOs`.
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout`, `app/wallet/psbt_builder.py:build_payout_transaction` ·
*Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_payout_builder.py`
*Stato:* da implementare (B-80, B-82) · *Oggi:* spende le monete confermate
dell'indirizzo condiviso lette dal solo server attivo, scelte dalla più
grande fino a coprire il montepremi, qualunque sia la loro provenienza.
*Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_payout_builder.py`, `tests/unit/test_round_pool_address.py`
*Stato:* da implementare (B-82) · *Oggi:* spende le monete confermate
dell'indirizzo del round (per un round vecchio, di quello condiviso) lette
dal solo server attivo, scelte dalla più grande fino a coprire il
montepremi, e non solo le uscite delle scommesse dei partecipanti: un'altra
moneta arrivata a quell'indirizzo verrebbe spesa.
- **[RND-23]** La transazione ha un'uscita verso l'indirizzo del vincitore,
una verso l'indirizzo delle commissioni fotografato all'apertura
([RND-05]) e una verso l'indirizzo di riserva ([RND-38]). Se gli ingressi
+8 -6
View File
@@ -439,15 +439,17 @@ implementato il [piano delle correzioni](piano-correzioni.md).
*Codice:* `app/bets/service.py:place_bet` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/withdrawals/service.py:request_withdrawal`
- **[LIM-17]** **Nessun consolidamento automatico del montepremi.** Monete
avanzate e resti si accumulano sull'unico indirizzo del montepremi; il
margine fra 400 partecipanti e 500 ingressi ([ADR-21](adr/ADR-21-tre-limiti-distinti-sul-numero-monete.md)) si consuma nel tempo.
avanzate e resti si accumulano sugli indirizzi del montepremi (fino a B-80
su uno solo, per tutti i round); il margine fra 400 partecipanti e 500
ingressi ([ADR-21](adr/ADR-21-tre-limiti-distinti-sul-numero-monete.md)) si consumava nel tempo.
- *Impatto:* raggiunto il limite, il pagamento fallisce con
`too_many_inputs` e il round resta in `paying_out`, con un `payout_failed`
ogni 60s.
- *Stato:* da chiudere con B-80.
- *Per chiuderlo:* con un indirizzo per round ([ADR-03](adr/ADR-03-ogni-round-ha-proprio-indirizzo-montepremi.md)) il pagamento spende
solo le monete del proprio round, e il margine non si consuma più. Resta
accettato dopo il piano: le riserve per l'RBF ([ADR-17](adr/ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md)) e le monete
- *Stato:* la parte del margine che si consumava è chiusa da B-80: con un
indirizzo per round ([ADR-03](adr/ADR-03-ogni-round-ha-proprio-indirizzo-montepremi.md)) il pagamento spende solo le monete del proprio
round, e il margine non si consuma più (resta lo stato descritto sopra per
i round aperti prima di B-80, che condividono l'indirizzo di riserva).
- *Resta accettato:* le riserve per l'RBF ([ADR-17](adr/ADR-17-commissione-pagamento-esce-dalla-quota-vincitore.md)) e le monete
estranee inviate agli indirizzi di round chiusi si accumulano senza
consolidamento automatico; recuperarle è un'operazione manuale
([operativita.md](operativita.md#intervento-manuale-sui-fondi)).
+23 -17
View File
@@ -102,7 +102,7 @@ erDiagram
bigint bet_amount_sats "snapshot (da implementare)"
string fee_address "snapshot (da implementare)"
int draw_confirmations "snapshot (da implementare)"
string pool_address "da implementare"
string pool_address
string server_seed_enc "cifrato fino alla rivelazione (da implementare)"
string server_seed "in chiaro solo dopo la rivelazione (da implementare)"
string server_seed_hash "da implementare"
@@ -362,9 +362,10 @@ del database sono tutti uguali, perché arrivano tutti dallo stesso percorso
*Codice:* `app/rounds/scheduler.py:_reserved_payout_outpoints` ·
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
`app/tx/reconcile.py:_abandon` · *Test:* `tests/unit/test_scheduler.py`
*Stato:* da implementare (B-80, B-82) · *Oggi:* un solo indirizzo del pool
(`m/84'/746'/0'/1/0`) per tutti i round; il pagamento sceglie fra tutti i
suoi UTXO confermati, di qualunque round o provenienza.
*Stato:* da implementare (B-82) · *Oggi:* il pagamento sceglie fra tutti gli
UTXO confermati dell'indirizzo del round (per un round vecchio, del
condiviso), lette dal solo server attivo, e non solo fra le uscite delle
scommesse dei partecipanti.
- **[DAT-12]** `spent_txid` ha tre significati:
- `NULL`: l'output è spendibile e conta nel saldo;
@@ -439,7 +440,7 @@ restano nella tabella come storico.
| `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` |
| `pool_address` | String(128) | no | backfill con l'indirizzo di riserva per i round esistenti | Indirizzo del montepremi di **questo** round ([DAT-60]) — *da implementare (B-80)* | `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` |
@@ -561,9 +562,14 @@ restano nella tabella come storico.
indirizzi distinti per costruzione (percorso diverso per ogni `id`), i round
vecchi condividono l'indirizzo di riserva.
*Codice:* `app/rounds/service.py:open_new_round_if_needed` ·
`app/wallet/hd.py:derive_pool_key` · *Test:* —
*Stato:* da implementare (B-80) · *Oggi:* la colonna non esiste; tutte le
scommesse di tutti i round pagano `m/84'/746'/0'/1/0`.
`app/wallet/hd.py:derive_pool_key` ·
`migrations/versions/477bddae1a84_add_pool_address_to_rounds.py` ·
*Test:* `tests/unit/test_round_pool_address.py`
La colonna ha `""` come valore di inserimento (l'`id` non esiste prima del
`flush`) e `""` come `server_default`, che serve solo ad aggiungerla a una
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
@@ -923,8 +929,7 @@ Le funzioni citate sono `app/bets/service.py:place_bet`,
questi identificativi, che non cambiano, e non dal txid, che cambia a ogni
bump (B-02). L'indirizzo di cui controllare la cronologia si ricava da
`kind` e `user_id` e, per un `payout`, da `rounds.pool_address` del
`round_id` (atteso, B-80; oggi è l'unico indirizzo del pool per tutti i
pagamenti).
`round_id` (B-80).
*Codice:* `app/tx/pending_address.py:own_address_for` ·
`app/tx/broadcast.py:_signing_context` ·
`app/bets/confirmation.py:_on_bet_confirmed` ·
@@ -1114,7 +1119,7 @@ dall'utente.
|---|---|---|---|---|---|
| `id` | Integer PK | no | autoincrement | — | database |
| `user_id` | FK `users.id`, indicizzata | no | — | Chi preleva | `app/withdrawals/service.py:request_withdrawal` |
| `external_address` | String(128) | no | — | Destinatario: un indirizzo PLM valido, diverso da quello dell'utente e (atteso, B-80) da ogni indirizzo di montepremi, di riserva e delle commissioni | `request_withdrawal` |
| `external_address` | String(128) | no | — | Destinatario: un indirizzo PLM valido, diverso da quello dell'utente e da ogni indirizzo di montepremi, di riserva e delle commissioni | `request_withdrawal` |
| `amount_requested_sats` | BigInteger | no | — | Importo chiesto dall'utente | `request_withdrawal` |
| `amount_sent_sats` | BigInteger | sì (sempre valorizzata dal codice) | `NULL` | Quanto riceve il destinatario, vedi [DAT-40] | `request_withdrawal` |
| `txid` | String(64) | sì | `NULL` | Txid corrente; `NULL` dopo un fallimento | `request_withdrawal`; `_retarget_txid_references`; `_release_failed_withdrawal`; `_abandon` |
@@ -1314,9 +1319,9 @@ letti con una scansione completa.
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
`app/wallet/psbt_builder.py:build_payout_transaction` ·
*Test:* `tests/unit/test_payout_builder.py`, `tests/unit/test_scheduler.py`
*Stato:* da implementare (B-79, B-80, B-82) · *Oggi:* tutti i round
condividono un indirizzo, il pagamento sceglie fra tutti i suoi UTXO
confermati e può avere un resto verso quello stesso indirizzo; non c'è
*Stato:* da implementare (B-79, B-82) · *Oggi:* il pagamento sceglie fra
tutti gli UTXO confermati dell'indirizzo del round e può avere un resto
verso quello stesso indirizzo; non c'è
riserva, e vale `pool_amount_sats` = `winner_amount_sats` +
`fee_amount_sats` + commissione di rete.
@@ -1439,7 +1444,7 @@ letti con una scansione completa.
schema. Una migrazione che usi `batch_alter_table` su `rounds` o `users`
deve quindi, nella stessa `upgrade()`, eliminare e ricreare esplicitamente
l'indice relativo con SQL grezzo, dopo il blocco `batch_alter_table`. Le
migrazioni di B-73/B-80/B-81/B-95 aggiungono colonne a `rounds`: se
migrazioni di B-73/B-81/B-95 (e quella di B-80, già scritta) aggiungono colonne a `rounds`: se
aggiungono solo colonne con `op.add_column` (che su SQLite non ricrea la
tabella) l'indice resta; se usano `batch_alter_table`, devono ricrearlo. Il
test di [DAT-75] è ciò che rende questa regola verificabile.
@@ -1499,10 +1504,11 @@ In ordine di applicazione (ogni riga rivede la precedente):
| `ee8508d98d34` | Tabella `bug_reports`, con un booleano `resolved` |
| `be71fdac734e` | `bug_reports.resolved` sostituito da `status` a tre valori (`resolved` → `'resolved'`, il resto → `'open'`) |
| `c1d4a97b5e10` | Indice `ix_users_username_lower` (B-57), [DAT-58] |
| `283844a44b4a` | `rounds.duration_seconds`/`cooldown_seconds` (B-61), backfill dalla configurazione attuale — **head** |
| `283844a44b4a` | `rounds.duration_seconds`/`cooldown_seconds` (B-61), backfill dalla configurazione attuale |
| `477bddae1a84` | `rounds.pool_address` (B-80, [DAT-60]), backfill con l'indirizzo di riserva per i round esistenti — **head** |
Revisioni attese, non ancora scritte (ciascuna nel commit della sua voce,
con i backfill di [DAT-55]): `rounds` (`pool_address`, `bet_amount_sats`,
con i backfill di [DAT-55]): `rounds` (`bet_amount_sats`,
`fee_address`, `draw_confirmations`, `server_seed`, `server_seed_hash`,
`participants_hash`, `draw_height`, `bump_reserve_sats`, `payout_blocked_at`),
`round_participants` (`bet_vout`, `draw_position` e il vincolo unico),
+10 -7
View File
@@ -593,10 +593,13 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
pagamento `building` o `pending` non risolto. Controllare in "Transazioni
pendenti" se c'è un `payout` di un round precedente ancora aperto: finché
resta tale, i suoi input sono esclusi. L'indirizzo del montepremi non è
mostrato dal pannello; si ottiene con
`docker compose exec app python -c "from app.wallet.hd import derive_pool_address; print(derive_pool_address())"`
e se ne può controllare il saldo con un explorer. Dopo B-80 ogni round ha
il proprio indirizzo (`rounds.pool_address`), e dopo B-82 un'uscita di
mostrato dal pannello (solo `GET /rounds/current` e `GET /admin/rounds` ne
danno `pool_address`); ogni round ha il proprio indirizzo
(`rounds.pool_address`, `m/84'/746'/0'/2/<round_id>`), e quello di riserva
(`m/84'/746'/0'/1/0`, anche il montepremi dei round aperti prima di B-80) si
ottiene con
`docker compose exec app python -c "from app.wallet.hd import derive_pool_address; print(derive_pool_address())"`;
se ne può controllare il saldo con un explorer. Dopo B-82 un'uscita di
scommessa scomparsa dalla catena esclude quel partecipante invece di
bloccare il pagamento; solo la scommessa scomparsa del vincitore lo blocca
([OPS-30]).
@@ -862,10 +865,10 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
Spendere UTXO del montepremi fuori dal sistema non è rilevato come spesa
esterna (l'indirizzo non è in `utxo_events`) e può far fallire il pagamento
di un round in corso (`insufficient pool UTXOs` o una trasmissione
rifiutata): farlo solo senza round in `paying_out`. Dopo B-80 ogni round ha
rifiutata): farlo solo senza round in `paying_out`. Ogni round ha
il proprio indirizzo `m/84'/746'/0'/2/<round_id>` e `m/84'/746'/0'/1/0`
diventa l'indirizzo di riserva; il pagamento di un round spende solo le
uscite delle sue scommesse.
è l'indirizzo di riserva; il pagamento di un round spende solo le monete del
suo indirizzo (dopo B-82, solo le uscite delle sue scommesse).
*Codice:* `app/wallet/hd.py:derive_pool_key`,
`app/rounds/scheduler.py:RoundScheduler._trigger_payout`
+2 -4
View File
@@ -178,10 +178,8 @@ riceve la riserva per l'RBF di ogni pagamento
registrate tra le monete degli utenti: il pagamento le legge dalla rete e
le verifica con il quorum
([ELC-27](connessione-electrum.md#5-il-quorum)).
*Codice:* `app/wallet/hd.py:derive_pool_key`, `app/wallet/hd.py:derive_pool_address` ·
*Test:* `tests/unit/test_pending_address.py`
*Stato:* da implementare (B-80) · *Oggi:* esiste un solo indirizzo del
montepremi, `m/84'/746'/0'/1/0`, per tutti i round.
*Codice:* `app/wallet/hd.py:derive_pool_key`, `app/wallet/hd.py:derive_pool_address`, `app/wallet/hd.py:derive_round_pool_key` ·
*Test:* `tests/unit/test_round_pool_address.py`, `tests/unit/test_pending_address.py`
## Deposito
+5 -9
View File
@@ -119,10 +119,9 @@ ancora `open` oltre la scadenza.
*Codice:* `app/wallet/psbt_builder.py:MAX_PARTICIPANTS_PER_ROUND`, `app/bets/service.py:place_bet` ·
*Test:* `tests/unit/test_payout_builder.py`, `tests/unit/test_bets.py`
Oggi il montepremi è un solo indirizzo per tutti i round, e la differenza fra
400 e 500 copre anche i resti accumulati dai pagamenti precedenti; con un
indirizzo per round
([RND-35](ciclo-di-vita-del-round.md#0--apertura), B-80) restano solo i
Con un indirizzo per round
([RND-35](ciclo-di-vita-del-round.md#0--apertura)) la differenza fra 400 e
500 non si consuma più con i resti dei pagamenti precedenti: restano solo i
pagamenti arrivati da fuori. Il numero di ingressi non è l'unica cosa che può
impedire un pagamento: vedi
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#4--pagamento).
@@ -162,10 +161,7 @@ flowchart TB
[RND-35](ciclo-di-vita-del-round.md#0--apertura)), e il resto (monete
spese meno il costo) verso l'indirizzo dell'utente.
*Codice:* `app/wallet/psbt_builder.py:select_utxos`, `app/wallet/psbt_builder.py:build_signed_transaction`, `app/bets/service.py:place_bet` ·
*Test:* `tests/unit/test_psbt_builder.py`
*Stato:* da implementare (B-80) · *Oggi:* tutte le scommesse di tutti i
round pagano lo stesso indirizzo (`derive_pool_address`,
`m/84'/746'/0'/1/0`).
*Test:* `tests/unit/test_psbt_builder.py`, `tests/unit/test_round_pool_address.py`
- **[BET-13]** La **commissione di rete** è la dimensione stimata della
transazione per `fee_rate_sat_vb` (1 sat/vB predefinito, modificabile da
`/admin`) ed è **dedotta dal costo** della scommessa: al montepremi arriva
@@ -271,7 +267,7 @@ HTTP 400 prima che si muova qualunque cifra:
|---|---|---|
| **[WDR-02]** | L'indirizzo di destinazione è un indirizzo PLM bech32: inizia con `plm1` ed è decodificabile. Un indirizzo di un'altra catena (ad esempio `bc1…`) o in formato legacy (`P…`) è rifiutato | `invalid_address` |
| **[WDR-03]** | L'indirizzo di destinazione non è l'indirizzo di deposito dell'utente stesso | `withdrawal_to_own_address` |
| **[WDR-14]** | L'indirizzo di destinazione non è un indirizzo della piattaforma: l'indirizzo del montepremi di un round qualunque (`rounds.pool_address`), l'indirizzo di riserva (`m/84'/746'/0'/1/0`, [RND-35](ciclo-di-vita-del-round.md#0--apertura)), il `fee_address` corrente di `round_config` o quello fotografato su un round. *Stato:* da implementare (B-80) · *Oggi:* nessun controllo; un prelievo verso il montepremi finisce fra le monete di un round | `withdrawal_to_platform_address` |
| **[WDR-14]** | L'indirizzo di destinazione non è un indirizzo della piattaforma: l'indirizzo del montepremi di un round qualunque (`rounds.pool_address`), l'indirizzo di riserva (`m/84'/746'/0'/1/0`, [RND-35](ciclo-di-vita-del-round.md#0--apertura)), il `fee_address` corrente di `round_config` o quello fotografato su un round. *Codice:* `app/withdrawals/service.py:request_withdrawal` · *Test:* `tests/unit/test_round_pool_address.py` | `withdrawal_to_platform_address` |
| **[WDR-04]** | L'importo è almeno pari al costo di una scommessa **corrente** in `round_config` (`bet_amount_sats`; non esiste un minimo separato, e non si usa il costo fotografato su un round) | `amount_below_minimum` (parametro `minimum_sats`) |
| **[WDR-05]** | La somma delle monete confermate e non spese copre l'importo | `balance_pending_confirmation` se lo coprirebbe il saldo comprensivo di quanto è in corso (parametro `pending_sats`), altrimenti `insufficient_balance` (parametro `required_sats`) |
| **[WDR-06]** | L'importo si copre con al massimo 50 monete (`MAX_TX_INPUTS`) | `too_many_inputs` (parametro `max_inputs`) |
+6 -10
View File
@@ -231,9 +231,8 @@ sì che un backup dei dati non contenga entrambe.
([REG-14](registrazione-e-depositi.md#il-conto-del-montepremi)) — stanno
nella stessa master key, non sono segreti separati. Non esiste un endpoint
che ne esporti la chiave privata.
*Codice:* `app/wallet/hd.py:derive_pool_key`, `app/wallet/hd.py:derive_pool_address`
*Stato:* da implementare (B-80), per le chiavi per round · *Oggi:* esiste
una sola chiave del montepremi, `m/84'/746'/0'/1/0`, per tutti i round.
*Codice:* `app/wallet/hd.py:derive_pool_key`, `app/wallet/hd.py:derive_pool_address`, `app/wallet/hd.py:derive_round_pool_key` ·
*Test:* `tests/unit/test_round_pool_address.py`
### Caricamento a runtime
@@ -747,13 +746,10 @@ di discrezione, non una protezione: la pagina è pubblica, gli endpoint no.
(`bc1…`) sarebbe altrimenti uno script valido su cui i fondi andrebbero
persi. Il prelievo verso il proprio indirizzo di deposito è rifiutato, e
così quello verso un indirizzo del montepremi, di riserva o delle
commissioni ([scommesse-e-prelievi.md](scommesse-e-prelievi.md#prelievo),
B-80).
commissioni ([scommesse-e-prelievi.md](scommesse-e-prelievi.md#prelievo)).
*Codice:* `app/wallet/address.py:is_valid_plm_address`,
`app/withdrawals/service.py:request_withdrawal` ·
*Test:* `tests/unit/test_withdrawals.py`, `tests/unit/test_qr.py`
*Stato:* da implementare (B-80), per gli indirizzi di montepremi, riserva
e commissioni · *Oggi:* è rifiutato solo il proprio indirizzo di deposito.
*Test:* `tests/unit/test_withdrawals.py`, `tests/unit/test_qr.py`, `tests/unit/test_round_pool_address.py`
- **[SEC-44]** `GET /qr/{address}` è pubblico (lo carica un `<img>`, che non
può portare il token) ma limitato: rifiuta con 400 `invalid_address` un
@@ -809,10 +805,10 @@ sicurezza.
lotteria in pausa ([OPS-27](operativita.md#ripristino), B-90), e ogni round
ha il proprio indirizzo del montepremi, le cui monete sono verificate con
il quorum prima del pagamento
([RND-35](ciclo-di-vita-del-round.md#0--apertura), B-80, B-82): un round
([RND-35](ciclo-di-vita-del-round.md#0--apertura), B-82): un round
del backup già pagato risulta impagabile invece di essere pagato due volte
con il denaro dei round successivi. *Oggi:* la pausa si imposta a mano nel
database e tutti i round condividono lo stesso indirizzo.
database, e le monete del round non sono ancora verificate con il quorum.
- **Chiave sbagliata.** Una master key che non corrisponde al database
ripristinato ferma l'avvio ([SEC-49]).
- **Commit persi.** Con `synchronous=FULL` un commit confermato al chiamante
+3 -3
View File
@@ -228,7 +228,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
| B-77 | Si ricorda solo l'ultimo txid di una transazione accelerata, e la sostituzione viene inviata prima di essere salvata | Critica | **aperto** | — | TX |
| B-78 | Una transazione viene abbandonata sulla base del silenzio di un solo server | Critica | **aperto** | — | TX, ELC |
| B-79 | Il pagamento di norma non ha resto, quindi non si può accelerare e può bloccare la lotteria | Critica | **aperto** | — | RND, TX |
| B-80 | Un solo indirizzo del montepremi per tutti i round | Critica | **aperto** | — | RND, BET, WDR |
| B-80 | Un solo indirizzo del montepremi per tutti i round | Critica | *da registrare* | `test_round_pool_address.py`, `test_pending_address.py`, `test_scheduler.py` | RND, BET, WDR |
| B-81 | `drawing` resta bloccato dopo qualsiasi eccezione, non solo dopo un riavvio | Critica | **aperto** | — | RND |
| B-82 | Un deposito annullato da una riorganizzazione dopo la scommessa rende il round impagabile; coinbase accreditate a 1 conferma | Critica | **aperto** | — | DEP, RND |
| B-83 | Conferme e reintegri si basano solo sul server attivo | Alta | **aperto** | — | ELC, TX, DEP |
@@ -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.
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80.
- **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 (9 critici,
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (8 critici,
8 alti, 6 medi), da implementare nell'ordine di
[piano-correzioni.md](piano-correzioni.md).
- **Prossimo numero libero:** `B-99`.
+3 -4
View File
@@ -182,10 +182,9 @@ monete:
per indirizzo a ogni giro; un indirizzo che non si riesce a leggere non
blocca gli altri.
*Codice:* `app/tx/confirmation.py:poll_once`, `app/tx/pending_address.py:own_address_for` ·
*Test:* `tests/unit/test_confirmation.py`
*Stato:* da implementare (B-77, B-80) · *Oggi:* cerca solo `current_txid`,
nella cronologia dell'unico indirizzo del montepremi condiviso da tutti i
round per i pagamenti; un'altezza maggiore di 0 riportata dal solo server
*Test:* `tests/unit/test_confirmation.py`, `tests/unit/test_round_pool_address.py`
*Stato:* da implementare (B-77) · *Oggi:* cerca solo `current_txid`,
nella cronologia dell'indirizzo del montepremi del round per i pagamenti; un'altezza maggiore di 0 riportata dal solo server
attivo basta a confermare ([TX-22]).
- **[TX-22]** **Conferma verificata.** Un candidato ([TX-06]) è confermato
solo quando il quorum dei server