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:
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
@@ -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 |
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user