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:
1 parent
fd6a91e93d
commit
13cb66cc06
18 files changed
+451
-125
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# ADR-07 — Ogni decisione che sposta denaro in base a una risposta di rete è corroborata da un quorum degli altri server configurati
|
||||
|
||||
- **Stato:** Accettata — implementazione da completare (B-83, B-84)
|
||||
- **Stato:** Accettata — implementazione da completare (B-83)
|
||||
- **Area:** Conferme e fiducia nella rete
|
||||
- **Fonte di verità per:** la scelta di progetto descritta qui sotto; i requisiti (`PREFISSO-NN`) che la attuano stanno nei documenti di dominio, indicati nelle *Conseguenze*.
|
||||
|
||||
@@ -33,6 +33,5 @@ di 2 server, la corroborazione fallisce.
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority` · `app/electrum/listener.py:ElectrumListener.corroborate_header` · `app/electrum/listener.py:ElectrumListener.corroborate_utxo_spent` · `app/electrum/listener.py:ElectrumListener.corroborate_utxo_credit` · `app/main.py:lifespan` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-83, B-84) · *Oggi:* senza server di riserva la
|
||||
corroborazione passa sempre, basta una risposta, e conferme, ripristini e
|
||||
abbandoni si decidono sul solo server attivo.
|
||||
*Stato:* da implementare (B-83) · *Oggi:* conferme, ripristini e abbandoni
|
||||
si decidono sul solo server attivo.
|
||||
+9
-12
@@ -342,7 +342,7 @@ sequenceDiagram
|
||||
U->>L: startup
|
||||
L->>L: validate_runtime_secrets()
|
||||
L->>L: parse_endpoints(...)
|
||||
L->>L: quorum minimo (B-84, da implementare)
|
||||
L->>L: quorum minimo (validate_quorum_servers)
|
||||
L->>L: verifica della master key (B-89, da implementare)
|
||||
L->>L: START_PAUSED (B-90, da implementare)
|
||||
L->>L: app.state.electrum_listener, app.state.user_locks
|
||||
@@ -424,8 +424,6 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
scritto nel log.
|
||||
*Codice:* `app/electrum/client.py:parse_endpoints` ·
|
||||
*Test:* `tests/unit/test_electrum_client.py`
|
||||
*Stato:* da implementare (B-84) · *Oggi:* la sintassi è `host:porta[:notls]`,
|
||||
senza pin del certificato.
|
||||
|
||||
- **[ARC-60]** **Controlli di avvio.** Dopo [ARC-19] e [ARC-20] e prima di
|
||||
creare i task, il lifespan esegue nell'ordine, e ogni controllo fallito
|
||||
@@ -448,11 +446,11 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
dei task: gira alla prima connessione del listener, e finché non si è
|
||||
concluso senza trovare cronologia la registrazione è rifiutata
|
||||
([operativita.md](operativita.md#ripristino), B-90).
|
||||
*Codice:* `app/main.py:lifespan` · `app/config.py:validate_runtime_secrets` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-84, B-89, B-90) · *Oggi:* dopo i segreti e gli
|
||||
endpoint il lifespan crea subito i task; la chiave si decifra alla prima
|
||||
registrazione o al primo pagamento.
|
||||
*Codice:* `app/main.py:lifespan` · `app/config.py:validate_runtime_secrets` · `app/config.py:validate_quorum_servers` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-89, B-90), per i controlli 2 e 3 · *Oggi:* dopo
|
||||
i segreti, gli endpoint e il quorum il lifespan crea subito i task; la
|
||||
chiave si decifra alla prima registrazione o al primo pagamento.
|
||||
- **[ARC-21]** Lo stato condiviso con le route vive su `app.state`:
|
||||
`electrum_listener` (l'`ElectrumListener`) e `user_locks` (un
|
||||
`UserLocks`), creati nel lifespan; `auth_rate_limiters` viene invece creato
|
||||
@@ -610,10 +608,9 @@ La configurazione è divisa in due posti con regole diverse.
|
||||
| `enable_api_docs` | `ENABLE_API_DOCS` | `False` | Espone Swagger/ReDoc/OpenAPI ([ARC-14]) |
|
||||
|
||||
*Codice:* `app/config.py:Settings` · *Test:* `tests/unit/test_config.py`
|
||||
*Stato:* da implementare (B-84, B-90, B-91) · *Oggi:* `ALLOW_NO_QUORUM`,
|
||||
`START_PAUSED` e `XPRV_ENCRYPTION_KEY_FILE` non esistono, la chiave Fernet è
|
||||
la variabile `XPRV_ENCRYPTION_KEY` e la sintassi delle riserve non ha
|
||||
`pin=`.
|
||||
*Stato:* da implementare (B-90, B-91) · *Oggi:* `START_PAUSED` e
|
||||
`XPRV_ENCRYPTION_KEY_FILE` non esistono, la chiave Fernet è la variabile
|
||||
`XPRV_ENCRYPTION_KEY`.
|
||||
- **[ARC-31]** **Parametri operativi e di business** (costo della scommessa,
|
||||
durata e pausa del round, animazione, commissione di rete, timeout RBF,
|
||||
indirizzo commissioni, pausa di manutenzione e suo motivo, conferme dei
|
||||
|
||||
@@ -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… |
|
||||
|---|---|---|
|
||||
|
||||
+13
-16
@@ -258,23 +258,20 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
|
||||
*Codice:* `app/deposits/service.py:find_utxos_missing_from` · `app/deposits/service.py:reinstate_reappeared_utxos` · `app/electrum/listener.py:ElectrumListener._apply_header`
|
||||
|
||||
- **[LIM-11]** **Senza server di riserva configurati la corroborazione passa
|
||||
sempre.** Con `ELECTRUM_FALLBACK_SERVERS` vuoto (il default di
|
||||
`.env.example`) il solo server primario decide estrazione, spese esterne e
|
||||
accrediti. Anche con server di riserva, la maggioranza è calcolata **su chi
|
||||
risponde**: un solo server di riserva raggiungibile decide da solo. I
|
||||
certificati TLS non sono verificati.
|
||||
- *Impatto:* chi controlla il server primario (o la maggioranza dei server
|
||||
- **[LIM-11]** **I server del quorum li sceglie l'operatore.** Il server
|
||||
rifiuta di partire con meno di 2 server di riserva e ogni corroborazione
|
||||
richiede almeno 2 risposte ([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md), B-84), ma più server controllati dalla
|
||||
stessa parte contano come voci indipendenti. I certificati TLS non sono
|
||||
verificati contro un'autorità; si possono fissare per endpoint
|
||||
(`host:porta:pin=<sha256>`, `ELECTRUM_CERT_PIN`).
|
||||
- *Impatto:* chi controlla la maggioranza dei server configurati (o
|
||||
raggiungibili) può scegliere il vincitore o alterare i saldi mostrati.
|
||||
- *Mitigazione attuale:* nessun server risponde → la corroborazione
|
||||
**fallisce** (una rete irraggiungibile non prova nulla).
|
||||
- *Stato:* da chiudere con B-84.
|
||||
- *Per chiuderlo:* almeno 2 server di riserva configurati (altrimenti il
|
||||
server rifiuta di partire, salvo `ALLOW_NO_QUORUM=true` in sviluppo) e
|
||||
almeno 2 risposte per ogni corroborazione ([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md)); pin facoltativo del
|
||||
certificato per endpoint (`host:porta:pin=<sha256>`). Resta accettato
|
||||
dopo il piano che i server del quorum siano scelti dall'operatore: più
|
||||
server controllati dalla stessa parte contano come voci indipendenti.
|
||||
- *Mitigazione attuale:* quorum minimo obbligatorio; meno di 2 risposte →
|
||||
la corroborazione **fallisce** (una rete irraggiungibile non prova
|
||||
nulla); pin facoltativo del certificato. Il vecchio caso «senza riserve
|
||||
tutto passa» e «una sola risposta decide» è chiuso da B-84.
|
||||
- *Per chiuderlo:* non si chiude: resta una scelta dell'operatore usare
|
||||
server indipendenti fra loro.
|
||||
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority`
|
||||
|
||||
|
||||
+3
-10
@@ -69,10 +69,9 @@ dovranno allora rispettare quel proprietario.
|
||||
`app/config.py:validate_runtime_secrets`,
|
||||
`app/electrum/client.py:parse_endpoints`, `app/wallet/hd.py:_load_account_key` ·
|
||||
*Test:* `tests/unit/test_config.py`, `tests/unit/test_electrum_client.py`
|
||||
*Stato:* da implementare (B-89, B-84) · *Oggi:* della master key si
|
||||
controlla solo che il file esista e che `XPRV_ENCRYPTION_KEY` non sia vuota;
|
||||
una chiave sbagliata emerge alla prima derivazione, e il server parte anche
|
||||
senza server di riserva.
|
||||
*Stato:* da implementare (B-89) · *Oggi:* della master key si controlla
|
||||
solo che il file esista e che `XPRV_ENCRYPTION_KEY` non sia vuota; una
|
||||
chiave sbagliata emerge alla prima derivazione.
|
||||
|
||||
- **[OPS-03]** Il database SQLite lavora in modalità **WAL**: accanto a
|
||||
`plm_lottery.db` esistono `plm_lottery.db-wal` e `plm_lottery.db-shm`, e le
|
||||
@@ -469,12 +468,6 @@ con `docker compose logs app` ([architettura.md](architettura.md) ARC-09).
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority`,
|
||||
`app/main.py:lifespan` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-84) · *Oggi:* con `ELECTRUM_FALLBACK_SERVERS`
|
||||
vuoto (il valore di `.env.example`) il server parte e la corroborazione
|
||||
passa sempre: un solo server decide tutto. Con server di riserva la
|
||||
maggioranza è su chi risponde, quindi basta una sola riserva raggiungibile;
|
||||
se nessuna risponde, la corroborazione fallisce
|
||||
(`draw_header_corroboration_failed` a ogni blocco).
|
||||
|
||||
### Round bloccato in `drawing`
|
||||
|
||||
|
||||
+8
-4
@@ -47,14 +47,18 @@ endpoint admin inclusi, quindi restano disattivati a meno di non impostarlo
|
||||
esplicitamente a `true` — utile in locale, da evitare in produzione.
|
||||
|
||||
`ELECTRUM_FALLBACK_SERVERS` elenca i server di riserva, separati da virgola, nel
|
||||
formato `host:porta` (TLS, il caso normale) oppure `host:porta:notls`. Esempio:
|
||||
formato `host:porta` (TLS, il caso normale), `host:porta:notls` oppure
|
||||
`host:porta:pin=<sha256>` (TLS con il certificato del server fissato). Esempio:
|
||||
|
||||
```
|
||||
ELECTRUM_FALLBACK_SERVERS=nodo2.example.net:50002,nodo3.example.net:50001:notls
|
||||
ELECTRUM_FALLBACK_SERVERS=nodo2.example.net:50002,nodo3.example.net:50002,nodo4.example.net:50002:pin=<sha256>
|
||||
```
|
||||
|
||||
Vengono provati a rotazione dopo il primario. Attenzione: un valore scritto male
|
||||
**blocca l'avvio** dell'app — è voluto, meglio accorgersene subito che durante il
|
||||
Vengono provati a rotazione dopo il primario e servono anche da quorum: ne
|
||||
servono **almeno 2**, altrimenti l'app non parte (`ALLOW_NO_QUORUM=true` lo
|
||||
permette, ma solo in sviluppo). `ELECTRUM_CERT_PIN` fissa allo stesso modo il
|
||||
certificato del 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
|
||||
|
||||
+2
-8
@@ -135,8 +135,8 @@ l'host.
|
||||
riavvio. L'elenco completo è in
|
||||
[architettura.md](architettura.md#configurazione).
|
||||
*Codice:* `app/config.py:Settings`
|
||||
*Stato:* da implementare (B-84, B-90, B-91) · *Oggi:* `ALLOW_NO_QUORUM`,
|
||||
`START_PAUSED` e `XPRV_ENCRYPTION_KEY_FILE` non esistono.
|
||||
*Stato:* da implementare (B-90, B-91) · *Oggi:* `START_PAUSED` e
|
||||
`XPRV_ENCRYPTION_KEY_FILE` non esistono.
|
||||
|
||||
`.env.example` documenta le chiavi necessarie e il comando per generarle;
|
||||
non elenca `DATABASE_URL` né `MASTER_KEY_PATH`, che in Docker sono comunque
|
||||
@@ -629,12 +629,6 @@ loro significato per la sicurezza.
|
||||
più server controllati dalla stessa parte contano come voci indipendenti
|
||||
([LIM-11](limiti-noti.md)).
|
||||
|
||||
*Oggi* (B-84): un `ELECTRUM_FALLBACK_SERVERS` vuoto (il valore di
|
||||
`.env.example`) è accettato in silenzio e fa passare ogni corroborazione; con
|
||||
server di riserva la maggioranza è calcolata su chi risponde, quindi una sola
|
||||
risposta decide; nessun certificato è verificato e non esiste un modo di
|
||||
fissarlo.
|
||||
|
||||
## Host e catena di fornitura
|
||||
|
||||
Chi controlla l'host controlla tutti i fondi ([Modello di
|
||||
|
||||
+3
-3
@@ -232,7 +232,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
| B-81 | `drawing` resta bloccato dopo qualsiasi eccezione, non solo dopo un riavvio | Critica | **aperto** | — | RND |
|
||||
| B-82 | Un deposito annullato da una riorganizzazione dopo la scommessa rende il round impagabile; coinbase accreditate a 1 conferma | Critica | **aperto** | — | DEP, RND |
|
||||
| B-83 | Conferme e reintegri si basano solo sul server attivo | Alta | **aperto** | — | ELC, TX, DEP |
|
||||
| B-84 | Il quorum può ridursi a un solo server o a nessuno senza avvisi; certificati TLS non verificati | Alta | **aperto** | — | ELC, SEC |
|
||||
| B-84 | Il quorum può ridursi a un solo server o a nessuno senza avvisi; certificati TLS non verificati | Alta | *da registrare* | `test_electrum_client.py`, `test_electrum_listener.py` | ELC, SEC |
|
||||
| B-85 | Le riorganizzazioni della catena non vengono né seguite né segnalate | Alta | **aperto** | — | ELC, RND |
|
||||
| B-86 | Un server connesso ma fermo non viene mai sostituito | Alta | **aperto** | — | ELC, OPS |
|
||||
| B-87 | L'RBF può lasciare un resto sotto la soglia di dust | Alta | **aperto** | — | TX |
|
||||
@@ -252,9 +252,9 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
|
||||
- **Corretti:** B-01 … B-70, tranne B-16, che è chiuso nella sostanza (il
|
||||
crash è eliminato e `/guida` serve un segnaposto) ma lascia la guida vera
|
||||
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80.
|
||||
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84.
|
||||
- **Aperti:** B-71, B-72 (gravità bassa, operativi o di documentazione) e
|
||||
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (8 critici,
|
||||
8 alti, 6 medi), da implementare nell'ordine di
|
||||
7 alti, 6 medi), da implementare nell'ordine di
|
||||
[piano-correzioni.md](piano-correzioni.md).
|
||||
- **Prossimo numero libero:** `B-99`.
|
||||
Reference in new issue
Block a user