# Connessione alla rete PLM (Electrum) Il server non gestisce un nodo completo: parla con la rete PLM esclusivamente tramite il **protocollo Electrum**, con **una sola connessione** alla volta. Da quella connessione passa tutto — accredito dei depositi, invio delle transazioni, conferme, altezza della catena su cui attende l'estrazione — per cui è sia il principale punto di rottura sia, se il server dall'altra parte fosse ostile, il principale rischio per l'integrità. I requisiti usano il prefisso `ELC`, secondo la convenzione di [processo-di-modifica.md](processo-di-modifica.md). La configurazione (`.env`) è descritta in [setup.md](setup.md), i rischi accettati in [sicurezza.md](sicurezza.md) e [decisioni-e-limiti.md](decisioni-e-limiti.md). Le difese sono cinque. ## 1. Rotazione tra server `ELECTRUM_HOST`/`ELECTRUM_PORT` è il server principale; `ELECTRUM_FALLBACK_SERVERS` è un elenco di riserva nel formato `host:porta[:notls]`, separato da virgole. - **[ELC-01]** L'elenco dei server è il principale (con TLS secondo `ELECTRUM_USE_SSL`) seguito da quelli di riserva. Ogni voce di riserva è `host:porta` (TLS) oppure `host:porta:`, con `` tra `ssl`/`tls` (TLS) e `notls`/`plain` (in chiaro); le voci vuote sono ignorate e i duplicati eliminati mantenendo la prima posizione. Una voce malformata blocca l'avvio subito, non durante il guasto in cui servirebbe. *Codice:* `app/electrum/client.py:parse_endpoints` · *Test:* `tests/unit/test_electrum_client.py` - **[ELC-02]** Dopo ogni sessione fallita o caduta si prova **subito** il server successivo nell'elenco, a rotazione; l'attesa crescente (da 1s raddoppiando fino a 30s) scatta solo dopo che ogni server ha fallito una volta di seguito. Una sessione che si era stabilita azzera l'attesa. Senza alcun server configurato il listener si ferma con un errore nei log. *Codice:* `app/electrum/listener.py:ElectrumListener.run` · *Test:* `tests/unit/test_electrum_listener.py` - **[ELC-03]** Con TLS la connessione è cifrata ma il certificato del server **non viene verificato** (i server Electrum usano spesso certificati autofirmati): la fiducia non viene dal TLS ma dal quorum ([§ 5](#5-il-quorum)). Ogni connessione si apre con la stretta di mano `server.version`. *Codice:* `app/electrum/client.py:ElectrumClient.connect` ## 2. Richieste con limite di tempo - **[ELC-04]** Ogni richiesta ha un limite di 15 secondi (`_REQUEST_TIMEOUT_SECONDS`); allo scadere la connessione viene chiusa, la sessione termina e si passa al server successivo. Quando la connessione cade, tutte le richieste in attesa falliscono subito invece di restare sospese. Senza questo limite una risposta mai arrivata poteva bloccare una scommessa (con il blocco per-utente acquisito) o fermare per sempre il controllo delle conferme. *Codice:* `app/electrum/client.py:ElectrumClient.request`, `app/electrum/client.py:ElectrumClient._read_loop` · *Test:* `tests/unit/test_electrum_client.py` ## 3. Caduta rilevata - **[ELC-05]** La connessione viene pingata (`server.ping`) ogni 60 secondi (`_PING_INTERVAL_SECONDS`); un ping fallito chiude la sessione. *Codice:* `app/electrum/listener.py:ElectrumListener._keepalive` - **[ELC-06]** La chiusura della connessione è un evento osservato: la sessione termina non appena la connessione si chiude, il ping fallisce o l'elaborazione degli header o delle notifiche degli indirizzi si interrompe con un errore. Il listener non resta quindi in attesa di notifiche che non arriveranno mai mentre sembra ancora connesso; a sessione finita il client non è più disponibile. *Codice:* `app/electrum/listener.py:ElectrumListener._run_once`, `app/electrum/client.py:ElectrumClient.wait_closed` · *Test:* `tests/unit/test_electrum_client.py` - **[ELC-07]** Una connessione viene resa disponibile al resto del sistema **solo dopo** che il primo header è stato applicato: se il client c'è, l'altezza della catena è nota. Se il primo header è inutilizzabile (privo del contenuto binario) e non si conosce ancora alcuna altezza, la sessione viene rifiutata e si passa al server successivo. Chi trova il client assente (ad esempio durante una riconnessione) salta semplicemente quel giro, e scommesse e prelievi rispondono `network_unavailable`. *Codice:* `app/electrum/listener.py:ElectrumListener._run_once` · *Test:* `tests/unit/test_electrum_listener.py` ## 4. Header validati, non creduti Ogni nuovo header di blocco — quello ricevuto all'iscrizione e ogni successivo annunciato dal server — viene controllato prima di essere accettato. Tre casi vengono semplicemente **ignorati**, con un avviso nei log e senza chiudere la sessione, perché non indicano un server ostile: - **[ELC-08]** un header per un'altezza **inferiore** a quella nota: l'altezza non torna mai indietro; - **[ELC-09]** un header per l'altezza **già nota** (lo stesso blocco riannunciato, una riorganizzazione in punta, o un server in disaccordo): l'hash già adottato per quell'altezza non viene mai sostituito; se fosse quello sbagliato, è la corroborazione dell'estrazione a scartarlo; - **[ELC-10]** un header privo del contenuto binario: non c'è nulla da validare né da usare per l'estrazione. *Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · *Test:* `tests/unit/test_electrum_listener.py` Due controlli invece, se falliscono, **chiudono la sessione** e fanno passare a un altro server: quell'header è l'unica fonte di casualità dell'estrazione, e uno falsificato sceglierebbe il vincitore. - **[ELC-11]** L'header deve essere lungo 80 byte e il suo hash deve rispettare la difficoltà che l'header stesso dichiara (campo `bits`). *Codice:* `app/rounds/draw.py:header_meets_its_own_target`, `app/electrum/listener.py:ElectrumListener._apply_header` · *Test:* `tests/unit/test_draw.py`, `tests/unit/test_electrum_listener.py` - **[ELC-12]** Un avanzamento di **un solo** blocco deve agganciarsi all'hash del blocco corrente. Per un salto di più blocchi questo controllo non è possibile senza la catena intera, e vale solo quello di difficoltà. *Codice:* `app/rounds/draw.py:header_prev_hash`, `app/electrum/listener.py:ElectrumListener._apply_header` · *Test:* `tests/unit/test_draw.py`, `tests/unit/test_electrum_listener.py` - **[ELC-13]** Un header accettato aggiorna insieme altezza e contenuto della punta (mai uno senza l'altro). Ogni notifica di nuovi header invia una notifica SSE, perché è proprio ciò che l'estrazione attende. *Codice:* `app/electrum/listener.py:ElectrumListener._apply_header`, `app/electrum/listener.py:ElectrumListener._consume_headers` · *Test:* `tests/unit/test_electrum_listener.py` Il controllo di difficoltà respinge un header mai minato, ma non uno minato davvero a una difficoltà dichiarata irrealisticamente bassa: per questo il blocco dell'estrazione viene anche corroborato ([ELC-15]). ## 5. Il quorum Ogni decisione che sposta denaro viene confermata dagli **altri** server configurati, mai da quello attivo (che è proprio quello che un attaccante controllerebbe). - **[ELC-14]** La corroborazione interroga in parallelo tutti i server configurati tranne quello attivo, ciascuno con una connessione nuova e un limite di 10 secondi (`_CORROBORATION_TIMEOUT_SECONDS`) per collegarsi e 10 per rispondere. Passa se concorda la **maggioranza stretta dei server che hanno risposto**; un server che non risponde o dà errore non conta. - **Nessun server di riserva configurato** → la verifica passa. È il rischio accettato di un `ELECTRUM_FALLBACK_SERVERS` vuoto: in produzione conviene configurarne più d'uno (vedi [setup.md](setup.md)). - **Nessuno risponde** → la verifica **fallisce**: una rete irraggiungibile non prova nulla. *Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority` · *Test:* `tests/unit/test_electrum_listener.py` | ID | Prima di… | Si verifica che… | |---|---|---| | **[ELC-15]** | usare un blocco per l'estrazione | l'header che l'altro server restituisce per quell'altezza (`blockchain.block.header`) abbia lo stesso hash | | **[ELC-16]** | togliere dal saldo una moneta sparita | l'altro server non elenchi più la moneta tra quelle non spese | | **[ELC-17]** | accreditare un deposito | l'altro server elenchi la moneta tra quelle non spese, con lo stesso importo e confermata (altezza maggiore di 0) | *Codice:* `app/electrum/listener.py:ElectrumListener.corroborate_header`, `app/electrum/listener.py:ElectrumListener.corroborate_utxo_spent`, `app/electrum/listener.py:ElectrumListener.corroborate_utxo_credit` · *Test:* `tests/unit/test_electrum_listener.py` I saldi si muovono in entrambe le direzioni, quindi entrambe richiedono lo stesso quorum. Le conseguenze di una verifica non riuscita: - un accredito non verificato è solo **rimandato**, non perso: la moneta viene riproposta al controllo successivo ([DEP-04](registrazione-e-depositi.md#deposito)); - una moneta sparita non verificata resta nel saldo ([DEP-07](registrazione-e-depositi.md#deposito)); - un blocco non verificato fa attendere il blocco seguente, e lo si registra in audit (`draw_header_corroboration_failed`, [RND-16](ciclo-di-vita-del-round.md#3--estrazione)). Il reintegro di una moneta ricomparsa ([DEP-08](registrazione-e-depositi.md#deposito)) e la lettura delle monete del montepremi per il pagamento si basano invece sul solo server attivo. ## Riconnessione - **[ELC-18]** Alla riconnessione il listener si reiscrive agli indirizzi di tutti gli utenti (letti dal database) e ne aggiorna le monete, in un'attività separata, al massimo 20 alla volta (`_RESUBSCRIBE_CONCURRENCY`): in questo modo l'altezza della catena — e con essa un'eventuale estrazione in corso — continua ad aggiornarsi durante la reiscrizione. Un utente la cui reiscrizione fallisce viene registrato nei log e saltato; lo riprende il giro periodico di `DepositReconciler` ([DEP-10](registrazione-e-depositi.md#dep-10)). Se la sessione termina, la reiscrizione in corso viene interrotta. *Codice:* `app/electrum/listener.py:ElectrumListener._subscribe_all_users`, `app/electrum/listener.py:ElectrumListener._run_once` · *Test:* `tests/unit/test_electrum_listener.py` Lo stato della connessione e l'altezza corrente sono visibili nella barra di stato della catena di `/` (da `chain_tip_height` di `GET /rounds/current`) e nell'indicatore Electrum di `/admin`.