Files
plm-lottery/docs/connessione-electrum.md
T
davideandClaude Sonnet 5.5 54dbffb552 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>
2026-10-16 14:25:41 +02:00

23 KiB
Raw Blame History

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. La configurazione (.env) è descritta in setup.md, i rischi accettati in sicurezza.md e limiti-noti.md. Questo documento descrive il comportamento atteso dopo la revisione del 2026-09-28; i requisiti che il codice non soddisfa ancora sono marcati da implementare (vedi piano-correzioni.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[:<modo>], 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), host:porta:<modo> con <modo> tra ssl/tls (TLS) e notls/plain (in chiaro), oppure host:porta:pin=<sha256> (TLS con certificato fissato, [ELC-03]); <sha256> sono 64 cifre esadecimali (maiuscole accettate, conservate in minuscolo); pin= può seguire anche <modo> con TLS, mai notls/plain (non c'è un certificato da fissare). 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. Il certificato non è verificato contro un'autorità (i server Electrum usano spesso certificati autofirmati), ma può essere fissato: per una voce con pin=<sha256> ([ELC-01]), e per il server principale con ELECTRUM_CERT_PIN, lo SHA-256 del certificato del server in formato DER deve coincidere con il valore configurato, altrimenti la connessione fallisce come un server irraggiungibile e si passa al successivo ([ELC-02]). Senza pin la fiducia non viene dal TLS ma dal quorum (§ 5). Ogni connessione si apre con la stretta di mano server.version. Codice: app/electrum/client.py:ElectrumClient.connect, app/electrum/client.py:ElectrumClient._check_pin, app/main.py:_make_electrum_client · Test: tests/unit/test_electrum_client.py
  • [ELC-19] Il server rifiuta di avviarsi se, dopo l'eliminazione dei duplicati, l'elenco contiene meno di 2 server di riserva oltre al principale: con meno di tre server il quorum di [ELC-14] non può mai avere due risposte indipendenti da quella del server attivo. L'unica eccezione è ALLOW_NO_QUORUM=true, solo per lo sviluppo: il server parte, scrive un avviso nei log a ogni avvio, e le corroborazioni seguono la regola ridotta di [ELC-14]. Codice: app/config.py:validate_quorum_servers, app/main.py:lifespan · Test: tests/unit/test_electrum_listener.py

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
  • [ELC-20] Una riga di risposta può essere lunga fino a 16 MiB (16 777 216 byte). Una riga più lunga, o una riga che non è JSON valido, è un errore della richiesta, non della connessione: la riga viene scartata fino al suo a capo e la connessione resta aperta. La richiesta a cui rispondeva fallisce subito con un errore se il suo id si legge nei primi o negli ultimi 256 byte della riga; altrimenti fallisce allo scadere di [ELC-04]. Codice: app/electrum/client.py:ElectrumClient.connect, 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 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. Un errore nell'aggiornamento di un utente dopo una notifica del suo indirizzo non termina la sessione (DEP-16). Codice: app/electrum/listener.py:ElectrumListener._run_once, app/electrum/client.py:ElectrumClient.wait_closed, app/electrum/listener.py:ElectrumListener._consume_scripthash · Test: tests/unit/test_electrum_client.py Stato: da implementare (B-96), per gli errori di aggiornamento di un utente · Oggi: qualunque eccezione in refresh_user chiamato da _consume_scripthash (ad esempio la collisione sull'outpoint di due aggiornamenti concorrenti) termina la sessione.
  • [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
  • [ELC-21] Server fermo. Un server può restare connesso e rispondere ai ping senza più seguire la catena. A ogni ping di [ELC-05], se l'ultimo header accettato ha più di 360 secondi, il listener chiede la punta (blockchain.headers.subscribe) a ciascuno degli altri server configurati, con i tempi di [ELC-14]; se almeno uno riporta un'altezza maggiore di quella nota, la sessione termina e si passa al server successivo nell'elenco ([ELC-02]). Se nessuno riporta un'altezza maggiore (la rete non ha prodotto blocchi) la sessione continua. Codice: app/electrum/listener.py:ElectrumListener._keepalive, app/electrum/listener.py:ElectrumListener._check_not_stalled · 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. Il listener tiene gli ultimi 10 header accettati ([ELC-22]): è ciò che gli permette di riconoscere una riorganizzazione e seguirla invece di scambiarla per un server ostile.

flowchart TB
    A["Header annunciato<br/>all'altezza H"] --> B{"Ha il contenuto<br/>binario?"}
    B -- "No" --> I1["Ignorato (ELC-10)"]
    B -- "Sì" --> C{"H rispetto alla<br/>punta T"}
    C -- "H < T" --> I2["Ignorato (ELC-08)"]
    C -- "H = T, stesso hash" --> I3["Ignorato (ELC-09)"]
    C -- "H = T, hash diverso" --> F
    C -- "H > T" --> D["Si scaricano gli header<br/>T+1 … H−1 (ELC-12)"]
    D --> E{"80 byte, bits non più<br/>facili del powLimit, hash<br/>entro il bersaglio? (ELC-11)"}
    E -- "No" --> X["HeaderValidationError:<br/>sessione chiusa,<br/>server successivo"]
    E -- "Sì" --> G{"T+1 si aggancia<br/>alla punta T?"}
    G -- "Sì, e la catena<br/>è collegata fino a H" --> OK["Punta = H<br/>finestra aggiornata (ELC-13)"]
    G -- "No" --> F["Ricerca del punto di<br/>biforcazione negli ultimi<br/>10 header (ELC-23)"]
    F -- "Trovato" --> R["Si segue il nuovo ramo<br/>chain_reorg_detected;<br/>pausa se profondità ≥ 2 (ELC-24)"]
    F -- "Non trovato" --> X

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. Un server rimasto indietro non sposta la punta, e un ramo più corto del nostro non viene seguito. Il rovescio va detto: se un header falso a un'altezza molto alta venisse accettato, nessun header onesto (più basso) lo sostituirebbe più; la punta resta bloccata fino al riavvio del processo (l'altezza è solo in memoria). Con [ELC-11] e [ELC-12] quell'header deve portarsi dietro una catena collegata di header con lavoro valido; e comunque non sceglie il vincitore, perché il blocco di estrazione è letto a un'altezza fissa e confermato dal quorum ([ELC-15]): l'estrazione attende, e l'attesa è visibile (draw_stalled, GET /health/ready, [ELC-29]).
  • [ELC-09] un header per l'altezza già nota con lo stesso hash (lo stesso blocco riannunciato). Un header per l'altezza già nota con un hash diverso non è ignorato: è una riorganizzazione in punta e si segue secondo [ELC-23].
  • [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

I controlli seguenti invece, se falliscono, chiudono la sessione e fanno passare a un altro server.

  • [ELC-11] L'header deve essere lungo 80 byte; il bersaglio che dichiara (campo bits) non deve essere più facile del powLimit di PLM mainnet, 00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff (in forma compatta 0x1d00ffff, lo stesso di Bitcoin; fonte: palladiumcore/src/chainparams.cpp, consensus.powLimit della rete principale); e il suo hash deve rispettare quel bersaglio. Il controllo toglie la contraffazione gratuita, non quella economica: anche al powLimit un header costa in media 2³² hash, 43 µs per un ASIC da 100 TH/s. Ciò che protegge davvero l'estrazione è il quorum sul blocco ([ELC-15]). 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] Ogni avanzamento della punta deve essere collegato alla punta corrente. Un avanzamento di un blocco deve avere come prev_hash l'hash della punta. Per un avanzamento di più blocchi, dalla punta T a H, si scaricano dal server attivo gli header T+1 … H−1 (blockchain.block.headers, a blocchi di al più 2016 header per richiesta); ciascuno deve superare [ELC-11] e agganciarsi al precedente, e H all'ultimo. Un header scaricato che non supera [ELC-11], o una catena scaricata spezzata, chiude la sessione. Se invece è il primo passo (T+1, o H stesso per un avanzamento di un blocco) a non agganciarsi alla punta, non si chiude la sessione: è una possibile riorganizzazione e si segue [ELC-23]. Codice: app/rounds/draw.py:header_prev_hash, app/electrum/listener.py:ElectrumListener._apply_header, app/electrum/listener.py:ElectrumListener._fetch_headers, app/electrum/client.py:ElectrumClient.get_block_headers · 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) e la finestra di [ELC-22]. 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
  • [ELC-22] Il listener tiene in memoria gli ultimi 10 header accettati (altezza → contenuto), punta compresa. Il primo header del processo non ha nulla a cui agganciarsi ed è accettato con il solo [ELC-11]; subito dopo si scaricano i 9 header precedenti, che devono superare [ELC-11] ed essere collegati fra loro e al primo, altrimenti la sessione si chiude. La finestra sopravvive alle riconnessioni, non ai riavvii. Codice: app/electrum/listener.py:ElectrumListener._apply_header, app/electrum/listener.py:ElectrumListener._adopt_first_header, app/electrum/listener.py:ElectrumListener._extend_window · Test: tests/unit/test_electrum_listener.py

