Files
plm-lottery/docs/setup.md
T
davideandClaude Opus 5 7c4e9983ea Survive a dropped Electrum connection, and fall back to other servers
A dropped connection used to hang the whole platform permanently, and three
defects composed to do it (BUGS.md B-01):

The read loop's death was invisible. When the socket closed, _read_loop broke out
and finished, but _run_once was blocked on gather() over two notification
consumers waiting on queues nobody would ever fill again — it never returned and
never raised, so the reconnect-with-backoff logic was unreachable.
client.wait_closed() now resolves when the loop ends for any reason, and
_run_once races it against the consumers and a keepalive with
asyncio.wait(FIRST_COMPLETED).

Nothing had a timeout. request() registered a future, wrote to a half-closed
socket (drain() often doesn't raise) and awaited a reply that would never come.
That hung a POST /bets *while holding the per-user lock*, and could stop the
confirmation poller for good. Every request is now bounded at 15s, and a timeout
tears the connection down rather than leaving a server that owes us a reply in
rotation.

There was no keepalive, so on a quiet instance the normal way this connection
dies is an idle-timeout drop by the server (~10 minutes for many). A server.ping
every 60s makes that observable within a minute.

listener.client is also cleared before reconnecting, so callers stop treating a
dead connection as live.

On top of the finding, the listener now rotates over a list of servers:
ELECTRUM_FALLBACK_SERVERS holds comma-separated host:port[:notls] extras, tried
after the primary. Everything the platform does goes through this one connection
— deposit credits, broadcasts, confirmations, the chain tip the draw waits on —
which made a single hardcoded server its biggest point of failure. A failed or
dropped session moves to the next server immediately and only sleeps on the
backoff once every server has had a turn, so one dead server costs one attempt
instead of an outage, while a genuinely offline network still backs off. A
malformed entry fails at startup, not during the outage when the fallback is what
you need.

Also fixes B-19: header handling refuses a height below the current tip and
applies height and hex together, since _wait_for_next_block waits for
tip_height > tip_at_close (a regression silently added a block to the draw's
wait) and that hex is the draw's entropy source, so a mismatched pair would be
worse than a stale one.

Verified in the live deployment: the log shows the endpoint list, then "Electrum
connected to santantonio.sytes.net:50002", and the connection holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 00:34:59 +02:00

5.7 KiB

Setup

Passaggi da eseguire una tantum per preparare un'istanza di PLM Lottery, prima di poterla avviare (in locale o via Docker). Per come avviarla poi ogni volta, vedi running-the-server.md.

1. Prerequisiti

  • Python 3.12+ (serve solo per il workflow locale/venv — puoi saltarlo se usi solo Docker)
  • Docker + Docker Compose (serve solo per il workflow a container)
  • Un server Electrum raggiungibile per la rete PLM. Il server di bootstrap per lo sviluppo è santantonio.sytes.net:50002 (SSL) — va bene per i test, ma in produzione conviene usarne uno di cui ci si fida o gestirne uno proprio.
  • Consigliato in produzione: più di un server. Tutto passa da questa singola connessione (accredito depositi, invio transazioni, conferme, altezza della catena su cui si basa l'estrazione), quindi un solo server è il principale punto di rottura della piattaforma. Elencane altri in ELECTRUM_FALLBACK_SERVERS (vedi sotto): l'app li prova a rotazione, così un server irraggiungibile costa un solo tentativo di riconnessione invece di un disservizio.

2. Creare il file .env

Copia .env.example in .env e compila i segreti. Ogni valore sotto viene generato una volta e non cambia più (ruotarlo invalida sessioni/dati cifrati esistenti):

cp .env.example .env
Variabile Scopo Come generarla
XPRV_ENCRYPTION_KEY Chiave simmetrica che cifra a riposo la master xprv del server. Perdere questa chiave significa perdere per sempre l'accesso ai fondi di tutti gli utenti. python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
JWT_SECRET Firma i token di sessione degli utenti. python -c "import secrets; print(secrets.token_urlsafe(32))"
ADMIN_TOKEN Token bearer richiesto sugli endpoint admin (header X-Admin-Token). python -c "import secrets; print(secrets.token_urlsafe(32))"

Le altre chiavi di .env (DATABASE_URL, ELECTRUM_HOST/PORT/USE_SSL, MASTER_KEY_PATH) hanno default sensati in .env.example.

ELECTRUM_FALLBACK_SERVERS elenca i server di riserva, separati da virgola, nel formato host:porta (TLS, il caso normale) oppure host:porta:notls. Esempio:

ELECTRUM_FALLBACK_SERVERS=nodo2.example.net:50002,nodo3.example.net:50001:notls

Vengono provati a rotazione dopo il primario. Attenzione: un valore scritto male blocca l'avvio dell'app — è voluto, meglio accorgersene subito che durante il disservizio in cui il fallback serve davvero.

JWT_SECRET e XPRV_ENCRYPTION_KEY vengono verificati all'avvio: se sono vuoti (o JWT_SECRET è più corto di 32 caratteri) il container si rifiuta di partire con un errore esplicito, invece di avviarsi e rompersi al primo login. Nota: .env contiene solo segreti e configurazione di infrastruttura — i parametri di business (bet amount, durata round, fee, ecc.) si configurano dal pannello admin dopo l'avvio, non qui — vedi guida-admin.md.

Non committare mai .env. È già escluso da .gitignore.

3. Generare la master key

Il server deriva l'indirizzo di deposito di ogni utente (e l'indirizzo pool) da un'unica master xprv, generata una volta e cifrata a riposo con XPRV_ENCRYPTION_KEY. Questo passaggio va eseguito esattamente una volta per ogni deployment, dopo aver impostato XPRV_ENCRYPTION_KEY in .env:

  • Locale/venv: PYTHONPATH=. python scripts/generate_master_key.py
  • Docker: docker compose run --rm app python scripts/generate_master_key.py

Questo scrive un file cifrato (MASTER_KEY_PATH, default ./master.xprv.enc in locale o ./data/keys/master.xprv.enc con Docker). Fai il backup di questo file insieme a XPRV_ENCRYPTION_KEY — uno dei due da solo è inutile, ma perderli entrambi insieme significa perdere i fondi di tutti gli utenti senza possibilità di recupero.

Recuperare o portare una xprv esistente

Due script, entrambi manuali/una tantum, per lo scenario di disaster recovery o per usare una xprv generata altrove (es. offline/air-gapped) invece di farla generare al server:

  • scripts/decrypt_master_key.py: decifra e stampa a schermo la xprv già presente in MASTER_KEY_PATH (con fallback automatico su ./data/keys/master.xprv.enc se il path di .env non esiste in locale). Chiede conferma esplicita prima di stampare.
  • scripts/encrypt_master_key.py: cifra una xprv esterna e la scrive in MASTER_KEY_PATH con lo stesso identico schema (Fernet + XPRV_ENCRYPTION_KEY) usato da generate_master_key.py. La xprv va incollata con input nascosto (non appare a schermo). Si rifiuta di sovrascrivere un file esistente a meno di passare --overwrite.

Entrambi vanno eseguiti localmente (o dentro il container via docker compose run --rm app ...), mai esposti da un endpoint API o dal pannello admin: chi ottiene questa xprv ottiene il controllo dei fondi di tutti gli utenti e del pool.

4. Installare le dipendenze (solo workflow locale/venv)

Salta questo passaggio se usi solo Docker — l'immagine installa le proprie dipendenze durante la build.

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

5. Applicare le migrazioni del database

  • Locale/venv: alembic upgrade head
  • Docker: le migrazioni vengono eseguite automaticamente all'avvio del container (vedi il CMD del Dockerfile) — nessun passaggio manuale.

6. Impostare l'indirizzo delle fee

Prima che il primo round possa pagare, un admin deve impostare fee_address tramite il pannello admin o l'API — vedi guida-admin.md. I payout si rifiutano di partire finché non è impostato.

A questo punto l'istanza è pronta per essere avviata — continua con running-the-server.md.