Credit deposits at deposit_confirmations and verify the round's coins with the quorum before paying (B-82, DEP-03, DEP-04, DEP-09, DEP-12, DEP-14, ELC-17, ELC-27, RND-21, RND-22, RND-25, RND-39, DAT-10, DAT-11, DAT-24, DAT-62, DAT-67, DAT-68, AUD-12, AUD-44, AUD-45, API-30, API-31, API-36, ADR-06, ADR-52)
A user could bet and then double-spend the 1-conference deposit that funded the bet with a private chain: the bet left the chain but its participant stayed `confirmed`, and the payout failed "insufficient pool UTXOs" for ever with everyone else's money stuck. Coinbase outputs were also credited at 1 confirmation (maturity is 120). Deposits - `round_config.deposit_confirmations` (default 6, 1-100, editable from /admin) is the threshold for a deposit; an output of a platform transaction (any txid of a pending_transactions row, RBF versions included) keeps 1; a coinbase output needs 120, recognised from the raw transaction, which must hash to the txid (`is_coinbase`, cached). `read_deposit_policy` / `classify_new_deposits` run between the read and the write, so no session is open across the lookup. - The quorum must also place the coin deep enough on our own tip (ELC-17). - Confirmed outputs short of their threshold are pending deposits (memory only, replaced at every refresh): shown in `pending_balance_sats` / `has_pending` (also for DEP-13's withdrawal message), never spendable. No scripthash notification announces a maturing deposit, so each new block refreshes only the users that have one, one task per user (DEP-09 now says so). Payout - `_trigger_payout` verifies the round's confirmed participants' bet outputs with the other servers (`verify_pool_outpoints`: strict majority of >= 2 answers per outpoint, a mempool-only coin or a tie gives no verdict) and builds from exactly those outpoints (RND-22), never from whatever else sits at the address. No quorum: `payout_failed`, `pool verification unavailable`, nothing changes. Bets that are gone become `reorged_out`, one `participant_reorged_out` row each, `pool_amount_sats` rewritten (only downward) in the same transaction, and the 70/30 split is computed on what is left. - Winner's own bet gone: nothing is built; `rounds.payout_blocked_at` (Alembic f2c64a8d1e93, with `deposit_confirmations`, plain add_column) is set, `payout_blocked` and the pause with `paused_reason = 'payout_blocked'` are written in one transaction; `POST /admin/resume` clears the mark and the next attempt verifies again. /admin/rounds returns it. - Rounds opened before B-80 (pool = the shared reserve address) keep the old active-server `listunspent` choice, since that address holds no single round's bets. Ambiguities resolved: the winner's row stays `confirmed` while held (AUD-45 says every participant whose bet is gone becomes `reorged_out`, but OPS-30/DAT-68 need the verification to repeat after the resume, which only a `confirmed` row allows); `/`'s deposit hint no longer says "1 confirmation" (7 languages, same keys); guides updated. Tests changed because the spec changed: listener tests that credited height-100 coins at tip 0 now set a tip past every threshold; the payout fixtures seed confirmed participants (the new source of the inputs) instead of an active-server `listunspent`, FakeNetwork answers the verification, and test_round_pool_address asserts the quorum is asked about the round's address. New: thresholds at exactly 6/5, configured value, coinbase at 119/120 and a mismatching or missing raw tx, platform outputs at 1, dust, quorum height, waiting-deposit display and disappearance, block-triggered refresh, resume, verification verdicts, gone bets excluded with the split recomputed, winner gone held and paused, held payouts untouched until the resume, re-verification after it, no quorum, a payout with 1 and with 400 participants against reserve+fee+dust, admin config bounds and migration. Mutation-checked: threshold 1, ignored coinbase, no height bound, no verification and no winner hold each fail tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
daa85cf4f0
commit
54dbffb552
42 files changed
+1360
-270
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# ADR-06 — I depositi richiedono `deposit_confirmations` conferme (6 per default, configurabile in `round_config`), le uscite coinbase 120; ogni altra transazione ne richiede 1
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-82)
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Conferme e fiducia nella rete
|
||||
- **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*.
|
||||
|
||||
@@ -34,7 +34,5 @@ decisione esplicita.
|
||||
è contenuto da [ADR-52](ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md) e accettato in [LIM-24]. Regole in
|
||||
[registrazione-e-depositi.md](../registrazione-e-depositi.md#deposito) (B-82).
|
||||
|
||||
*Codice:* `app/tx/confirmation.py:poll_once` · `app/deposits/service.py:find_new_credit_candidates` ·
|
||||
*Test:* `tests/unit/test_confirmation.py::test_unconfirmed_tx_is_left_pending`, `tests/unit/test_deposits.py::test_unconfirmed_entry_is_ignored`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* ogni moneta, deposito o coinbase
|
||||
compresi, si accredita a 1 conferma.
|
||||
*Codice:* `app/tx/confirmation.py:poll_once` · `app/deposits/service.py:find_new_credit_candidates` · `app/deposits/service.py:classify_new_deposits` ·
|
||||
*Test:* `tests/unit/test_confirmation.py::test_unconfirmed_tx_is_left_pending`, `tests/unit/test_deposits.py::test_unconfirmed_entry_is_ignored`, `tests/unit/test_deposit_confirmations.py::test_a_deposit_is_credited_exactly_at_six_confirmations`, `tests/unit/test_deposit_confirmations.py::test_an_immature_coinbase_is_not_credited_until_it_has_120_confirmations`, `tests/unit/test_deposit_confirmations.py::test_an_output_of_a_platform_transaction_is_credited_at_one_confirmation`
|
||||
@@ -1,6 +1,6 @@
|
||||
# ADR-52 — Le riorganizzazioni della catena si contengono invece di ignorarle
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-82)
|
||||
- **Stato:** Accettata
|
||||
- **Area:** Conferme e fiducia nella rete
|
||||
- **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*.
|
||||
|
||||
@@ -43,6 +43,4 @@ e [ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#4--pagamento)
|
||||
([LIM-24]).
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._record_reorg` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/rounds/service.py:open_new_round_if_needed` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* il pagamento non verifica le monete
|
||||
del round: le prime due misure (rilevare, fermare) sono attive.
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_pool_verification.py`
|
||||
+6
-11
@@ -577,7 +577,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-82, B-95), per i campi marcati · *Oggi:*
|
||||
*Stato:* da implementare (B-95), per i campi marcati · *Oggi:*
|
||||
`bet_amount_sats` è sempre il costo configurato.
|
||||
|
||||
#### GET /rounds/stream
|
||||
@@ -760,8 +760,7 @@ 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-82, B-95) · *Oggi:* mancano
|
||||
`deposit_confirmations` e `max_round_pool_sats`.
|
||||
*Stato:* da implementare (B-95) · *Oggi:* manca `max_round_pool_sats`.
|
||||
|
||||
#### PUT /admin/config
|
||||
|
||||
@@ -778,7 +777,7 @@ dei parametri operativi in
|
||||
| `fee_rate_sat_vb` | 1 – 10 000 (`MAX_FEE_RATE_SAT_VB`) |
|
||||
| `rbf_timeout_seconds` | 60 – 604 800 |
|
||||
| `draw_animation_seconds` | 0 – 600 |
|
||||
| `deposit_confirmations` | 1 – 100 — *da implementare (B-82)* |
|
||||
| `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)* |
|
||||
|
||||
@@ -803,9 +802,9 @@ dei parametri operativi in
|
||||
*Codice:* `app/api/routes/admin.py:update_config`,
|
||||
`app/api/routes/admin.py:RoundConfigUpdate` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-82, B-95), per i due campi nuovi e la copia
|
||||
*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
|
||||
otto campi.
|
||||
nove campi.
|
||||
|
||||
#### POST /admin/pause e POST /admin/resume
|
||||
|
||||
@@ -834,9 +833,6 @@ dei parametri operativi in
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery`,
|
||||
`app/api/routes/admin.py:resume_lottery` ·
|
||||
*Test:* `tests/unit/test_admin.py`, `tests/unit/test_pause_reason.py`
|
||||
*Stato:* da implementare (B-82), per l'azzeramento di `payout_blocked_at` ·
|
||||
*Oggi:* `rounds.payout_blocked_at` non esiste ancora; le pause automatiche
|
||||
esistenti sono solo `resurrected_tx`.
|
||||
|
||||
#### GET /admin/users
|
||||
|
||||
@@ -893,8 +889,7 @@ 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-82), per `payout_blocked_at` · *Oggi:*
|
||||
la lista si ferma a `bump_reserve_sats`.
|
||||
|
||||
|
||||
#### GET /admin/audit-log
|
||||
|
||||
|
||||
@@ -616,8 +616,7 @@ 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-82, B-95) · *Oggi:* `deposit_confirmations` e
|
||||
`max_round_pool_sats` non esistono.
|
||||
*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
|
||||
@@ -626,8 +625,6 @@ La configurazione è divisa in due posti con regole diverse.
|
||||
dei depositi sono invece configurabili ([ARC-31]).
|
||||
*Codice:* `app/rounds/service.py:winner_share` ·
|
||||
`app/wallet/psbt_builder.py` · `app/tx/confirmation.py`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* 1 conferma per ogni moneta e
|
||||
transazione, depositi e coinbase compresi.
|
||||
|
||||
## Aggiornamenti in tempo reale (SSE)
|
||||
|
||||
|
||||
+12
-18
@@ -187,8 +187,8 @@ flowchart LR
|
||||
riga. `user_id` no, `round_id` no. Stessa transazione della modifica.
|
||||
*Codice:* `app/api/routes/admin.py:update_config` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-82, B-95), per gli ultimi due campi ·
|
||||
*Oggi:* i campi sono i primi otto.
|
||||
*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
|
||||
@@ -216,10 +216,10 @@ flowchart LR
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout`,
|
||||
`app/tx/reconcile.py:reconcile_once` ·
|
||||
*Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82, B-90) · *Oggi:* scritto da
|
||||
`POST /admin/pause` (`operator`), dal riconciliatore (`resurrected_tx`) e dal
|
||||
listener (`reorg`); `payout_blocked` e l'avvio con `START_PAUSED` non lo
|
||||
scrivono ancora.
|
||||
*Stato:* da implementare (B-90) · *Oggi:* scritto da
|
||||
`POST /admin/pause` (`operator`), dal riconciliatore (`resurrected_tx`), dal
|
||||
listener (`reorg`) e dallo scheduler (`payout_blocked`); l'avvio con
|
||||
`START_PAUSED` non lo scrive ancora.
|
||||
|
||||
- **[AUD-12]** `lottery_resumed` — `POST /admin/resume`. Payload
|
||||
`{"reason"}`: il `paused_reason` che la ripresa ha tolto (`null` se la
|
||||
@@ -229,9 +229,7 @@ flowchart LR
|
||||
`paused = false`, `paused_reason = NULL` e dell'azzeramento di
|
||||
`payout_blocked_at`.
|
||||
*Codice:* `app/api/routes/admin.py:resume_lottery` ·
|
||||
*Test:* `tests/unit/test_pause_reason.py`
|
||||
*Stato:* da implementare (B-82), per l'azzeramento di `payout_blocked_at` ·
|
||||
*Oggi:* `rounds.payout_blocked_at` non esiste ancora.
|
||||
*Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_admin.py::test_resume_releases_a_payout_held_because_the_winners_bet_left_the_chain`
|
||||
|
||||
- **[AUD-13]** `admin_privkey_accessed` — `GET /admin/users/{id}/privkey` ha
|
||||
esportato la chiave privata (WIF) di un utente. Payload `{"user_id"}`.
|
||||
@@ -491,7 +489,7 @@ flowchart LR
|
||||
scrive invece `payout_blocked` ([AUD-44]). L'elenco dei motivi è di
|
||||
[RND-25](ciclo-di-vita-del-round.md#4--pagamento); la verifica senza
|
||||
quorum di [RND-39](ciclo-di-vita-del-round.md#4--pagamento) termina anche
|
||||
lei con `payout_failed`, motivo `pool verification unavailable` (B-82). Cosa fare per ogni motivo è descritto in
|
||||
lei con `payout_failed`, motivo `pool verification unavailable`. Cosa fare per ogni motivo è descritto in
|
||||
[operativita.md](operativita.md).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._log_payout_failure` ·
|
||||
@@ -515,10 +513,8 @@ flowchart LR
|
||||
riscritto dopo l'esclusione. `user_id` sì (il partecipante), `round_id` sì.
|
||||
Stessa transazione del passaggio a `reorged_out` e della riscrittura di
|
||||
`pool_amount_sats`. Nessuna logica rilegge la riga.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-82) · *Oggi:* nessun partecipante viene mai
|
||||
escluso; il pagamento fallisce con `insufficient pool UTXOs`.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout`, `app/rounds/scheduler.py:RoundScheduler._verify_round_pool` ·
|
||||
*Test:* `tests/unit/test_pool_verification.py::test_a_reorged_out_bet_is_excluded_and_the_split_is_computed_on_what_is_there`, `tests/unit/test_pool_verification.py::test_several_bets_gone_at_once_each_get_a_row_in_one_transaction`
|
||||
|
||||
- **[AUD-44]** `payout_blocked` — la verifica prima del pagamento ha trovato
|
||||
assente la scommessa **del vincitore**: il pagamento non si costruisce, il
|
||||
@@ -532,10 +528,8 @@ flowchart LR
|
||||
verifica si ripete: se la scommessa è ancora assente si scrive una nuova
|
||||
riga. Non è un `payout_failed` e non fa da orologio dei tentativi.
|
||||
Procedura in [operativita.md](operativita.md) (OPS-30).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-82) · *Oggi:* il pagamento fallisce con
|
||||
`insufficient pool UTXOs` a ogni tentativo, senza pausa.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout`, `app/rounds/scheduler.py:RoundScheduler._verify_round_pool` ·
|
||||
*Test:* `tests/unit/test_pool_verification.py::test_the_winners_own_bet_gone_holds_the_payout_and_pauses_the_lottery`, `tests/unit/test_pool_verification.py::test_after_the_resume_the_verification_repeats`
|
||||
|
||||
### Riconciliazione
|
||||
|
||||
|
||||
@@ -410,11 +410,7 @@ del montepremi del round.
|
||||
([RND-38]). La ripartizione è fissata nel codice, non modificabile da
|
||||
`/admin`.
|
||||
*Codice:* `app/rounds/service.py:winner_share`, `app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* `tests/unit/test_payout_builder.py`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* la ripartizione si calcola
|
||||
su `pool_amount_sats` fissato all'estrazione, senza la verifica di
|
||||
[RND-39]; la riserva per l'RBF è già tolta dalla quota delle commissioni
|
||||
(B-79).
|
||||
*Test:* `tests/unit/test_payout_builder.py`, `tests/unit/test_pool_verification.py::test_a_reorged_out_bet_is_excluded_and_the_split_is_computed_on_what_is_there`
|
||||
- **[RND-22]** Il pagamento spende le uscite delle scommesse dei partecipanti
|
||||
del round **verificate** ([RND-39]), all'indirizzo del montepremi del round
|
||||
([RND-35]), e nient'altro: altre monete arrivate a quell'indirizzo non
|
||||
@@ -427,12 +423,7 @@ 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`, `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.
|
||||
*Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_payout_builder.py`, `tests/unit/test_round_pool_address.py`, `tests/unit/test_pool_verification.py::test_the_payout_spends_exactly_the_verified_bet_outputs_and_nothing_else`
|
||||
- **[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
|
||||
@@ -469,7 +460,7 @@ del montepremi del round.
|
||||
`insufficient pool UTXOs`, il codice dell'errore di costruzione
|
||||
(`too_many_inputs`, `winner_share_below_network_fee`,
|
||||
`commission_below_dust_limit`), `payout build failed` per ogni altro errore
|
||||
di costruzione, `broadcast rejected`, e — *da implementare (B-82)* —
|
||||
di costruzione, `broadcast rejected`, e
|
||||
`pool verification unavailable` quando la verifica del montepremi prima del
|
||||
pagamento ([RND-39]) non ottiene abbastanza risposte dal quorum. Fanno eccezione due casi, che non
|
||||
scrivono `payout_failed`: un pagamento già in volo per il round ([RND-26]),
|
||||
@@ -524,18 +515,17 @@ del montepremi del round.
|
||||
`paying_out` senza altri tentativi finché l'operatore non toglie la pausa
|
||||
(`POST /admin/resume`); al tentativo successivo la verifica si ripete. Lo
|
||||
schema è in [modello-dati.md](modello-dati.md) (B-82), la procedura in
|
||||
[operativita.md](operativita.md) (B-82).
|
||||
[operativita.md](operativita.md) (B-82). La riga del vincitore **non**
|
||||
passa a `reorged_out` in questo caso: resta `confirmed`, così che la
|
||||
verifica dopo la ripresa la riesamini davvero; i partecipanti diversi dal
|
||||
vincitore la cui scommessa è assente sono invece esclusi nella stessa
|
||||
transazione.
|
||||
- Se il quorum non risponde o non concorda, non si conclude nulla: nessun
|
||||
partecipante cambia stato, il tentativo termina con `payout_failed` e si
|
||||
riprova secondo [RND-25].
|
||||
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-82) · *Oggi:* le monete sono lette dal solo
|
||||
server attivo e nessun partecipante viene mai escluso: se una scommessa
|
||||
sparisce dalla catena il pagamento fallisce con
|
||||
`insufficient pool UTXOs` a ogni tentativo, con i soldi di tutti gli altri
|
||||
bloccati.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout`, `app/rounds/scheduler.py:RoundScheduler._verify_round_pool`, `app/electrum/listener.py:ElectrumListener.verify_pool_outpoints` ·
|
||||
*Test:* `tests/unit/test_pool_verification.py`
|
||||
- **[RND-40]** **Configurazione pagabile**: `rounds_can_open` rifiuta una
|
||||
configurazione in cui un round non si potrebbe pagare, verificandola per
|
||||
**n = 1** e per **n = `MAX_PARTICIPANTS_PER_ROUND`** partecipanti (i due
|
||||
|
||||
@@ -253,12 +253,10 @@ Ogni decisione che sposta denaro in base a una risposta della rete viene
|
||||
confermata dagli **altri** server configurati, mai dal solo server attivo
|
||||
(che è proprio quello che un attaccante controllerebbe).
|
||||
|
||||
Oggi non è ancora così per una sola decisione: la lettura delle monete del
|
||||
montepremi per il pagamento
|
||||
(`app/rounds/scheduler.py:RoundScheduler._trigger_payout`) si basa sul solo
|
||||
server attivo, ed è la riga [ELC-27]. Le conferme delle transazioni
|
||||
([ELC-26]), il reintegro di una moneta ricomparsa ([ELC-25]), l'abbandono
|
||||
([ELC-28]) e l'invio ([TX-04], [TX-21]) usano già gli altri server.
|
||||
Vale per ogni decisione di questo tipo: le monete del montepremi per il
|
||||
pagamento ([ELC-27]), le conferme delle transazioni ([ELC-26]), il reintegro
|
||||
di una moneta ricomparsa ([ELC-25]), l'abbandono ([ELC-28]) e l'invio
|
||||
([TX-04], [TX-21]) usano gli altri server.
|
||||
|
||||
- **[ELC-14]** La corroborazione interroga in parallelo tutti i server
|
||||
configurati tranne quello attivo, ciascuno con una connessione nuova e un
|
||||
@@ -286,17 +284,11 @@ server attivo, ed è la riga [ELC-27]. Le conferme delle transazioni
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener.agreed_block_hash`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_spent`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_credit`,
|
||||
`app/electrum/listener.py:ElectrumListener.verify_pool_outpoints`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_reinstatement`,
|
||||
`app/electrum/listener.py:ElectrumListener.confirmation_proven`,
|
||||
`app/electrum/listener.py:ElectrumListener._corroborate_majority` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_confirmation_proof.py`
|
||||
|
||||
Stato delle righe della tabella:
|
||||
|
||||
- [ELC-17] — *Stato:* da implementare (B-82) · *Oggi:* basta un'altezza
|
||||
maggiore di 0 (1 conferma).
|
||||
- [ELC-27] — *Stato:* da implementare (B-82) · *Oggi:* il pagamento sceglie
|
||||
fra le monete che il server attivo elenca per il montepremi.
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_confirmation_proof.py`, `tests/unit/test_deposit_confirmations.py`, `tests/unit/test_pool_verification.py`
|
||||
|
||||
I saldi si muovono in entrambe le direzioni, quindi entrambe richiedono lo
|
||||
stesso quorum. Le conseguenze di una verifica non riuscita:
|
||||
@@ -311,7 +303,15 @@ stesso quorum. Le conseguenze di una verifica non riuscita:
|
||||
al controllo successivo;
|
||||
- un blocco di estrazione non verificato fa attendere e richiedere la stessa
|
||||
altezza, e lo si registra in audit (`draw_header_corroboration_failed`,
|
||||
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#3--estrazione)).
|
||||
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#3--estrazione));
|
||||
- le monete del montepremi non verificate fanno terminare il tentativo di
|
||||
pagamento con `payout_failed` (`pool verification unavailable`), senza
|
||||
escludere nessun partecipante ([RND-39](ciclo-di-vita-del-round.md#4--pagamento)).
|
||||
La verifica ([ELC-27]) chiede a ogni altro server l'elenco delle monete
|
||||
dell'indirizzo del round: per ogni uscita di scommessa una maggioranza
|
||||
stretta di chi risponde dice *presente* (stesso importo, confermata) o
|
||||
*assente* (non elencata, o con un altro importo); un'uscita elencata solo
|
||||
nel mempool, o senza maggioranza, non dà alcun verdetto e si riprova.
|
||||
|
||||
L'**invio** di una transazione non è una corroborazione ma usa gli stessi
|
||||
server: dopo che il server attivo l'ha accettata, la transazione grezza viene
|
||||
|
||||
@@ -52,6 +52,7 @@ business — quelli si toccano solo da qui.
|
||||
| **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. |
|
||||
| **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. |
|
||||
| **Conferme sul blocco di estrazione** | Da 1 a 20 (predefinito 3): quanti blocchi deve avere la catena a partire dal blocco di estrazione prima che il suo hash scelga il vincitore. Più conferme rendono l'estrazione più robusta a una riorganizzazione della catena e allungano l'attesa di circa 2 minuti per blocco. Si applica dal round successivo. |
|
||||
| **Fee rate di rete (sat/vB)** | Fee per byte usata per costruire bet, payout e prelievi. |
|
||||
| **Timeout prima del fee-bump RBF (secondi)** | Dopo quanto tempo senza conferma una transazione viene ritrasmessa con fee più alta. |
|
||||
|
||||
@@ -102,15 +102,20 @@ L'avviso sparisce da solo appena l'operatore riprende la lotteria.
|
||||
confermata — così non sembra che il saldo sia crollato più del dovuto
|
||||
subito dopo un'operazione. Il colore indica lo stato:
|
||||
- **verde**: tutto confermato, il saldo mostrato è quello definitivo
|
||||
- **arancione**: c'è una bet o un prelievo ancora in attesa di conferma —
|
||||
il numero è corretto, ma non ancora "finale"
|
||||
- **arancione**: c'è una bet o un prelievo ancora in attesa di conferma,
|
||||
oppure un deposito che ha qualche conferma ma non ancora quelle richieste —
|
||||
il numero è corretto, ma non ancora "finale"; un deposito in attesa non è
|
||||
spendibile finché non viene accreditato
|
||||
- Il tuo **indirizzo di deposito**, con bottone per copiarlo negli appunti
|
||||
- Il **QR code** dello stesso indirizzo, comodo per inviare PLM da un altro
|
||||
wallet scansionandolo invece di copiare l'indirizzo a mano
|
||||
|
||||
Per depositare, invia PLM (mainnet reale) a quell'indirizzo da un wallet
|
||||
esterno. Il saldo si aggiorna da solo dopo la prima conferma (e quasi subito,
|
||||
grazie all'aggiornamento in tempo reale); premi "Aggiorna" se vuoi comunque
|
||||
esterno. Un deposito viene accreditato (e diventa spendibile) dopo
|
||||
**6 conferme** sulla rete (il valore è configurabile dall'operatore; un'uscita
|
||||
di mining ne richiede 120), cioè circa 12 minuti; nel frattempo compare in
|
||||
arancione come in attesa. Il saldo si aggiorna da solo, con l'aggiornamento in
|
||||
tempo reale; premi "Aggiorna" se vuoi comunque
|
||||
ricontrollarlo a mano.
|
||||
|
||||
### Bet
|
||||
|
||||
+10
-46
@@ -36,8 +36,8 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
| [LIM-02] RBF di una sola forma (e pagamento senza resto) | **Sì, in caso di congestione della rete** | Chiuso da B-79 e B-87 |
|
||||
| [LIM-03] Attesa in `closing` senza limite di tempo | **Sì, in caso di congestione** | Resta senza limite di tempo per scelta ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)); con B-78 e B-87 finisce quando la rete decide |
|
||||
| [LIM-04] Prelievo e RBF mai provati in rete reale | **Sì, come verifica** | Da chiudere con la prova su rete reale ([operativita.md](operativita.md#prova-su-rete-reale)) prima di aprire al pubblico |
|
||||
| [LIM-10] Riorganizzazioni non gestite | **Sì** — una scommessa sparita rende il round impagabile | Chiuso da B-82 e B-85 (la pausa e il registro sono attivi; resta il pagamento, B-82); resta [LIM-24] |
|
||||
| [LIM-11], [LIM-12] Quorum facoltativo e parziale | **Sì** — un solo server decide conferme e abbandoni | Chiusi da B-83 e B-84 (resta la lettura delle monete del montepremi, B-82) |
|
||||
| ~~[LIM-10]~~ Riorganizzazioni non gestite | Non più (chiuso da B-82 e B-85) | Resta [LIM-24] |
|
||||
| [LIM-11], ~~[LIM-12]~~ Quorum facoltativo e parziale | Non più (chiusi da B-82, B-83 e B-84) | Resta la scelta dell'operatore di server indipendenti ([LIM-11]) |
|
||||
| [LIM-13] Estrazione manipolabile | **Sì** — per un giocatore che mina | Chiuso da B-73; resta [LIM-25] |
|
||||
| [LIM-14] Header senza difficoltà minima | **Sì** — un header falso congela ogni estrazione | Chiuso da B-74 per il blocco; resta il costo basso di un header a difficoltà minima |
|
||||
| [LIM-06] Token admin unico | No per il funzionamento, ma è il rischio di sicurezza più ampio | Invariato |
|
||||
@@ -211,30 +211,11 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
|
||||
### Fiducia nella rete
|
||||
|
||||
- **[LIM-10]** **Riorganizzazioni non gestite, depositi a 1 conferma.** Un
|
||||
deposito accreditato a 1 conferma può sparire con una riorganizzazione; la
|
||||
punta della catena non regredisce mai, un header concorrente alla stessa
|
||||
altezza è ignorato e nessuna riorganizzazione viene rilevata.
|
||||
- *Impatto:* il caso grave è una scommessa **già confermata** che sparisce:
|
||||
un utente scommette con un deposito a 1 conferma e, se perde, sostituisce
|
||||
il deposito con una catena privata. La scommessa non è più in catena, ma il
|
||||
partecipante resta `confirmed` e contato, e il pagamento fallisce per
|
||||
sempre con «monete del montepremi insufficienti», con il denaro di tutti
|
||||
gli altri giocatori bloccato. Anche le uscite coinbase sono accreditate a 1
|
||||
conferma, mentre maturano a 120.
|
||||
- *Mitigazione attuale:* una moneta che sparisce dall'elenco del server viene,
|
||||
previo quorum, segnata come spesa all'esterno e tolta dal saldo; se
|
||||
riappare viene ripristinata. Una scommessa **non ancora inviata** costruita
|
||||
su una moneta scomparsa è rifiutata dalla rete e annullata; per una
|
||||
scommessa già confermata non esiste alcuna mitigazione (la versione
|
||||
precedente di questa voce diceva il contrario).
|
||||
- *Stato:* da chiudere con B-82; le riorganizzazioni sono già rilevate, registrate e (se profonde) in pausa (B-85).
|
||||
- *Per chiuderlo:* depositi a `deposit_confirmations` conferme e coinbase a
|
||||
120 ([ADR-06](adr/ADR-06-depositi-richiedono-deposit-confirmations-conferme-6.md)); riorganizzazioni rilevate, registrate e, se profonde,
|
||||
con pausa; monete del round verificate prima del pagamento, con i
|
||||
partecipanti spariti esclusi ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)). Resta accettato il 51% ([LIM-24]).
|
||||
|
||||
*Codice:* `app/deposits/service.py:find_utxos_missing_from` · `app/deposits/service.py:reinstate_reappeared_utxos` · `app/electrum/listener.py:ElectrumListener._apply_header`
|
||||
- ~~[LIM-10]~~ Rimosso: chiuso da B-82 e B-85. I depositi si accreditano a
|
||||
`deposit_confirmations` conferme (le coinbase a 120), le riorganizzazioni sono
|
||||
seguite, registrate e, da profondità 2, mettono in pausa; le monete del round
|
||||
si verificano con il quorum prima del pagamento e i partecipanti spariti
|
||||
restano fuori ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)). Resta accettato il 51% ([LIM-24]).
|
||||
|
||||
- **[LIM-11]** **I server del quorum li sceglie l'operatore.** Il server
|
||||
rifiuta di partire con meno di 2 server di riserva e ogni corroborazione
|
||||
@@ -253,26 +234,9 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority`
|
||||
|
||||
- **[LIM-12]** **Non tutte le letture di rete sono corroborate.** Passano dal
|
||||
solo server attivo: l'elenco delle monete del montepremi usato per il
|
||||
pagamento.
|
||||
- *Impatto:* un server ostile potrebbe far scegliere al pagamento monete che
|
||||
non esistono (il pagamento verrebbe rifiutato dalla rete) o nasconderne
|
||||
di vere (il pagamento fallirebbe e verrebbe ritentato).
|
||||
- *Mitigazione attuale:* l'abbandono richiede una prova ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)) e l'invio
|
||||
arriva a tutti i server; un invio basato su monete inesistenti è rifiutato
|
||||
dalla rete.
|
||||
- *Stato:* da chiudere con B-82 (monete del round prima del pagamento);
|
||||
conferme e ripristini sono chiusi da B-83.
|
||||
- *Per chiuderlo:* una conferma vale quando il quorum restituisce una prova
|
||||
merkle valida (`blockchain.transaction.get_merkle`) contro un header che
|
||||
possediamo; il ripristino richiede lo stesso quorum di un accredito;
|
||||
l'abbandono richiede la prova di [ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md); le monete del round si
|
||||
verificano con il quorum prima di costruire il pagamento ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md)); ogni
|
||||
transazione accettata dal server attivo viene inviata anche a tutti i
|
||||
server di riserva.
|
||||
|
||||
*Codice:* `app/tx/confirmation.py:poll_once` · `app/tx/reconcile.py:reconcile_once` · `app/deposits/service.py:reinstate_reappeared_utxos` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout`
|
||||
- ~~[LIM-12]~~ Rimosso: chiuso da B-83 e B-82. Conferme (prova merkle),
|
||||
reintegri, abbandoni e le monete del montepremi usate per il pagamento sono
|
||||
tutti decisi dal quorum degli altri server, mai dal solo server attivo.
|
||||
|
||||
- **[LIM-13]** **L'estrazione attuale è manipolabile da chi produce o
|
||||
trasmette i blocchi.**
|
||||
|
||||
+28
-40
@@ -345,9 +345,7 @@ del database sono tutti uguali, perché arrivano tutti dallo stesso percorso
|
||||
*Codice:* `app/deposits/service.py:find_new_credit_candidates` ·
|
||||
`app/deposits/service.py:credit_confirmed_utxos` ·
|
||||
`app/electrum/listener.py:ElectrumListener.refresh_user` ·
|
||||
*Test:* `tests/unit/test_deposits.py`, `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* si accredita ogni output
|
||||
con `height > 0` (1 conferma), coinbase comprese.
|
||||
*Test:* `tests/unit/test_deposits.py`, `tests/unit/test_electrum_listener.py`, `tests/unit/test_deposit_confirmations.py`
|
||||
|
||||
- **[DAT-11]** Gli UTXO dei montepremi e della riserva **non** sono in
|
||||
`utxo_events`: non appartengono a un utente. Il pagamento di un round spende
|
||||
@@ -360,11 +358,10 @@ del database sono tutti uguali, perché arrivano tutti dallo stesso percorso
|
||||
lo stesso motivo l'abbandono di un pagamento non ha input da liberare.
|
||||
*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-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.
|
||||
`app/tx/reconcile.py:_abandon` · *Test:* `tests/unit/test_scheduler.py`, `tests/unit/test_pool_verification.py::test_the_payout_spends_exactly_the_verified_bet_outputs_and_nothing_else`
|
||||
Un round aperto prima dell'indirizzo per round (il cui montepremi è
|
||||
l'indirizzo di riserva condiviso) conserva la scelta di prima: le monete
|
||||
confermate dell'indirizzo, lette dal server attivo.
|
||||
|
||||
- **[DAT-12]** `spent_txid` ha tre significati:
|
||||
- `NULL`: l'output è spendibile e conta nel saldo;
|
||||
@@ -443,12 +440,12 @@ restano nella tabella come storico.
|
||||
| `draw_block_hash` | String(64) | sì | `NULL` | Hash (esadecimale) di quel blocco | `_complete_draw` |
|
||||
| `seed_int` | String(128) | sì | `NULL` | Il seme in decimale (fino a 78 cifre, per questo è una stringa). Il valore intero di `sha256(server_seed ‖ block_hash ‖ participants_hash)`, con la codifica definita in [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) (RND-17) | `_complete_draw` |
|
||||
| `winner_user_id` | FK `users.id` | sì | `NULL` | Il vincitore | `_complete_draw` |
|
||||
| `pool_amount_sats` | BigInteger | sì | `NULL` | Montepremi: somma delle scommesse confermate e ancora sulla catena ([DAT-21], [DAT-67]) | `_close_and_draw`, `_complete_draw`; `RoundScheduler._trigger_payout` (atteso, B-82) |
|
||||
| `pool_amount_sats` | BigInteger | sì | `NULL` | Montepremi: somma delle scommesse confermate e ancora sulla catena ([DAT-21], [DAT-67]) | `_close_and_draw`, `_complete_draw`; `RoundScheduler._trigger_payout` (la ridefinisce la verifica, solo al ribasso) |
|
||||
| `winner_amount_sats` | BigInteger | sì | `NULL` | Quanto riceve il vincitore (70% meno la commissione di rete) | `RoundScheduler._trigger_payout` |
|
||||
| `fee_amount_sats` | BigInteger | sì | `NULL` | Quanto va a `fee_address`: 30% più il resto dell'arrotondamento, meno `bump_reserve_sats` nello schema atteso | `_trigger_payout` |
|
||||
| `fee_amount_sats` | BigInteger | sì | `NULL` | Quanto va a `fee_address`: 30% più il resto dell'arrotondamento, meno `bump_reserve_sats` | `_trigger_payout` |
|
||||
| `bump_reserve_sats` | BigInteger | sì | `NULL` | Output di riserva per l'RBF del pagamento, verso l'indirizzo di riserva ([DAT-73]) | `_trigger_payout`; `app/tx/broadcast.py:bump_fee` |
|
||||
| `payout_txid` | String(64) | sì | `NULL` | Txid corrente della transazione di pagamento | `_trigger_payout`; `_retarget_txid_references`; `app/tx/reconcile.py:_abandon` |
|
||||
| `payout_blocked_at` | DateTime | sì | `NULL` | Il pagamento è trattenuto perché la scommessa del vincitore non è più sulla catena ([DAT-68]) — *da implementare (B-82)* | `_trigger_payout`; `app/api/routes/admin.py:resume_lottery` |
|
||||
| `payout_blocked_at` | DateTime | sì | `NULL` | Il pagamento è trattenuto perché la scommessa del vincitore non è più sulla catena ([DAT-68]) | `_trigger_payout`; `app/api/routes/admin.py:resume_lottery` |
|
||||
|
||||
- ~~**[DAT-15]**~~ Assorbito da
|
||||
[RND-02](ciclo-di-vita-del-round.md): i valori di `status` e le transizioni
|
||||
@@ -622,10 +619,8 @@ restano nella tabella come storico.
|
||||
database**. Il valore scritto è sempre minore o uguale al precedente.
|
||||
La regola di dominio (quorum, ripartizione su ciò che c'è) è in
|
||||
[ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md) (B-82).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · *Test:* —
|
||||
*Stato:* da implementare (B-82) · *Oggi:* `pool_amount_sats` non cambia dopo
|
||||
l'estrazione, e un pagamento che non trova tutte le scommesse fallisce con
|
||||
"insufficient pool UTXOs" a ogni tentativo.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._verify_round_pool` ·
|
||||
*Test:* `tests/unit/test_pool_verification.py`
|
||||
|
||||
- **[DAT-68]** `payout_blocked_at` si scrive quando la verifica di [DAT-67]
|
||||
trova assente la scommessa del **vincitore**: il round resta `paying_out`,
|
||||
@@ -634,8 +629,8 @@ restano nella tabella come storico.
|
||||
([DAT-69]). Torna `NULL` solo con `POST /admin/resume` ([api.md](api.md)),
|
||||
dopo di che la verifica si ripete al tentativo successivo.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
`app/api/routes/admin.py:resume_lottery` · *Test:* —
|
||||
*Stato:* da implementare (B-82) · *Oggi:* la colonna non esiste.
|
||||
`app/api/routes/admin.py:resume_lottery` ·
|
||||
*Test:* `tests/unit/test_pool_verification.py::test_the_winners_own_bet_gone_holds_the_payout_and_pauses_the_lottery`, `tests/unit/test_pool_verification.py::test_after_the_resume_the_verification_repeats`, `tests/unit/test_admin.py::test_resume_releases_a_payout_held_because_the_winners_bet_left_the_chain`
|
||||
|
||||
- **[DAT-73]** Ogni pagamento ha un output di **riserva** verso l'indirizzo
|
||||
di riserva (`m/84'/746'/0'/1/0`), di valore
|
||||
@@ -691,7 +686,7 @@ Una scommessa: un utente in un round.
|
||||
broadcast --> confirmed: _on_bet_confirmed
|
||||
building --> [*]: cancellata (_release_failed_bet / _abandon)
|
||||
broadcast --> [*]: cancellata (_abandon)
|
||||
confirmed --> reorged_out: verifica prima del pagamento (da implementare)
|
||||
confirmed --> reorged_out: verifica prima del pagamento
|
||||
confirmed --> [*]
|
||||
reorged_out --> [*]
|
||||
```
|
||||
@@ -703,7 +698,7 @@ Una scommessa: un utente in un round.
|
||||
| `building`, `broadcast` | `confirmed` | `app/bets/confirmation.py:_on_bet_confirmed` |
|
||||
| `building` | cancellata | `app/bets/service.py:_release_failed_bet` (broadcast rifiutato esplicitamente) |
|
||||
| `building`, `broadcast` | cancellata | `app/tx/reconcile.py:_abandon` (abbandono con prova) |
|
||||
| `confirmed` | `reorged_out` | `app/rounds/scheduler.py:RoundScheduler._trigger_payout` (da implementare) |
|
||||
| `confirmed` | `reorged_out` | `app/rounds/scheduler.py:RoundScheduler._verify_round_pool` (da `_trigger_payout`), per ogni partecipante tranne il vincitore ([RND-39](ciclo-di-vita-del-round.md#4--pagamento)) |
|
||||
|
||||
Se il compare-and-set di [DAT-16] fallisce, la riga `building` non arriva
|
||||
nemmeno al commit. Un broadcast dall'esito ambiguo (timeout, connessione
|
||||
@@ -713,10 +708,9 @@ Una scommessa: un utente in un round.
|
||||
`app/bets/service.py:_release_failed_bet` · `app/tx/reconcile.py:_promote` ·
|
||||
`app/tx/reconcile.py:_abandon` · `app/bets/confirmation.py:_on_bet_confirmed` ·
|
||||
*Test:* `tests/unit/test_bets.py`, `tests/unit/test_confirmation.py`,
|
||||
`tests/unit/test_reconcile.py`
|
||||
*Stato:* da implementare (B-82, B-76) · *Oggi:* `reorged_out` non esiste (un
|
||||
partecipante confermato la cui scommessa sparisce resta `confirmed`), e
|
||||
qualunque eccezione del broadcast cancella la riga come un rifiuto.
|
||||
`tests/unit/test_reconcile.py`, `tests/unit/test_pool_verification.py`
|
||||
*Stato:* da implementare (B-76) · *Oggi:* qualunque eccezione del broadcast
|
||||
cancella la riga come un rifiuto.
|
||||
|
||||
- **[DAT-25]** `broadcast_at` è l'istante della fase 1 di `place_bet`, appena
|
||||
prima del broadcast, e non viene mai riscritto (nemmeno da un bump RBF). Non
|
||||
@@ -785,7 +779,7 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.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`) |
|
||||
| `fee_rate_sat_vb` | Integer | `1` | 1 – `MAX_FEE_RATE_SAT_VB` (10 000) | Commissione di rete delle nuove transazioni | Subito, per le transazioni costruite dopo; quelle in volo tengono il proprio `fee_rate_sat_vb` |
|
||||
| `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]) — *da implementare (B-82)* | Subito, per gli accrediti successivi; gli output già accreditati restano |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -810,8 +804,8 @@ 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-82, B-95), per le ultime due colonne ·
|
||||
*Oggi:* le colonne modificabili sono le prime otto.
|
||||
*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
|
||||
@@ -819,8 +813,6 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
[DAT-69], che scrivono la propria voce di audit.
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery` ·
|
||||
`app/api/routes/admin.py:resume_lottery` · *Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* le pause automatiche sono
|
||||
`resurrected_tx` (B-78) e `reorg` (B-85); quella di B-82 non esiste ancora.
|
||||
|
||||
- **[DAT-62]** `deposit_confirmations` (predefinito 6) è il numero di
|
||||
conferme dopo cui un output che paga un utente si accredita ([DAT-10]);
|
||||
@@ -834,10 +826,9 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
[scommesse-e-prelievi.md](scommesse-e-prelievi.md) (B-95). La migrazione le
|
||||
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`
|
||||
*Stato:* da implementare (B-82, B-95) · *Oggi:* `deposit_confirmations` e
|
||||
`max_round_pool_sats` non esistono: i depositi usano 1 conferma e non c'è
|
||||
tetto; `draw_confirmations` esiste.
|
||||
*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:
|
||||
@@ -861,9 +852,10 @@ senza riavviare (vedi [limiti-noti.md](limiti-noti.md)).
|
||||
`app/electrum/listener.py:ElectrumListener._record_reorg` ·
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
`app/tx/reconcile.py:reconcile_once` · *Test:* `tests/unit/test_pause_reason.py`, `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82, B-90) · *Oggi:* si scrivono
|
||||
`operator` (`POST /admin/pause`), `resurrected_tx` (il riconciliatore) e
|
||||
`reorg` (il listener); `payout_blocked` e `START_PAUSED` non hanno ancora chi li scrive.
|
||||
*Stato:* da implementare (B-90) · *Oggi:* si scrivono
|
||||
`operator` (`POST /admin/pause`), `resurrected_tx` (il riconciliatore),
|
||||
`reorg` (il listener) e `payout_blocked` (lo scheduler); `START_PAUSED` non ha
|
||||
ancora chi lo scrive.
|
||||
|
||||
- **[DAT-31]** Con `fee_address` vuoto (o di soli spazi) non si apre nessun round
|
||||
(B-66). Una volta impostato, l'API non permette di svuotarlo di nuovo: la
|
||||
@@ -1274,11 +1266,7 @@ letti con una scansione completa.
|
||||
*Codice:* `app/rounds/service.py:winner_share` ·
|
||||
`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-82) · *Oggi:* il pagamento sceglie fra
|
||||
tutti gli UTXO confermati dell'indirizzo del round, senza la verifica di
|
||||
[DAT-67]; l'identità con la riserva vale già per i round pagati con tutti
|
||||
e soli i loro ingressi.
|
||||
*Test:* `tests/unit/test_payout_builder.py`, `tests/unit/test_scheduler.py`, `tests/unit/test_pool_verification.py::test_a_reorged_out_bet_is_excluded_and_the_split_is_computed_on_what_is_there`
|
||||
|
||||
- ~~**[DAT-47]**~~ Assorbito da [RND-12](ciclo-di-vita-del-round.md): l'attesa
|
||||
delle scommesse in volo prima dell'estrazione è comportamento del round. Il
|
||||
|
||||
+9
-7
@@ -561,7 +561,7 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
|
||||
(`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
|
||||
se ne può controllare il saldo con un explorer. 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]).
|
||||
@@ -639,10 +639,12 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
|
||||
- **[OPS-18]**
|
||||
**Sintomi.** Un utente segnala un deposito che non vede nel saldo.
|
||||
**Cause, in ordine di probabilità.**
|
||||
1. La transazione non ha ancora le conferme richieste: oggi 1, dopo B-82
|
||||
`deposit_confirmations` (predefinito 6) e 120 per un'uscita coinbase
|
||||
1. La transazione non ha ancora le conferme richieste:
|
||||
`deposit_confirmations` (predefinito 6, modificabile da `/admin`) e 120
|
||||
per un'uscita coinbase
|
||||
([registrazione-e-depositi.md](registrazione-e-depositi.md#deposito)).
|
||||
Il deposito non viene mostrato nemmeno come "in attesa" finché non le ha.
|
||||
Nel frattempo compare come deposito *in attesa* nel saldo mostrato
|
||||
(ambra), non spendibile; si accredita da solo al blocco in cui matura.
|
||||
2. La corroborazione fallisce: nel log
|
||||
`N new UTXO(s) for user_id=… not corroborated by the other servers` e
|
||||
spesso `could not corroborate … with any of N other configured server(s)`.
|
||||
@@ -790,9 +792,9 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
|
||||
`app/tx/confirmation.py:poll_once`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-82, B-90) · *Oggi:* il sistema mette in
|
||||
pausa la lotteria per `resurrected_tx` (B-78) e `reorg` (B-85);
|
||||
`payout_blocked` non esiste ancora.
|
||||
*Stato:* da implementare (B-90) · *Oggi:* il sistema mette in pausa la
|
||||
lotteria per `resurrected_tx` (B-78), `reorg` (B-85) e `payout_blocked` (B-82);
|
||||
l'avvio con `START_PAUSED=true` non esiste ancora.
|
||||
|
||||
### Intervento manuale sui fondi
|
||||
|
||||
|
||||
@@ -213,10 +213,8 @@ flowchart TB
|
||||
H -- "Sì, e il quorum<br/>la conferma non spesa" --> I["Reintegrata<br/>(utxo_external_spend_reinstated)"]
|
||||
```
|
||||
|
||||
*Comportamento atteso.* Oggi ogni moneta confermata si accredita a 1
|
||||
conferma, di qualunque importo e anche se coinbase; il reintegro non è
|
||||
corroborato; gli aggiornamenti dello stesso utente non sono in fila; le
|
||||
monete sotto la soglia non compaiono nel saldo mostrato.
|
||||
*Comportamento atteso.* Gli aggiornamenti dello stesso utente non sono
|
||||
ancora in fila ([DEP-15], B-96).
|
||||
|
||||
- **[DEP-01]** Non c'è un importo minimo di deposito oltre alla soglia di
|
||||
polvere: una moneta di valore inferiore a `DUST_LIMIT_SATS` (294 sat) non
|
||||
@@ -260,10 +258,8 @@ monete sotto la soglia non compaiono nel saldo mostrato.
|
||||
Una moneta accreditata e poi sparita per una riorganizzazione più profonda
|
||||
della soglia segue la strada delle monete spese altrove
|
||||
([DEP-06], [DEP-07]); non c'è altra logica di annullamento.
|
||||
*Codice:* `app/deposits/service.py:find_new_credit_candidates`, `app/electrum/listener.py:ElectrumListener.refresh_user` ·
|
||||
*Test:* `tests/unit/test_deposits.py`
|
||||
*Stato:* da implementare (B-82) · *Oggi:* **una conferma basta** per ogni
|
||||
moneta, deposito o coinbase che sia.
|
||||
*Codice:* `app/deposits/service.py:find_new_credit_candidates`, `app/deposits/service.py:read_deposit_policy`, `app/deposits/service.py:classify_new_deposits`, `app/electrum/listener.py:ElectrumListener.refresh_user`, `app/electrum/listener.py:ElectrumListener.is_coinbase` ·
|
||||
*Test:* `tests/unit/test_deposits.py`, `tests/unit/test_deposit_confirmations.py`
|
||||
- **[DEP-04]** **Ogni accredito è corroborato.** Prima di aumentare un saldo,
|
||||
il server chiede agli *altri* server Electrum configurati se quella moneta
|
||||
esiste davvero, con quell'importo e con le conferme richieste
|
||||
@@ -312,11 +308,13 @@ monete sotto la soglia non compaiono nel saldo mostrato.
|
||||
*Test:* `tests/unit/test_deposits.py`, `tests/unit/test_confirmation_proof.py`
|
||||
- **[DEP-09]** **Due vie, un solo codice.** Un aggiornamento parte quando il
|
||||
server notifica un cambiamento su un indirizzo osservato, alla
|
||||
(re)iscrizione di un indirizzo, e al giro periodico: tutte e tre usano la
|
||||
stessa funzione di aggiornamento, così i due percorsi non possono
|
||||
comportarsi diversamente.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._consume_scripthash`, `app/electrum/listener.py:ElectrumListener._subscribe_and_refresh`, `app/deposits/reconcile.py:DepositReconciler._sweep_once` ·
|
||||
*Test:* `tests/unit/test_deposit_reconcile.py`
|
||||
(re)iscrizione di un indirizzo, al giro periodico e, per gli utenti che
|
||||
hanno un deposito in attesa ([DEP-14]), a ogni nuovo blocco — la notifica
|
||||
dell'indirizzo non arriva quando un deposito matura, perché la sua
|
||||
cronologia non cambia —: tutti usano la stessa funzione di aggiornamento,
|
||||
così i percorsi non possono comportarsi diversamente.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._consume_scripthash`, `app/electrum/listener.py:ElectrumListener._subscribe_and_refresh`, `app/electrum/listener.py:ElectrumListener._refresh_waiting_deposits`, `app/deposits/reconcile.py:DepositReconciler._sweep_once` ·
|
||||
*Test:* `tests/unit/test_deposit_reconcile.py`, `tests/unit/test_deposit_confirmations.py::test_new_blocks_refresh_the_users_that_have_a_waiting_deposit`
|
||||
- **[DEP-10]** <a id="dep-10"></a>Il giro periodico (`DepositReconciler`)
|
||||
riaggiorna **tutti** gli utenti, non solo quelli che risultano non
|
||||
iscritti: serve a recuperare un'iscrizione persa silenziosamente. Prima di
|
||||
@@ -388,9 +386,7 @@ corso (`has_pending`).
|
||||
contata due volte. `has_pending` è vero finché esiste almeno una
|
||||
transazione di (a) o un deposito di (b).
|
||||
*Codice:* `app/wallet/balance.py:compute_pending_balance`, `app/api/routes/users.py:me` ·
|
||||
*Test:* `tests/unit/test_balance.py`
|
||||
*Stato:* da implementare (B-82), per la parte (b) · *Oggi:* conta solo la
|
||||
parte (a).
|
||||
*Test:* `tests/unit/test_balance.py`, `tests/unit/test_deposit_confirmations.py::test_the_displayed_balance_includes_waiting_deposits_but_the_spendable_one_does_not`, `tests/unit/test_users.py::test_me_shows_waiting_deposits_in_the_pending_balance_only`
|
||||
- **[DEP-14]** **Depositi in attesa.** Una moneta sopra la soglia di polvere,
|
||||
confermata (altezza maggiore di 0), non ancora registrata e con meno
|
||||
conferme della sua soglia ([DEP-03]) è un *deposito in attesa*. Il listener
|
||||
@@ -401,10 +397,8 @@ corso (`has_pending`).
|
||||
non sposta denaro e non è spendibile, quindi non è corroborato — e dopo un
|
||||
riavvio si ricostruisce alla prima reiscrizione ([ELC-18](connessione-electrum.md#riconnessione)).
|
||||
Le monete ancora in mempool non sono depositi in attesa.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener.refresh_user`, `app/wallet/balance.py:compute_pending_balance` · *Test:* —
|
||||
*Stato:* da implementare (B-82) · *Oggi:* con la soglia a 1 conferma non
|
||||
esistono depositi in attesa; una moneta in mempool non compare in nessun
|
||||
saldo.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener.refresh_user`, `app/electrum/listener.py:ElectrumListener.pending_deposit_sats`, `app/wallet/balance.py:compute_pending_balance` ·
|
||||
*Test:* `tests/unit/test_deposit_confirmations.py::test_a_deposit_is_credited_exactly_at_six_confirmations`, `tests/unit/test_deposit_confirmations.py::test_a_waiting_deposit_that_vanishes_stops_being_shown`
|
||||
- **[DEP-13]** Ogni spesa (scommessa, prelievo) usa solo monete accreditate e
|
||||
non spese: il saldo mostrato è informativo, non spendibile. Se un utente
|
||||
chiede un prelievo o una scommessa coperti dal saldo mostrato ma non ancora
|
||||
|
||||
+2
-3
@@ -90,8 +90,7 @@ Restano **accettati**, e descritti in
|
||||
riorganizzazioni con pausa sopra la profondità 1 e verifica delle monete
|
||||
del round prima del pagamento ([ADR-52](adr/ADR-52-riorganizzazioni-catena-contengono-invece-ignorarle.md), B-82,
|
||||
B-85). Il denaro che un attaccante riesce a sottrarre è perso per la
|
||||
piattaforma. *Oggi:* depositi a 1 conferma e nessuna verifica prima
|
||||
del pagamento (B-82); le riorganizzazioni sono rilevate e, da profondità 2, mettono in pausa.
|
||||
piattaforma.
|
||||
|
||||
## Inventario dei segreti
|
||||
|
||||
@@ -799,7 +798,7 @@ sicurezza.
|
||||
([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 le monete del round non sono ancora verificate con il quorum.
|
||||
database.
|
||||
- **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
@@ -230,7 +230,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
| B-79 | Il pagamento di norma non ha resto, quindi non si può accelerare e può bloccare la lotteria | Critica | *da registrare* | `test_payout_builder.py`, `test_broadcast.py`, `test_scheduler.py`, `test_rounds_service.py`, `test_admin.py`, `test_migrations.py` | RND, TX |
|
||||
| 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 | *da registrare* | `test_draw_proof.py` | 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-82 | Un deposito annullato da una riorganizzazione dopo la scommessa rende il round impagabile; coinbase accreditate a 1 conferma | Critica | *da registrare* | `test_deposit_confirmations.py`, `test_pool_verification.py`, `test_electrum_listener.py`, `test_admin.py`, `test_users.py` | DEP, RND |
|
||||
| B-83 | Conferme e reintegri si basano solo sul server attivo | Alta | *da registrare* | `test_confirmation_proof.py`, `test_confirmation.py` | ELC, TX, DEP |
|
||||
| B-84 | Il quorum può ridursi a un solo server o a nessuno senza avvisi; certificati TLS non verificati | Alta | *da registrare* | `test_electrum_client.py`, `test_electrum_listener.py` | ELC, SEC |
|
||||
| B-85 | Le riorganizzazioni della catena non vengono né seguite né segnalate | Alta | *da registrare* | `test_electrum_listener.py` | ELC, RND |
|
||||
@@ -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.
|
||||
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.
|
||||
- **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 (1 critico,
|
||||
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (0 critici,
|
||||
4 alti, 6 medi), da implementare nell'ordine di
|
||||
[piano-correzioni.md](piano-correzioni.md).
|
||||
- **Prossimo numero libero:** `B-99`.
|
||||
Reference in new issue
Block a user