fee_address was the dangerous one (B-05). PUT /admin/config assigned whatever it
was given, and a well-formed address from another chain (bc1...) parses fine as a
witness program — so every round's 30% commission would be signed and broadcast
to a script nobody holds the key for. A malformed one instead wedged the payout
with an unhandled EmbitError. It now has to pass is_valid_plm_address, the same
check user withdrawals already had. Numeric fields got bounds too:
fee_rate_sat_vb=0 produces transactions no node relays, which stalls bets,
payouts and withdrawals alike, and round_duration_seconds=0 expires a round the
instant it opens.
Config changes are audit-logged (B-10). /pause and /resume were logged but a
config edit wasn't, so the most sensitive setting in the system could be changed
without leaving any trace — contradicting CLAUDE.md, which says audit_log records
what changed. The entry carries a before/after diff per field, computed before
assignment, and no-op updates write nothing. `paused` was removed from
_CONFIG_FIELDS so the maintenance switch has exactly one audited path; it stays
in the response model.
Admin token comparison is constant-time (B-14), with the empty-token check kept
*ahead* of it: compare_digest("", "") returns True, so the obvious ordering would
have opened the panel on any instance without an ADMIN_TOKEN.
Registration input (B-12). It accepted an empty username and a one-character
password while /users/me/change-password demanded 8 — an odd place to be lenient
on a custodial system holding real funds. MIN_PASSWORD_LENGTH moved to
auth/security.py so both share it, and the username is constrained to 3-32 chars
of [A-Za-z0-9_.-]. The IntegrityError handler also distinguishes a username
collision (answers username_taken) from a derivation-index one (retries): a
concurrent duplicate username used to be retried five times and then reported as
derivation_index_conflict, which told the user the wrong thing.
verify_password (B-13) catches VerificationError and InvalidHashError, not just
VerifyMismatchError, so an unparseable stored hash reads as "wrong password"
instead of a 500 — logged as an error, since that one is a data problem.
guida-admin.md gains a table of the audit events worth watching, including
payout_failed, which needs manual intervention.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
183 lines
10 KiB
Markdown
183 lines
10 KiB
Markdown
# Guida admin
|
|
|
|
Come gestire la configurazione operativa di PLM Lottery. Presuppone che il
|
|
server sia già avviato — vedi [running-the-server.md](running-the-server.md).
|
|
|
|
## Accesso
|
|
|
|
Il pannello admin è su **`https://<host>/admin`** — **non è collegato** da
|
|
nessun link nell'interfaccia utente (né in entrata né in uscita): ci si
|
|
arriva solo conoscendo l'URL. La pagina mostra solo un campo token finché non
|
|
accedi: non è protetta da login personale, ma da un **token condiviso**
|
|
(`ADMIN_TOKEN`, definito in `.env`).
|
|
|
|
Incolla il valore di `ADMIN_TOKEN` e premi "Accedi" (o Invio): se il token è
|
|
valido, si apre la dashboard con una navbar in alto e carica automaticamente
|
|
tutte le sezioni — nessun bottone "Carica" separato. Il token resta in
|
|
`sessionStorage` (si perde chiudendo la tab/il browser); "Esci" torna alla
|
|
sola schermata di login.
|
|
|
|
## Sezioni della dashboard
|
|
|
|
- **Parametri** — configurazione operativa (vedi tabella sotto)
|
|
- **Utenti** — elenco utenti, saldo, accesso alla chiave privata, reset password
|
|
- **Round** — storico round: stato, vincitore, importi, txid di payout
|
|
- **Transazioni pendenti** — bet/payout/prelievi non ancora confermati, candidati al fee-bump RBF
|
|
- **Audit log** — eventi registrati dal sistema (config cambiata, bet, payout, accessi a chiavi private, ecc.)
|
|
|
|
## Parametri
|
|
|
|
Tutti i parametri operativi/di business sono nella sezione "Parametri",
|
|
salvati nel database — modificabili in qualsiasi momento, effetto immediato,
|
|
**nessun riavvio del server necessario**. Non esiste alcuna variabile
|
|
d'ambiente equivalente: `.env` contiene solo segreti e configurazione di
|
|
infrastruttura (chiave master, JWT, Electrum, token admin), non parametri di
|
|
business — quelli si toccano solo da qui.
|
|
|
|
| Campo | Significato |
|
|
|---|---|
|
|
| **Fee address** | L'indirizzo PLM su cui finisce il 30% di ogni round (fee). Obbligatorio: i payout **non partono** se questo campo è vuoto. |
|
|
| **Bet amount (PLM)** | Il costo fisso d'ingresso per round. È anche l'importo minimo prelevabile: un prelievo sotto questa soglia viene rifiutato (i depositi non hanno un controllo minimo lato server). |
|
|
| **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 fino a 3 blocchi confermati in sequenza (ultima bet in sospeso, estrazione, payout — ~2 minuti l'uno), quindi l'animazione può durare più a lungo di questo valore, mai meno. |
|
|
| **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. |
|
|
|
|
Tutti gli importi in PLM vengono convertiti in sats (1 PLM = 100.000.000 sats)
|
|
solo nella chiamata API — il backend lavora sempre in sats.
|
|
|
|
Su un'istanza nuova (mai avviata), questi campi partono con dei default
|
|
hardcoded nel codice (`RoundConfig` in `app/db/models.py`: bet 10 PLM, round
|
|
10 minuti, cooldown 30s, animazione estrazione 20s, fee 1
|
|
sat/vB, RBF timeout 900s) — vanno
|
|
comunque rivisti e confermati dal pannello prima del primo utilizzo reale.
|
|
|
|
## Manutenzione (pausa/ripresa lotteria)
|
|
|
|
In cima alla sezione "Parametri" c'è una card "Manutenzione" con un pulsante
|
|
per fermare l'apertura di nuovi round — utile per intervenire sul server
|
|
(aggiornamenti, riavvii) senza lasciare gli utenti a metà di un round o
|
|
sorprenderli con un'interruzione improvvisa.
|
|
|
|
- **"Interrompi dopo questo round"**: il round eventualmente in corso viene
|
|
**completato normalmente** — chiude, estrae il vincitore da un blocco
|
|
confermato, e paga il 70/30 come sempre. Solo l'apertura del **round
|
|
successivo** viene sospesa. Gli utenti vedono un avviso di manutenzione
|
|
sulla loro dashboard (e sulla home, anche da sloggati) finché la lotteria
|
|
resta in pausa.
|
|
- **"Riprendi lotteria"**: annulla la pausa — al prossimo giro dello
|
|
scheduler (ogni 5 secondi) un nuovo round si apre normalmente (rispettando
|
|
comunque il cooldown se il precedente si è appena chiuso).
|
|
|
|
Ogni pausa/ripresa viene registrata nell'audit log (`lottery_paused` /
|
|
`lottery_resumed`), ma — come per il resto del pannello — non registra
|
|
*quale* operatore l'ha premuta (token condiviso, vedi limiti noti in
|
|
[CLAUDE.md](../CLAUDE.md)).
|
|
|
|
**Chi paga il fee-bump RBF?** Quando una bet, un payout o un prelievo resta
|
|
troppo a lungo senza conferma (oltre il "Timeout prima del fee-bump RBF"), il
|
|
sistema lo ritrasmette con una fee più alta. Il costo aggiuntivo lo assorbe
|
|
sempre **chi ha originato la transazione**, non il destinatario: per bet e
|
|
prelievi è l'utente stesso (gli torna un resto più piccolo), per i payout è
|
|
il pool (il resto che torna all'indirizzo pool si riduce) — la quota del
|
|
vincitore e quella delle fee, già fissate, non vengono mai toccate. Se non
|
|
c'è un resto abbastanza grande da assorbire l'aumento, il bump fallisce e
|
|
resta un intervento manuale (vedi "Limiti noti").
|
|
|
|
## Round
|
|
|
|
La sezione "Round" mostra lo storico (`GET /admin/rounds`, ultimi 50 per
|
|
default): id, stato, orario di apertura, vincitore (username), importo del
|
|
pool, importo vinto, importo di fee, txid del payout — tutti in PLM salvo il
|
|
txid.
|
|
|
|
## Transazioni pendenti
|
|
|
|
`GET /admin/pending-transactions` elenca bet, payout e prelievi ancora senza
|
|
conferma: tipo, stato, txid corrente, fee rate usata, numero di tentativi
|
|
(si incrementa a ogni bump RBF) e orario dell'ultima trasmissione. Una riga
|
|
che resta qui a lungo, con `attempt_count` che sale, indica una transazione
|
|
in difficoltà — vedi "Limiti noti" sul fallback RBF.
|
|
|
|
## Audit log
|
|
|
|
`GET /admin/audit-log` elenca gli ultimi 200 eventi registrati dal sistema
|
|
(tipo evento, payload JSON, utente/round coinvolti, timestamp): bet
|
|
piazzate, payout inviati, round chiusi, configurazione modificata, accessi
|
|
alle chiavi private, ecc. È il primo posto da controllare per ricostruire
|
|
cosa è successo dopo un problema.
|
|
|
|
Eventi a cui vale la pena prestare attenzione:
|
|
|
|
| Evento | Significato |
|
|
|---|---|
|
|
| `config_updated` | Un parametro è stato modificato; il payload contiene valore precedente e nuovo per ogni campo cambiato. |
|
|
| `bet_broadcast_failed` / `withdrawal_broadcast_failed` | La rete ha rifiutato la transazione. Non è stato speso nulla: gli UTXO sono stati liberati e il saldo dell'utente è tornato come prima. |
|
|
| `pending_tx_abandoned` | Una transazione trasmessa è scomparsa dalla catena e il sistema l'ha dichiarata persa: UTXO liberati, bet rimossa o prelievo segnato `failed`. Se capita spesso, la fee rate configurata è probabilmente troppo bassa. |
|
|
| `pending_tx_recovered` | Una transazione che si credeva incompleta è invece finita in catena (tipicamente dopo un riavvio a metà invio) e il sistema l'ha ripresa da sé. |
|
|
| `payout_failed` | Il payout di un round non è partito. Il round resta in `paying_out` e **richiede intervento manuale**: non esiste un retry automatico. Controlla `fee_address`, il saldo dell'indirizzo pool e la connessione Electrum. |
|
|
|
|
## Alternative all'interfaccia grafica
|
|
|
|
Le stesse operazioni si possono fare da terminale o da Swagger UI
|
|
(`https://<host>/docs`, sezione `admin`), sempre passando `ADMIN_TOKEN`
|
|
nell'header `X-Admin-Token`:
|
|
|
|
```bash
|
|
# leggere la configurazione
|
|
curl https://<host>/admin/config -H "X-Admin-Token: <ADMIN_TOKEN>"
|
|
|
|
# aggiornarla (importi in sats: 10 PLM = 1000000000; solo i campi passati vengono cambiati)
|
|
curl -X PUT https://<host>/admin/config \
|
|
-H "X-Admin-Token: <ADMIN_TOKEN>" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"fee_address": "plm1q...", "bet_amount_sats": 1000000000, "round_duration_seconds": 600}'
|
|
```
|
|
|
|
I valori vengono validati: `fee_address` deve essere un indirizzo bech32 PLM
|
|
valido (`plm1...`) e i parametri numerici hanno limiti di buon senso
|
|
(`fee_rate_sat_vb` almeno 1, durata round almeno 30s, ecc.). Un valore fuori
|
|
range viene rifiutato con un errore 422 e la configurazione resta invariata.
|
|
Il controllo su `fee_address` è deliberatamente severo: un indirizzo di
|
|
un'altra catena (per esempio `bc1...`) sarebbe formalmente valido come witness
|
|
program, e il 30% di commissione di ogni round finirebbe su uno script di cui
|
|
nessuno ha la chiave.
|
|
|
|
```bash
|
|
```
|
|
|
|
## Utenti, chiave privata e reset password
|
|
|
|
La card "Utenti" elenca id, username, indirizzo e saldo di ogni utente
|
|
registrato. Il bottone "Mostra" su ogni riga rivela la chiave privata (WIF)
|
|
di quell'utente, dietro conferma esplicita — serve per interventi manuali
|
|
(es. restituire fondi bloccati). **Ogni visualizzazione viene registrata
|
|
nell'audit log** (`admin_privkey_accessed`). Questo non introduce una nuova
|
|
falla: il server è già custodial, la chiave master da cui derivano tutte le
|
|
chiavi utente vive sul server — questo pannello espone solo qualcosa che
|
|
l'operatore può già fare via script.
|
|
|
|
Il bottone "Reset" nella colonna "Password" genera una **nuova password
|
|
casuale** per l'utente e sovrascrive quella esistente — mostrata una sola
|
|
volta nel pannello, così puoi comunicarla a chi ti ha chiesto aiuto perché
|
|
l'ha dimenticata. Non è un "recupero": le password sono salvate solo come
|
|
hash Argon2 (`app/auth/security.py`), quindi quella vecchia **non è mai
|
|
recuperabile** né per l'admin né per il codice stesso — l'unica opzione è
|
|
sempre sostituirla con una nuova. Anche questa azione è audit-loggata
|
|
(`admin_password_reset`) e non esiste alcun flusso self-service equivalente
|
|
per l'utente: solo un admin col token può farlo.
|
|
|
|
## Limiti noti
|
|
|
|
- Il token è unico e condiviso: non c'è identità per singolo admin né audit
|
|
di chi ha cambiato cosa (oltre alla tabella `audit_log` generica).
|
|
- Nessun rate limiting sugli endpoint admin (né su registrazione/bet/
|
|
prelievo utente).
|
|
- Se un payout fallisce (es. Electrum disconnesso, UTXO insufficienti), il
|
|
round resta bloccato in `paying_out` senza retry automatico — richiede
|
|
intervento manuale.
|
|
|
|
Per l'elenco completo dei gap noti vedi la sezione "Known gaps / TODO" in
|
|
`CLAUDE.md`.
|