Require a real Electrum quorum and allow certificate pinning (B-84, ELC-01, ELC-03, ELC-14, ELC-19, ARC-20, OPS-12)

An empty ELECTRUM_FALLBACK_SERVERS (the .env.example default) silently made every
corroboration pass, and with fallbacks the majority was taken over whoever
answered, so one reachable fallback decided alone. TLS certificates were never
checked.

- _corroborate_majority passes only with at least 2 answers (MIN_QUORUM_RESPONSES)
  and a strict majority of the answers; no other server configured, or fewer than
  2 answering, fails. ALLOW_NO_QUORUM=true keeps the old reduced rule.
- config.validate_quorum_servers, called from the lifespan after parse_endpoints,
  refuses to start with fewer than 2 fallback servers unless ALLOW_NO_QUORUM=true
  (which logs a warning at every start).
- Optional pinning: `host:port[:mode]:pin=<sha256>` per fallback and
  ELECTRUM_CERT_PIN for the primary; ElectrumClient compares the SHA-256 of the
  DER certificate after the TLS handshake and a mismatch fails like an unreachable
  server. A pin on a plaintext entry is a startup error.
- .env.example now ships the four bootstrap servers and documents both new
  variables; docs/setup.md, spec, storico and BUGS.md updated.
- Tests changed because the spec changed: the two "no other servers configured ->
  True" corroboration tests now assert that outcome only under allow_no_quorum.

Note: a dev .env with fewer than 2 fallback servers will now refuse to start.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-14 14:04:35 +02:00
1 parent fd6a91e93d
commit 13cb66cc06
18 files changed
+451 -125

No files matched your search

+7 -16
View File
@@ -27,15 +27,14 @@ Le difese sono cinque.
`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. Le
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`
*Stato:* da implementare (B-84), per la forma `pin=` · *Oggi:* il terzo
campo accetta solo `ssl`/`tls`/`notls`/`plain`; `pin=…` è una voce
malformata.
- **[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
@@ -52,9 +51,8 @@ Le difese sono cinque.
irraggiungibile e si passa al successivo ([ELC-02]). Senza pin 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` · *Test:* —
*Stato:* da implementare (B-84) · *Oggi:* nessun certificato viene mai
verificato; non esiste un modo di fissarlo.
*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
@@ -62,10 +60,8 @@ Le difese sono cinque.
`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_runtime_secrets`, `app/main.py:lifespan` · *Test:* —
*Stato:* da implementare (B-84) · *Oggi:* un `ELECTRUM_FALLBACK_SERVERS`
vuoto (il valore di `.env.example`) è accettato in silenzio e disattiva
ogni corroborazione.
*Codice:* `app/config.py:validate_quorum_servers`, `app/main.py:lifespan` ·
*Test:* `tests/unit/test_electrum_listener.py`
## 2. Richieste con limite di tempo
@@ -299,11 +295,6 @@ server attivo; sono le righe [ELC-26], [ELC-28], [ELC-25] e [ELC-27].
maggioranza stretta delle risposte, anche di una sola.
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority` ·
*Test:* `tests/unit/test_electrum_listener.py`
*Stato:* da implementare (B-84) · *Oggi:* vale per tutti la regola ridotta.
Senza server di riserva ogni verifica passa; con server di riserva la
maggioranza è calcolata su chi ha risposto, quindi **una risposta su una
decide**: se è raggiungibile un solo altro server, la sua parola basta.
Nessuna risposta fa fallire la verifica.
| ID | Prima di… | Si verifica che… |
|---|---|---|