Complete the target spec for the 2026-09-28 review (B-73..B-98)
Finishes what 88221b0 started: sicurezza.md, audit-log.md and storico-bug.md
now describe the target behaviour too, the operator guides are corrected
against today's code, and api.md gains the missing /health/ready and
/admin/electrum sections plus every new error code in its catalog.
Harmonised names and owners across documents (the `resurrected_tx` pause
reason, the reconciler as the one that watches failed rows, the full
`winner_drawn` payload, a reason for an unanswered pre-payout pool check,
one corroboration-failure row per hash instead of one per tick).
Adds B-98 (coin selection stops at the first covering UTXO and refuses a
payable bet), tightens RND-40 to check both 1 and 400 participants and to
guard PUT /admin/config, and gives a stalled `closing` round a signal
(TX-29, closing_stalled).
Records three decisions confirmed on 2026-09-29: the draw's server_seed is
stored Fernet-encrypted until revealed (SEC-56, DAT-64), the chosen abuse
and key-check thresholds (SEC-49, SEC-54, SEC-55), and a broadcast
rejection is verified against every server before any coin is released,
so matching "already known" text is a shortcut, not a safety condition
(TX-04).
Spec only; no code changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88221b0dbd
commit
4be58709d1
16 files changed
+1377
-323
No files matched your search
+406
-96
@@ -28,21 +28,34 @@ ogni tipo di evento. Le colonne della tabella `audit_log` sono descritte in
|
||||
*Codice:* i chiamanti elencati nel catalogo, ognuno dei quali chiama
|
||||
`write_audit_log` e poi `session.commit()` sulla stessa sessione.
|
||||
|
||||
- **[AUD-03]** Quattro eventi sono **solo osservativi**: non accompagnano un
|
||||
cambiamento di stato e vengono salvati in una transazione propria, aperta
|
||||
apposta. Sono `draw_baseline_tip_unknown`, `draw_header_corroboration_failed`,
|
||||
`draw_stalled` e `payout_failed`. Per `payout_failed` la scrittura è anche
|
||||
*best effort*: se fallisce, l'errore viene registrato nel log applicativo
|
||||
- **[AUD-03]** Alcuni eventi sono **solo osservativi**: non accompagnano un
|
||||
cambiamento di stato del database e vengono salvati in una transazione
|
||||
propria, aperta apposta. Sono `draw_baseline_tip_unknown`,
|
||||
`draw_header_corroboration_failed`, `draw_stalled`, `payout_failed`,
|
||||
`rbf_bump_failed` ([AUD-40]), `restore_gap_detected` ([AUD-43]),
|
||||
`closing_stalled` ([AUD-46]) e `chain_reorg_detected` di profondità 1
|
||||
([AUD-38]; con profondità ≥ 2 accompagna la pausa e ne condivide la
|
||||
transazione). Per `payout_failed` la scrittura è anche *best effort*: se
|
||||
fallisce, l'errore viene registrato nel log applicativo
|
||||
(`could not record the payout failure of round …`) e ignorato.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._adopt_baseline_tip`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._log_payout_failure`
|
||||
`app/rounds/scheduler.py:RoundScheduler._log_payout_failure`,
|
||||
`app/tx/broadcast.py:RbfBumper._tick`, `app/main.py:lifespan`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header`
|
||||
*Stato:* da implementare (B-85, B-87, B-90, B-96), per i quattro eventi
|
||||
nuovi · *Oggi:* gli eventi osservativi sono solo i primi quattro.
|
||||
|
||||
- **[AUD-04]** Il `payload` è un dizionario serializzato in JSON nella colonna
|
||||
`payload_json`; `created_at` è impostato dal database all'inserimento, in
|
||||
UTC. `user_id` e `round_id` sono chiavi esterne opzionali, valorizzate come
|
||||
indicato evento per evento nel catalogo.
|
||||
*Codice:* `app/audit/log.py:write_audit_log`, `app/db/models.py:AuditLog`
|
||||
`payload_json`. `created_at` è impostato dall'applicazione, non dal
|
||||
database: è il default ORM `utcnow` della colonna (`datetime.now(timezone.utc)`,
|
||||
valutato quando la riga viene inserita), senza alcun `server_default`; una
|
||||
riga inserita a mano nel database senza `created_at` è quindi rifiutata
|
||||
(colonna non nulla). `user_id` e `round_id` sono chiavi esterne opzionali,
|
||||
valorizzate come indicato evento per evento nel catalogo.
|
||||
*Codice:* `app/audit/log.py:write_audit_log`, `app/db/models.py:utcnow`,
|
||||
`app/db/models.py:AuditLog`
|
||||
|
||||
- **[AUD-05]** Il registro è **in sola aggiunta**: nessuna parte del codice
|
||||
modifica o cancella una riga di `audit_log`, e non c'è alcuna rotazione o
|
||||
@@ -52,20 +65,27 @@ ogni tipo di evento. Le colonne della tabella `audit_log` sono descritte in
|
||||
- **[AUD-06]** `event_type` è una colonna `String(32)`. Il nome più lungo oggi
|
||||
in uso, `draw_header_corroboration_failed`, occupa esattamente 32 caratteri:
|
||||
un nuovo tipo di evento deve restare entro questo limite. SQLite non lo fa
|
||||
rispettare, un altro database lo farebbe.
|
||||
rispettare, un altro database lo farebbe. I tipi aggiunti dalle correzioni
|
||||
in corso ([AUD-37] … [AUD-46]) lo rispettano: il più lungo,
|
||||
`participant_reorged_out`, ha 23 caratteri.
|
||||
*Codice:* `app/db/models.py:AuditLog`
|
||||
|
||||
## Come si legge
|
||||
|
||||
- **[AUD-07]** `GET /admin/audit-log` (protetto da `X-Admin-Token`) restituisce
|
||||
le righe dalla più recente (ordine per `id` decrescente), con il parametro
|
||||
`limit` (predefinito 200, minimo 1, massimo 500). Ogni elemento contiene
|
||||
`id`, `event_type`, `payload` (già decodificato da JSON), `user_id`,
|
||||
`round_id` e `created_at` in ISO 8601 UTC. **Non esistono filtri** per tipo
|
||||
di evento, utente o round, né paginazione: le righe più vecchie delle ultime
|
||||
500 si leggono solo direttamente dal database.
|
||||
le righe dalla più recente (ordine per `id` decrescente), al massimo `limit`
|
||||
per richiesta (predefinito 200, minimo 1, massimo 500), filtrabili per tipo
|
||||
di evento, `round_id`, `user_id` e intervallo di `created_at`, e a pagine
|
||||
(`before_id`), così che ogni riga del registro sia raggiungibile dall'API.
|
||||
Ogni elemento contiene `id`, `event_type`, `payload` (già decodificato da
|
||||
JSON), `user_id`, `round_id` e `created_at` in ISO 8601 UTC. Parametri,
|
||||
combinazione dei filtri ed errori sono definiti in
|
||||
[api.md](api.md#get-adminaudit-log) (API-37), che ne è il proprietario.
|
||||
*Codice:* `app/api/routes/admin.py:list_audit_log` ·
|
||||
*Test:* `tests/unit/test_admin.py`
|
||||
*Stato:* da implementare (B-92), per filtri e paginazione · *Oggi:* accetta
|
||||
solo `limit`; le righe più vecchie delle ultime 500 si leggono solo
|
||||
direttamente dal database.
|
||||
|
||||
- **[AUD-08]** La sezione "Audit log" di `/admin` chiama l'endpoint senza
|
||||
parametri (quindi le ultime 200 righe) al login e ogni volta che si apre la
|
||||
@@ -73,20 +93,36 @@ ogni tipo di evento. Le colonne della tabella `audit_log` sono descritte in
|
||||
filtri né ricerca.
|
||||
*Codice:* `app/static/admin.js:loadAuditLog`
|
||||
|
||||
- **[AUD-09]** Una sola logica del programma rilegge il registro:
|
||||
`_retry_payout_if_due` usa la riga `payout_failed` più recente del round come
|
||||
orologio per decidere se ritentare il pagamento. Se è più recente di 60
|
||||
secondi (`_PAYOUT_RETRY_INTERVAL_SECONDS`) il tentativo viene saltato; se non
|
||||
esiste, il tentativo parte subito. Nessun'altra decisione dipende dal
|
||||
registro.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._retry_payout_if_due` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
- **[AUD-09]** Le logiche del programma che rileggono il registro sono
|
||||
esattamente queste, e nessun'altra decisione dipende da esso:
|
||||
- `_retry_payout_if_due` usa la riga `payout_failed` più recente del round
|
||||
come orologio per decidere se ritentare il pagamento. Se è più recente di
|
||||
60 secondi (`_PAYOUT_RETRY_INTERVAL_SECONDS`) il tentativo viene saltato;
|
||||
se non esiste, il tentativo parte subito;
|
||||
- l'RBF cerca, prima di scrivere `rbf_bump_failed`, una riga dello stesso
|
||||
tipo per la stessa transazione con lo stesso txid corrente e lo stesso
|
||||
motivo; se esiste non ne scrive un'altra ([AUD-40]);
|
||||
- lo scheduler usa la riga `closing_stalled` più recente del round come
|
||||
orologio delle ripetizioni ([AUD-46]);
|
||||
- lo scheduler scrive `draw_baseline_tip_unknown` solo se il round non ne
|
||||
ha già una ([AUD-25]).
|
||||
|
||||
Leggere il registro, invece di tenere lo stato in memoria, fa sì che un
|
||||
riavvio non ripeta né anticipi queste righe.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._retry_payout_if_due`,
|
||||
`app/tx/broadcast.py:RbfBumper._tick`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo `payout_failed`)
|
||||
*Stato:* da implementare (B-87, B-96, B-73), per le ultime tre letture ·
|
||||
*Oggi:* l'unica lettura è quella di `payout_failed`.
|
||||
|
||||
## Catalogo degli eventi
|
||||
|
||||
Legenda delle colonne `user_id` / `round_id`: **sì** = sempre valorizzato,
|
||||
**no** = sempre vuoto, **dipende** = come spiegato nel testo. Gli importi sono
|
||||
sempre in satoshi.
|
||||
sempre in satoshi. Nel diagramma, gli eventi marcati con * sono da
|
||||
implementare.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -98,6 +134,11 @@ flowchart LR
|
||||
admin_password_reset
|
||||
bug_report_status_changed
|
||||
end
|
||||
subgraph Account["Account"]
|
||||
user_registered["user_registered *"]
|
||||
password_changed["password_changed *"]
|
||||
restore_gap_detected["restore_gap_detected *"]
|
||||
end
|
||||
subgraph Fondi["Denaro degli utenti"]
|
||||
deposit_credited
|
||||
utxo_spent_externally
|
||||
@@ -109,16 +150,25 @@ flowchart LR
|
||||
end
|
||||
subgraph Round["Round"]
|
||||
round_closed
|
||||
closing_stalled["closing_stalled *"]
|
||||
draw_committed["draw_committed *"]
|
||||
winner_drawn
|
||||
draw_baseline_tip_unknown
|
||||
draw_header_corroboration_failed
|
||||
draw_stalled
|
||||
participant_reorged_out["participant_reorged_out *"]
|
||||
payout_blocked["payout_blocked *"]
|
||||
payout_failed
|
||||
payout_sent
|
||||
end
|
||||
subgraph Ricon["Riconciliazione"]
|
||||
pending_tx_recovered
|
||||
pending_tx_abandoned
|
||||
pending_tx_resurrected["pending_tx_resurrected *"]
|
||||
rbf_bump_failed["rbf_bump_failed *"]
|
||||
end
|
||||
subgraph Catena["Catena"]
|
||||
chain_reorg_detected["chain_reorg_detected *"]
|
||||
end
|
||||
```
|
||||
|
||||
@@ -129,23 +179,54 @@ flowchart LR
|
||||
cambiato**, ciascuna `{"from": <valore precedente>, "to": <valore nuovo>}`
|
||||
(i campi sono quelli di `_CONFIG_FIELDS`: `fee_address`, `bet_amount_sats`,
|
||||
`round_duration_seconds`, `round_cooldown_seconds`, `fee_rate_sat_vb`,
|
||||
`rbf_timeout_seconds`, `draw_animation_seconds`). Un campo inviato con lo
|
||||
`rbf_timeout_seconds`, `draw_animation_seconds`, `deposit_confirmations`,
|
||||
`draw_confirmations`, `max_round_pool_sats`). Un campo inviato con lo
|
||||
stesso valore che aveva non compare; se nulla cambia, non si scrive nessuna
|
||||
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-73, B-82, B-95), per gli ultimi tre campi ·
|
||||
*Oggi:* i campi sono i primi sette.
|
||||
|
||||
- **[AUD-11]** `lottery_paused` — `POST /admin/pause`. Payload vuoto `{}`.
|
||||
`user_id` no, `round_id` no. Viene scritto a ogni chiamata, anche se la
|
||||
lotteria era già in pausa. Stessa transazione di `paused = true`.
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery` ·
|
||||
- **[AUD-11]** `lottery_paused` — la lotteria è stata messa in pausa, da
|
||||
chiunque. Payload `{"reason"}`, più `"note"` quando l'operatore l'ha
|
||||
indicata nel corpo di `POST /admin/pause`
|
||||
([api.md](api.md#post-adminpause-e-post-adminresume), API-32). `reason` è
|
||||
la causa di **questa** pausa, con uno di questi valori, gli stessi di
|
||||
`paused_reason` ([modello-dati.md](modello-dati.md), DAT-69):
|
||||
|
||||
| `reason` | Chi scrive la riga | Evento che la accompagna, nella stessa transazione |
|
||||
|---|---|---|
|
||||
| `operator` | `POST /admin/pause`; l'avvio con `START_PAUSED=true` ([operativita.md](operativita.md), OPS-27) | — |
|
||||
| `reorg` | il listener Electrum, per una riorganizzazione di profondità ≥ 2 | `chain_reorg_detected` ([AUD-38]) |
|
||||
| `payout_blocked` | lo scheduler, quando la scommessa del vincitore non è più sulla catena | `payout_blocked` ([AUD-44]) |
|
||||
| `resurrected_tx` | il riconciliatore, quando una riga `failed` risulta confermata | `pending_tx_resurrected` ([AUD-39]) |
|
||||
|
||||
Le pause automatiche (`reorg`, `payout_blocked`, `resurrected_tx`) sono
|
||||
scritte dal sistema, senza alcuna richiesta al pannello. Una pausa
|
||||
`operator` richiesta mentre è attivo un motivo automatico lascia
|
||||
`paused_reason` com'era (DAT-69), ma la riga riporta comunque
|
||||
`reason = "operator"`. `user_id` no, `round_id` no. La riga viene scritta a
|
||||
ogni pausa, anche se la lotteria era già in pausa. Stessa transazione di
|
||||
`paused = true`.
|
||||
*Codice:* `app/api/routes/admin.py:pause_lottery`, `app/main.py:lifespan`,
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._trigger_payout`,
|
||||
`app/tx/reconcile.py:reconcile_once` ·
|
||||
*Test:* nessun test verifica la riga di audit.
|
||||
*Stato:* da implementare (B-78, B-82, B-85, B-90) · *Oggi:* scritto solo
|
||||
da `POST /admin/pause`, con payload vuoto `{}`.
|
||||
|
||||
- **[AUD-12]** `lottery_resumed` — `POST /admin/resume`. Payload vuoto `{}`.
|
||||
`user_id` no, `round_id` no. Scritto a ogni chiamata, anche se la lotteria
|
||||
non era in pausa. Stessa transazione di `paused = false`.
|
||||
- **[AUD-12]** `lottery_resumed` — `POST /admin/resume`. Payload
|
||||
`{"reason"}`: il `paused_reason` che la ripresa ha tolto (`null` se la
|
||||
lotteria non era in pausa), così il registro dice che cosa l'operatore ha
|
||||
accettato di riprendere (API-32). `user_id` no, `round_id` no. Scritto a
|
||||
ogni chiamata, anche se la lotteria non era in pausa. Stessa transazione di
|
||||
`paused = false`, `paused_reason = NULL` e dell'azzeramento di
|
||||
`payout_blocked_at`.
|
||||
*Codice:* `app/api/routes/admin.py:resume_lottery` ·
|
||||
*Test:* nessun test verifica la riga di audit.
|
||||
*Stato:* da implementare (B-78, B-82, B-85) · *Oggi:* payload vuoto `{}`.
|
||||
|
||||
- **[AUD-13]** `admin_privkey_accessed` — `GET /admin/users/{id}/privkey` ha
|
||||
esportato la chiave privata (WIF) di un utente. Payload `{"user_id"}`.
|
||||
@@ -175,6 +256,46 @@ flowchart LR
|
||||
*Codice:* `app/api/routes/admin.py:update_bug_report_status` ·
|
||||
*Test:* `tests/unit/test_bug_reports.py`
|
||||
|
||||
### Account
|
||||
|
||||
- **[AUD-41]** `user_registered` — una registrazione è riuscita
|
||||
([registrazione-e-depositi.md](registrazione-e-depositi.md#registrazione),
|
||||
REG-16). Payload `{"derivation_index", "address"}`: l'indice di derivazione
|
||||
e l'indirizzo assegnati. Lo username non compare (è nella tabella `users`).
|
||||
`user_id` sì (il nuovo utente), `round_id` no. Stessa transazione che crea
|
||||
l'utente. È ciò che, dopo un ripristino, permette di ricostruire quali
|
||||
indici erano già assegnati. Nessuna logica rilegge la riga.
|
||||
*Codice:* `app/auth/routes.py:register` · *Test:* —
|
||||
*Stato:* da implementare (B-92) · *Oggi:* la registrazione non scrive alcun
|
||||
evento.
|
||||
|
||||
- **[AUD-42]** `password_changed` — un utente ha cambiato la propria password
|
||||
con `POST /users/me/change-password`
|
||||
([registrazione-e-depositi.md](registrazione-e-depositi.md#sessione-e-password),
|
||||
REG-12). Payload `{"user_id"}`; né la vecchia né la nuova password
|
||||
compaiono. `user_id` sì, `round_id` no. Stessa transazione del nuovo hash e
|
||||
dell'incremento di `token_version`. Un tentativo fallito (password attuale
|
||||
sbagliata, nuova troppo corta) non scrive nulla. Nessuna logica rilegge la
|
||||
riga.
|
||||
*Codice:* `app/api/routes/users.py:change_password` · *Test:* —
|
||||
*Stato:* da implementare (B-94) · *Oggi:* il cambio password non scrive
|
||||
alcun evento.
|
||||
|
||||
- **[AUD-43]** `restore_gap_detected` — il controllo all'avvio ha trovato
|
||||
cronologia sulla catena per almeno uno dei 20 indici di derivazione
|
||||
successivi al massimo assegnato, cioè il database è probabilmente un backup
|
||||
più vecchio della realtà
|
||||
([registrazione-e-depositi.md](registrazione-e-depositi.md#registrazione),
|
||||
REG-15). Payload `{"highest_assigned_index", "used"}`:
|
||||
`highest_assigned_index` è il massimo indice presente in `users` (`null` se
|
||||
non c'è alcun utente), `used` una lista di `{"derivation_index", "address"}`
|
||||
per ogni indirizzo con cronologia. `user_id` no, `round_id` no. Una riga per
|
||||
ogni avvio in cui il controllo trova indirizzi usati; transazione propria
|
||||
([AUD-03]). La sospensione delle registrazioni che ne segue non dipende da
|
||||
questa riga. Procedura in [operativita.md](operativita.md) (OPS-28).
|
||||
*Codice:* `app/main.py:lifespan` · *Test:* —
|
||||
*Stato:* da implementare (B-90) · *Oggi:* nessun controllo e nessun evento.
|
||||
|
||||
### Denaro degli utenti
|
||||
|
||||
- **[AUD-16]** `deposit_credited` — un nuovo UTXO confermato sull'indirizzo di
|
||||
@@ -201,11 +322,16 @@ flowchart LR
|
||||
*Test:* `tests/unit/test_deposits.py`
|
||||
|
||||
- **[AUD-18]** `utxo_external_spend_reinstated` — un UTXO segnato come speso
|
||||
all'esterno è ricomparso tra i non spesi e la marcatura è stata annullata.
|
||||
Payload `{"txid", "vout", "amount_sats"}`. `user_id` sì, `round_id` no.
|
||||
Stessa transazione dell'annullamento e del ricalcolo del saldo.
|
||||
all'esterno è ricomparso tra i non spesi, il quorum degli altri server lo ha
|
||||
confermato non speso con lo stesso importo
|
||||
([registrazione-e-depositi.md](registrazione-e-depositi.md#deposito), DEP-08)
|
||||
e la marcatura è stata annullata. Payload `{"txid", "vout", "amount_sats"}`.
|
||||
`user_id` sì, `round_id` no. Stessa transazione dell'annullamento e del
|
||||
ricalcolo del saldo.
|
||||
*Codice:* `app/deposits/service.py:reinstate_reappeared_utxos` ·
|
||||
*Test:* `tests/unit/test_deposits.py`
|
||||
*Stato:* da implementare (B-83), per la conferma del quorum · *Oggi:* basta
|
||||
la parola del server attivo.
|
||||
|
||||
- **[AUD-19]** `bet_placed` — la rete ha accettato la transazione di una
|
||||
scommessa. Payload `{"txid", "amount_sats"}`: `txid` è quello del primo
|
||||
@@ -216,13 +342,19 @@ flowchart LR
|
||||
*Codice:* `app/bets/service.py:place_bet` ·
|
||||
*Test:* `tests/unit/test_bets.py`
|
||||
|
||||
- **[AUD-20]** `bet_broadcast_failed` — la rete ha rifiutato la transazione di
|
||||
una scommessa; gli UTXO sono stati liberati e il partecipante rimosso.
|
||||
- **[AUD-20]** `bet_broadcast_failed` — la rete ha rifiutato **esplicitamente**
|
||||
la transazione di una scommessa; gli UTXO sono stati liberati e il
|
||||
partecipante rimosso. Un esito incerto (timeout, connessione caduta dopo
|
||||
l'invio) non scrive questo evento: la riga resta `building` per il
|
||||
riconciliatore
|
||||
([scommesse-e-prelievi.md](scommesse-e-prelievi.md#cosa-succede), B-76).
|
||||
Payload `{"txid", "reason"}`, con `reason` il testo dell'errore troncato a
|
||||
200 caratteri. `user_id` sì, `round_id` **no** (a differenza di
|
||||
`bet_placed`). Stessa transazione del ripristino.
|
||||
*Codice:* `app/bets/service.py:_release_failed_bet` ·
|
||||
*Test:* `tests/unit/test_bets.py`
|
||||
*Stato:* da implementare (B-76) · *Oggi:* scritto per qualunque errore
|
||||
d'invio, compresi timeout e disconnessioni.
|
||||
|
||||
- **[AUD-21]** `withdrawal_sent` — la rete ha accettato la transazione di un
|
||||
prelievo. Payload `{"txid", "amount_sent_sats", "external_address"}`:
|
||||
@@ -234,12 +366,16 @@ flowchart LR
|
||||
*Codice:* `app/withdrawals/service.py:request_withdrawal` ·
|
||||
*Test:* nessun test verifica la riga di audit.
|
||||
|
||||
- **[AUD-22]** `withdrawal_broadcast_failed` — la rete ha rifiutato la
|
||||
transazione di un prelievo; gli UTXO sono stati liberati e il prelievo
|
||||
segnato `failed`. Payload `{"txid", "reason"}` (`reason` troncato a 200
|
||||
caratteri). `user_id` sì, `round_id` no. Stessa transazione del ripristino.
|
||||
- **[AUD-22]** `withdrawal_broadcast_failed` — la rete ha rifiutato
|
||||
**esplicitamente** la transazione di un prelievo; gli UTXO sono stati
|
||||
liberati e il prelievo segnato `failed`. Come per [AUD-20], un esito
|
||||
incerto non scrive questo evento (B-76). Payload `{"txid", "reason"}`
|
||||
(`reason` troncato a 200 caratteri). `user_id` sì, `round_id` no. Stessa
|
||||
transazione del ripristino.
|
||||
*Codice:* `app/withdrawals/service.py:_release_failed_withdrawal` ·
|
||||
*Test:* nessun test verifica la riga di audit.
|
||||
*Stato:* da implementare (B-76) · *Oggi:* scritto per qualunque errore
|
||||
d'invio, compresi timeout e disconnessioni.
|
||||
|
||||
### Round
|
||||
|
||||
@@ -251,44 +387,105 @@ flowchart LR
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
- **[AUD-24]** `winner_drawn` — il vincitore è stato estratto. Payload
|
||||
- **[AUD-46]** `closing_stalled` — un round è in `closing` oltre la propria
|
||||
scadenza da più di `CLOSING_STALL_THRESHOLD_SECONDS` (1800 s) perché
|
||||
almeno una scommessa è ancora in volo. La regola, soglia e ripetizione
|
||||
comprese, è
|
||||
[TX-29](transazioni-e-riconciliazione.md#riconciliazione); qui c'è solo la
|
||||
voce del catalogo. Payload `{"in_flight", "txids", "seconds_past_deadline"}`:
|
||||
numero di scommesse in `building` o `broadcast`, lista dei loro txid
|
||||
correnti, secondi trascorsi dalla scadenza. `user_id` no, `round_id` sì.
|
||||
Transazione propria ([AUD-03]). La riga più recente del round è l'orologio
|
||||
della ripetizione successiva, 1800 s dopo ([AUD-09]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._tick` · *Test:* —
|
||||
*Stato:* da implementare (B-96) · *Oggi:* l'attesa in `closing` non scrive
|
||||
alcun evento.
|
||||
|
||||
- **[AUD-37]** `draw_committed` — il round è entrato in `drawing`: elenco dei
|
||||
partecipanti e altezza del blocco di estrazione sono fissati
|
||||
([ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md#2--chiusura),
|
||||
RND-14, RND-33). Payload `{"participants_hash", "draw_height",
|
||||
"participants", "server_seed_hash"}`: impronta dell'elenco ordinato,
|
||||
altezza del blocco di estrazione, numero di partecipanti confermati,
|
||||
impronta del segreto pubblicata all'apertura (RND-34). `server_seed` **non**
|
||||
compare. `user_id` no, `round_id` sì. Stessa transazione che porta il round
|
||||
a `drawing` e salva `draw_position`, `participants_hash` e `draw_height`.
|
||||
Poiché la riga è salvata prima che il blocco a `draw_height` esista, il
|
||||
registro prova che impronta e altezza erano fissate in anticipo. Nessuna
|
||||
logica rilegge la riga: la ripresa dell'estrazione (RND-36) legge i valori
|
||||
dal round.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-73) · *Oggi:* l'ingresso in `drawing` non
|
||||
scrive alcun evento.
|
||||
|
||||
- **[AUD-24]** `winner_drawn` — il vincitore è stato estratto e salvato; in
|
||||
questa riga il segreto del round viene **rivelato**. Payload
|
||||
`{"winner_address", "pool_amount_sats", "block_height", "block_hash",
|
||||
"participants"}`: indirizzo del vincitore, montepremi totale (somma delle
|
||||
puntate nette), altezza e hash del blocco usato come seme, numero di
|
||||
partecipanti. `user_id` sì (il vincitore), `round_id` sì. Stessa transazione
|
||||
che salva i dati dell'estrazione sul round e lo porta a `paying_out`.
|
||||
"participants", "draw_height", "participants_hash", "server_seed",
|
||||
"seed"}`: indirizzo del vincitore, montepremi totale (somma delle puntate
|
||||
nette), altezza e hash del blocco usato, numero di partecipanti, altezza del
|
||||
blocco di estrazione fissata all'ingresso in `drawing` (uguale a
|
||||
`block_height`), impronta dei partecipanti (uguale a quella di
|
||||
`draw_committed`), `server_seed` in esadecimale (64 caratteri) e il seme
|
||||
calcolato, in esadecimale (64 caratteri, lo stesso valore di `seed_int`:
|
||||
come stringa, perché un intero di 256 bit perde precisione in JavaScript).
|
||||
Con `draw_committed` e la catena, la riga basta a rifare il calcolo di
|
||||
[RND-17](ciclo-di-vita-del-round.md#3--estrazione). `user_id` sì (il
|
||||
vincitore), `round_id` sì. Stessa transazione che salva il vincitore sul
|
||||
round e lo porta a `paying_out` ([RND-18](ciclo-di-vita-del-round.md#3--estrazione)):
|
||||
`server_seed` non può quindi comparire nel registro prima che il vincitore
|
||||
sia salvato.
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
*Stato:* da implementare (B-73), per le ultime quattro chiavi · *Oggi:* il
|
||||
payload ha solo le prime cinque; il round non ha segreto né impronta dei
|
||||
partecipanti.
|
||||
|
||||
- **[AUD-25]** `draw_baseline_tip_unknown` — al momento della chiusura
|
||||
l'altezza della catena non era nota, quindi il primo blocco ricevuto è stato
|
||||
preso come base e l'estrazione aspetta il blocco successivo. Payload
|
||||
`{"baseline_height"}`. `user_id` no, `round_id` sì. Transazione propria
|
||||
([AUD-03]). Dovrebbe essere irraggiungibile (lo scheduler non lavora senza
|
||||
un'altezza nota), vedi [ciclo-di-vita-del-round.md](ciclo-di-vita-del-round.md).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._adopt_baseline_tip` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
- **[AUD-25]** `draw_baseline_tip_unknown` — quando il round sarebbe pronto
|
||||
per `drawing`, l'altezza della catena non era nota: il round resta in
|
||||
`closing` e il passaggio si ritenta al passo successivo
|
||||
([RND-19](ciclo-di-vita-del-round.md#3--estrazione)). Payload vuoto `{}`.
|
||||
Una sola riga per round ([AUD-09]). `user_id` no, `round_id` sì.
|
||||
Transazione propria ([AUD-03]). Dovrebbe essere irraggiungibile (lo
|
||||
scheduler non lavora senza un'altezza nota).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._close_and_draw`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._adopt_baseline_tip` ·
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* il round entra comunque in
|
||||
`drawing`, il primo blocco ricevuto è preso come base e l'estrazione
|
||||
aspetta il blocco successivo; payload `{"baseline_height"}` con l'altezza
|
||||
adottata.
|
||||
|
||||
- **[AUD-26]** `draw_header_corroboration_failed` — il blocco candidato per
|
||||
l'estrazione non è stato confermato dalla maggioranza degli altri server
|
||||
Electrum (o nessuno ha risposto); l'estrazione aspetta un blocco ulteriore.
|
||||
Una riga per ogni blocco candidato rifiutato. Payload
|
||||
`{"height", "reported_hash"}`. `user_id` no, `round_id` sì. Transazione
|
||||
propria ([AUD-03]).
|
||||
- **[AUD-26]** `draw_header_corroboration_failed` — raggiunte le conferme
|
||||
richieste, il quorum dei server non concorda sull'header all'altezza
|
||||
`draw_height`, o non risponde abbastanza
|
||||
([RND-16](ciclo-di-vita-del-round.md#3--estrazione)); al passo successivo
|
||||
si richiede la stessa altezza. Una riga per ogni tentativo fallito. Payload
|
||||
`{"height", "reported_hash"}`: `height` è sempre `draw_height`,
|
||||
`reported_hash` l'hash riportato dal server attivo. `user_id` no,
|
||||
`round_id` sì. Transazione propria ([AUD-03]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* una riga per ogni blocco
|
||||
candidato rifiutato, con `height` l'altezza di quel candidato; l'estrazione
|
||||
aspetta poi un blocco ulteriore invece di richiedere la stessa altezza.
|
||||
|
||||
- **[AUD-27]** `draw_stalled` — l'estrazione aspetta un nuovo blocco da più di
|
||||
360 secondi (`_DRAW_STALL_THRESHOLD_SECONDS`), contati da
|
||||
`drawing_started_at`; la riga si ripete ogni 360 secondi finché l'attesa
|
||||
continua. Payload `{"tip_at_close", "current_tip_height",
|
||||
"elapsed_seconds"}`. Nota: `tip_at_close` è la base *attuale* dell'attesa, che
|
||||
dopo un `draw_header_corroboration_failed` diventa l'altezza del candidato
|
||||
rifiutato, non più quella della chiusura. `user_id` no, `round_id` sì.
|
||||
Transazione propria ([AUD-03]). Non viene scritta mentre è in corso una
|
||||
corroborazione, solo mentre si aspetta un blocco.
|
||||
- **[AUD-27]** `draw_stalled` — l'estrazione è in stallo secondo la regola di
|
||||
[RND-20](ciclo-di-vita-del-round.md#3--estrazione), che ne è il
|
||||
proprietario (soglia, condizioni e ripetizione); qui c'è solo la voce del
|
||||
catalogo. Payload `{"draw_height", "current_tip_height",
|
||||
"elapsed_seconds"}`: altezza del blocco di estrazione, altezza della catena
|
||||
nota in quel momento, secondi da quando dura la condizione di RND-20.
|
||||
`user_id` no, `round_id` sì. Transazione propria ([AUD-03]).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._wait_for_next_block` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
*Test:* `tests/unit/test_scheduler.py` (solo il comportamento attuale)
|
||||
*Stato:* da implementare (B-73) · *Oggi:* scritto quando l'attesa di un
|
||||
nuovo blocco supera 360 secondi contati da `drawing_started_at`, e mai
|
||||
durante una corroborazione; payload `{"tip_at_close",
|
||||
"current_tip_height", "elapsed_seconds"}`, dove `tip_at_close` è la base
|
||||
*attuale* dell'attesa, che dopo un `draw_header_corroboration_failed`
|
||||
diventa l'altezza del candidato rifiutato.
|
||||
|
||||
- **[AUD-28]** `payout_failed` — un tentativo di pagamento non è partito.
|
||||
Payload `{"round_id", "reason"}`. `user_id` dipende: il vincitore, tranne
|
||||
@@ -309,8 +506,13 @@ flowchart LR
|
||||
| `broadcast rejected` | La rete ha rifiutato la transazione; la riga `building` resta al riconciliatore |
|
||||
|
||||
Un tentativo saltato perché esiste già un pagamento `building` o `pending`
|
||||
per il round **non** scrive `payout_failed`. Cosa fare per ogni motivo è
|
||||
descritto in [operativita.md](operativita.md).
|
||||
per il round **non** scrive `payout_failed`, e nemmeno un pagamento
|
||||
trattenuto perché la scommessa del vincitore non è più sulla catena, che
|
||||
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
|
||||
[operativita.md](operativita.md).
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._log_payout_failure` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
@@ -319,10 +521,42 @@ flowchart LR
|
||||
pagamento. Payload `{"txid", "winner_sats", "commission_sats"}`: quanto
|
||||
riceve il vincitore (già al netto della commissione di rete) e quanto va a
|
||||
`fee_address`. `user_id` sì (il vincitore), `round_id` sì. Stessa transazione
|
||||
che porta la transazione pendente da `building` a `pending`.
|
||||
che porta la transazione pendente da `building` a `pending`. Per un
|
||||
pagamento promosso dal riconciliatore vale [AUD-30].
|
||||
*Codice:* `app/rounds/scheduler.py:RoundScheduler._trigger_payout` ·
|
||||
*Test:* `tests/unit/test_scheduler.py`
|
||||
|
||||
- **[AUD-45]** `participant_reorged_out` — la verifica prima del pagamento
|
||||
ha trovato assente, secondo il quorum, l'uscita della scommessa di un
|
||||
partecipante, che passa a `reorged_out` ed è escluso dal montepremi
|
||||
([RND-39](ciclo-di-vita-del-round.md#4--pagamento), DAT-67). Una riga per
|
||||
partecipante. Payload `{"txid", "vout", "amount_sats", "pool_amount_sats"}`:
|
||||
l'uscita della scommessa sparita, il suo importo netto e il montepremi
|
||||
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`.
|
||||
|
||||
- **[AUD-44]** `payout_blocked` — la verifica prima del pagamento ha trovato
|
||||
assente la scommessa **del vincitore**: il pagamento non si costruisce, il
|
||||
round resta in `paying_out` con `payout_blocked_at` e la lotteria va in
|
||||
pausa automatica
|
||||
([RND-39](ciclo-di-vita-del-round.md#4--pagamento), DAT-68). Payload
|
||||
`{"txid", "vout", "amount_sats"}`: l'uscita della scommessa del vincitore.
|
||||
`user_id` sì (il vincitore), `round_id` sì. Stessa transazione di
|
||||
`payout_blocked_at`, della pausa e della riga `lottery_paused` con
|
||||
`reason = "payout_blocked"` ([AUD-11]). Dopo `POST /admin/resume` la
|
||||
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.
|
||||
|
||||
### Riconciliazione
|
||||
|
||||
- **[AUD-30]** `pending_tx_recovered` — una transazione rimasta `building`
|
||||
@@ -330,42 +564,115 @@ flowchart LR
|
||||
ed è stata promossa a `pending`. Payload
|
||||
`{"pending_transaction_id", "kind", "txid"}`. `user_id` e `round_id`
|
||||
dipendono dal tipo: sono quelli della riga `pending_transactions` (per un
|
||||
pagamento `user_id` è vuoto, per un prelievo `round_id` è vuoto). Stessa
|
||||
transazione della promozione. Vedi
|
||||
[transazioni-e-riconciliazione.md](transazioni-e-riconciliazione.md).
|
||||
pagamento `user_id` è vuoto, per un prelievo `round_id` è vuoto). Nella
|
||||
stessa transazione della promozione si scrive anche l'evento che l'invio
|
||||
riuscito avrebbe scritto — `bet_placed` ([AUD-19]), `withdrawal_sent`
|
||||
([AUD-21]) o `payout_sent` ([AUD-29]) — con lo stesso payload
|
||||
([TX-16](transazioni-e-riconciliazione.md#riconciliazione)).
|
||||
*Codice:* `app/tx/reconcile.py:_promote` ·
|
||||
*Test:* nessun test verifica la riga di audit.
|
||||
*Stato:* da implementare (B-96), per il secondo evento · *Oggi:* si scrive
|
||||
solo `pending_tx_recovered`.
|
||||
|
||||
- **[AUD-31]** `pending_tx_abandoned` — una transazione `building` o `pending`
|
||||
non compare nella cronologia del proprio indirizzo ed è stata dichiarata
|
||||
persa: stato `failed`, UTXO liberati, partecipante rimosso o prelievo
|
||||
`failed`, `payout_txid` del round azzerato. Payload
|
||||
è stata dichiarata persa **con una prova**
|
||||
([TX-17](transazioni-e-riconciliazione.md#riconciliazione)): stato
|
||||
`failed`, UTXO liberati, partecipante rimosso o prelievo `failed`,
|
||||
`payout_txid` del round azzerato. Payload
|
||||
`{"pending_transaction_id", "kind", "txid", "reason", "utxos_released"}`, con
|
||||
`reason` oggi sempre `"not found on chain"` e `utxos_released` il numero di
|
||||
UTXO liberati (sempre 0 per un pagamento, perché l'indirizzo del montepremi
|
||||
non è tracciato in `utxo_events`). `user_id` e `round_id` come in [AUD-30].
|
||||
Stessa transazione dell'abbandono.
|
||||
`reason` uguale al `failure_reason` salvato sulla riga, che nomina la prova
|
||||
([modello-dati.md](modello-dati.md), DAT-36), e `utxos_released` il numero
|
||||
di UTXO liberati (sempre 0 per un pagamento, perché l'indirizzo del
|
||||
montepremi non è tracciato in `utxo_events`). `user_id` e `round_id` come
|
||||
in [AUD-30]. Stessa transazione dell'abbandono.
|
||||
*Codice:* `app/tx/reconcile.py:_abandon` ·
|
||||
*Test:* `tests/unit/test_reconcile.py`
|
||||
*Stato:* da implementare (B-78) · *Oggi:* basta che la cronologia del
|
||||
server attivo non contenga il txid corrente, e `reason` è sempre
|
||||
`"not found on chain"`.
|
||||
|
||||
- **[AUD-39]** `pending_tx_resurrected` — una riga `failed` da meno di 14
|
||||
giorni è risultata confermata: una transazione data per persa è arrivata in
|
||||
un blocco
|
||||
([TX-23](transazioni-e-riconciliazione.md#riconciliazione)). Payload
|
||||
`{"pending_transaction_id", "kind", "txid", "block_height"}`: `txid` è la
|
||||
versione confermata (una qualunque della catena di sostituzioni),
|
||||
`block_height` l'altezza del blocco che la contiene. `user_id` e
|
||||
`round_id` come in [AUD-30]. Una sola riga per transazione, perché la riga
|
||||
passa a `confirmed` e non viene più sorvegliata. Stessa transazione del
|
||||
passaggio a `confirmed`, della pausa e della riga `lottery_paused` con
|
||||
`reason = "resurrected_tx"` ([AUD-11]). Le righe collegate non vengono
|
||||
corrette: la procedura è in [operativita.md](operativita.md) (OPS-30).
|
||||
*Codice:* `app/tx/reconcile.py:reconcile_once` · *Test:* —
|
||||
*Stato:* da implementare (B-78) · *Oggi:* una riga `failed` non viene più
|
||||
guardata e l'evento non esiste.
|
||||
|
||||
- **[AUD-40]** `rbf_bump_failed` — un aumento di commissione non è riuscito
|
||||
([TX-24](transazioni-e-riconciliazione.md#rbf-accelerare-una-transazione-lenta)).
|
||||
Payload `{"pending_transaction_id", "kind", "txid", "reason", "message"}`:
|
||||
`txid` è il txid corrente della riga; `reason` è uno di
|
||||
`change_too_small` (il resto non assorbe l'aumento, TX-11),
|
||||
`max_fee_rate` (la riga è già a `MAX_FEE_RATE_SAT_VB`) o
|
||||
`replacement_rejected` (la rete ha rifiutato esplicitamente la
|
||||
sostituzione); `message` è il testo dell'errore, troncato a 200 caratteri.
|
||||
`user_id` e `round_id` come in [AUD-30]. Transazione propria ([AUD-03]);
|
||||
la riga `pending` non cambia. Si scrive **una volta per coppia (txid
|
||||
corrente, `reason`)**, come fissato da TX-24: prima di scrivere si cerca
|
||||
nel registro una riga uguale ([AUD-09]); un nuovo fallimento con la stessa
|
||||
coppia va solo nei log, mentre un nuovo txid (dopo un aumento riuscito) o un
|
||||
motivo diverso scrivono una riga nuova.
|
||||
*Codice:* `app/tx/broadcast.py:RbfBumper._tick`, `app/tx/broadcast.py:bump_fee` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-87) · *Oggi:* l'errore va solo nel log
|
||||
(`could not bump pending_transaction N`), ogni 30 secondi.
|
||||
|
||||
### Catena
|
||||
|
||||
- **[AUD-38]** `chain_reorg_detected` — il listener ha seguito una
|
||||
riorganizzazione della catena
|
||||
([ELC-24](connessione-electrum.md#riorganizzazioni)). Payload
|
||||
`{"depth", "fork_height", "old_tip_height", "old_tip_hash",
|
||||
"new_tip_height", "new_tip_hash"}`: numero dei nostri blocchi sostituiti
|
||||
(`old_tip_height − fork_height`), altezza del punto di biforcazione,
|
||||
altezza e hash della punta prima e dopo. `user_id` no, `round_id` no. Con
|
||||
`depth` 1 è solo osservativo (transazione propria, [AUD-03]); con `depth`
|
||||
≥ 2 è salvato nella stessa transazione della pausa e della riga
|
||||
`lottery_paused` con `reason = "reorg"` ([AUD-11]). Una biforcazione più
|
||||
profonda di 10 header non viene seguita e non scrive questo evento (la
|
||||
sessione si chiude). Nessuna logica rilegge la riga.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-85) · *Oggi:* nessuna riorganizzazione è
|
||||
rilevata; l'evento non esiste.
|
||||
|
||||
## Cosa non viene registrato
|
||||
|
||||
- **[AUD-32]** Questi fatti **non** producono una riga di audit; restano
|
||||
ricostruibili solo dallo stato delle tabelle o dal log applicativo:
|
||||
- apertura di un round;
|
||||
- passaggi di stato del round a `closing` e `drawing`, e la chiusura dopo la
|
||||
- passaggio di stato del round a `closing`, e la chiusura dopo la
|
||||
conferma del pagamento (`_on_payout_confirmed`);
|
||||
- conferma di una scommessa, di un prelievo o di un pagamento
|
||||
(`ConfirmationPoller`);
|
||||
- ogni aumento di commissione RBF e ogni RBF impossibile (solo log);
|
||||
- registrazione, login, login falliti e blocchi per troppi tentativi;
|
||||
- cambio password fatto dall'utente (`POST /users/me/change-password`);
|
||||
- ogni aumento di commissione RBF **riuscito** (solo il log
|
||||
`bumped <tipo> pending_transaction N`);
|
||||
- login, login falliti e blocchi per troppi tentativi, compresi i tentativi
|
||||
falliti di cambio password;
|
||||
- invio di una segnalazione di bug;
|
||||
- accesso in lettura al pannello admin (elenchi utenti, round, audit).
|
||||
|
||||
Escono da questo elenco, perché diventano eventi del catalogo: l'ingresso
|
||||
in `drawing` (`draw_committed`, [AUD-37]), ogni RBF impossibile o rifiutato
|
||||
(`rbf_bump_failed`, [AUD-40]), la registrazione (`user_registered`,
|
||||
[AUD-41]) e il cambio password riuscito (`password_changed`, [AUD-42]).
|
||||
*Codice:* `app/rounds/service.py:open_new_round_if_needed`,
|
||||
`app/rounds/scheduler.py:RoundScheduler._tick`,
|
||||
`app/rounds/confirmation.py:_on_payout_confirmed`,
|
||||
`app/tx/confirmation.py:poll_once`, `app/tx/broadcast.py:bump_fee`
|
||||
*Stato:* da implementare (B-73, B-87, B-92, B-94), per i quattro fatti che
|
||||
escono dall'elenco · *Oggi:* anche l'ingresso in `drawing`, gli RBF
|
||||
impossibili, la registrazione e il cambio password riuscito non producono
|
||||
alcuna riga.
|
||||
|
||||
## Log applicativo
|
||||
|
||||
@@ -383,13 +690,16 @@ va letto sul server.
|
||||
`./data/logs` dell'host.
|
||||
*Codice:* `app/logging_config.py:setup_logging`, `docker-compose.yml`
|
||||
|
||||
- **[AUD-34]** Il logger radice ha **solo** l'handler su file: i messaggi dei
|
||||
moduli dell'applicazione (scheduler, listener, riconciliatori) finiscono in
|
||||
`data/logs/app.log` e non sull'output del container. `docker compose logs app`
|
||||
mostra solo ciò che uvicorn scrive con la propria configurazione (avvio,
|
||||
richieste) e l'output di avvio del container (controllo della chiave,
|
||||
migrazioni Alembic).
|
||||
- **[AUD-34]** I messaggi dei moduli dell'applicazione (scheduler, listener,
|
||||
riconciliatori) finiscono in `data/logs/app.log` **e** sull'output del
|
||||
container, quindi `docker compose logs app` li mostra insieme all'output di
|
||||
avvio del container (controllo della chiave, migrazioni Alembic). La regola
|
||||
è di [ARC-09](architettura.md#log), che ne è il proprietario.
|
||||
*Codice:* `app/logging_config.py:setup_logging`
|
||||
*Stato:* da implementare (B-92) · *Oggi:* il logger radice ha solo
|
||||
l'handler su file: `docker compose logs app` mostra solo l'output di avvio
|
||||
del container e ciò che uvicorn scrive con la propria configurazione
|
||||
(avvio, richieste).
|
||||
|
||||
- **[AUD-35]** Un'eccezione non gestita in una richiesta HTTP viene registrata
|
||||
per intero nel log (`Unhandled error on <metodo> <percorso>`) e al client
|
||||
|
||||
Reference in new issue
Block a user