Riorganizzazioni

  • [ELC-23] Quando un header non si aggancia alla punta ([ELC-12]) o ne sostituisce l'hash alla stessa altezza ([ELC-09]), il listener cerca il punto di biforcazione: chiede al server attivo gli header delle altezze della finestra, dalla punta verso il basso, fino a 10, e prende la più alta altezza in cui l'hash del server coincide con il nostro. Se lo trova, il ramo del server dal punto di biforcazione fino al nuovo header deve superare [ELC-11] ed essere collegato; allora sostituisce la finestra sopra il punto di biforcazione e il nuovo header diventa la punta. Se non lo trova entro 10 header (una biforcazione più profonda, o un server su un'altra catena), la sessione si chiude come per [ELC-11]. Codice: app/electrum/listener.py:ElectrumListener._follow_reorg · Test: tests/unit/test_electrum_listener.py
  • [ELC-24] Ogni riorganizzazione seguita scrive l'evento chain_reorg_detected con la profondità (numero dei nostri blocchi sostituiti, cioè vecchia altezza della punta meno altezza del punto di biforcazione), l'altezza del punto di biforcazione, la vecchia e la nuova altezza della punta e i due hash di punta. Con profondità ≥ 2 la lotteria va in pausa (paused = true, paused_reason = 'reorg', vedi modello-dati.md B-85) finché l'operatore non la riprende: come ogni pausa, impedisce solo l'apertura del round successivo. Con profondità 1 si scrive solo l'evento. L'evento è scritto prima che la finestra si sposti: se la scrittura fallisce la sessione termina e la stessa biforcazione, annunciata dal server successivo, viene registrata allora (nessuna riorganizzazione è seguita senza traccia). Codice: app/electrum/listener.py:ElectrumListener._record_reorg, app/electrum/listener.py:ElectrumListener._follow_reorg, app/db/models.py:RoundConfig · Test: tests/unit/test_electrum_listener.py

5. Il quorum

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).

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 limite di 10 secondi (_CORROBORATION_TIMEOUT_SECONDS) per collegarsi e 10 per rispondere. Un server che non risponde o dà errore non conta. Passa se hanno risposto almeno 2 server e concorda la maggioranza stretta di quelli che hanno risposto; con meno di 2 risposte fallisce: una rete irraggiungibile, o un solo server raggiungibile, non prova nulla. Con ALLOW_NO_QUORUM=true ([ELC-19]) vale invece la regola ridotta: senza altri server configurati la verifica passa, altrimenti basta la maggioranza stretta delle risposte, anche di una sola. 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 ciascun server, attivo compreso, restituisce per l'altezza di estrazione (blockchain.block.header, 80 byte con lavoro valido) abbia lo stesso hash, con almeno 2 risposte dagli altri server e la maggioranza stretta di chi ha risposto; regole complete in ciclo-di-vita-del-round.md (B-73)
[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, a un'altezza che le dà almeno le conferme richieste (DEP-03) contate sulla nostra punta
[ELC-25] reintegrare una moneta data per spesa fuori l'altro server elenchi la moneta tra quelle non spese, con lo stesso importo e confermata (altezza maggiore di 0), come per un accredito
[ELC-26] segnare confermata una transazione l'altro server restituisca per il txid una prova merkle (blockchain.transaction.get_merkle) che, ricalcolata, dà la radice merkle dell'header di quell'altezza; l'header è quello della finestra ([ELC-22]) o, se più vecchio, quello su cui concorda il quorum come in [ELC-15]. Regole della conferma in transazioni-e-riconciliazione.md (B-83)
[ELC-27] costruire il pagamento di un round l'altro server elenchi come non spese le monete delle scommesse del round all'indirizzo del suo montepremi; regole in ciclo-di-vita-del-round.md (B-82)
[ELC-28] abbandonare una transazione l'altro server riporti un suo ingresso speso in un blocco da un txid diverso, oppure non conosca nessuno dei suoi txid; regole in transazioni-e-riconciliazione.md (B-78)

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, 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:

  • un accredito non verificato è solo rimandato, non perso: la moneta viene riproposta al controllo successivo (DEP-04);
  • una moneta sparita non verificata resta nel saldo (DEP-07), una moneta ricomparsa non verificata resta fuori (DEP-08);
  • una conferma non verificata lascia la transazione in attesa, e si riprova 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);
  • le monete del montepremi non verificate fanno terminare il tentativo di pagamento con payout_failed (pool verification unavailable), senza escludere nessun partecipante (RND-39). 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 inviata anche a ogni server di riserva, al meglio. La regola è in transazioni-e-riconciliazione.md (B-76, B-83).

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). 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

Stato della connessione

  • [ELC-29] Il listener espone lo stato della connessione: se è connesso, il server attivo, l'altezza della punta e da quanti secondi è stato accettato l'ultimo header. Lo leggono GET /health/ready (non pronto se non connesso o se l'ultimo header ha più di 360 secondi, api.md B-92) e l'indicatore Electrum di /admin. L'altezza corrente resta visibile anche nella barra di stato della catena di / (da chain_tip_height di GET /rounds/current). Codice: app/electrum/listener.py:ElectrumListener.current_endpoint, app/main.py:health · Test: — Stato: da implementare (B-86, B-92) · Oggi: /health risponde sempre ok; l'età dell'ultimo header non è registrata da nessuna parte e /admin mostra solo l'altezza.