Confirm transactions by merkle proof and reinstate coins only on a quorum (B-83, TX-06, TX-22, TX-23, ELC-25, ELC-26, DEP-02, DEP-08, AUD-18, ADR-07)
poll_once marked a transaction confirmed on one server's `height > 0`, and reinstate_reappeared_utxos restored a coin written off as spent outside the platform on the active server's word alone, so a hostile active server could report a never-relayed payout as confirmed (winner unpaid, round closed) or put a spent coin back into a balance. - electrum/merkle.py: pure merkle-branch verification against a block header. - ElectrumListener.confirmation_proven(txid, height): the *other* servers are asked for blockchain.transaction.get_merkle; a strict majority of at least the quorum minimum must return a proof that recomputes to the merkle root of that block's header (block height and position checked too). The header is the tip the listener holds, or for an older block the one a majority of the quorum agrees on and that meets its own target; a height above our tip cannot be proven. Quorum rules are _corroborate_majority's. - poll_once takes the listener as `network`: the active server's history only nominates a candidate (txid, height); the row confirms only when the proof holds, and stays pending otherwise. With no network nothing confirms. ConfirmationPoller wires it. - The reconciler's late-confirmation check of failed rows (B-78) now puts each claimed (txid, height) to the same proof instead of counting histories. - refresh_user splits reinstatement like a credit: find_reinstate_candidates (read-only; same outpoint, recorded amount, confirmed) -> corroborate_utxo_reinstatement (same quorum as a credit) -> reinstate_reappeared_utxos writes only what was corroborated, rechecking the mark under the lock. Nothing is committed in the read phase any more. - Broadcast fan-out, the third part of the finding, shipped with B-76. Tests changed because the spec changed: every poll_once call now passes a network that proves (FakeNetwork) — the confirmation tests are otherwise unchanged; the failed-row "quorum does not confirm" test now asserts the claim goes to the proof. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a4b160e240
commit
a3c51719bd
23 files changed
+745
-119
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)
|
||||
- **Stato:** Accettata
|
||||
- **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*.
|
||||
|
||||
@@ -31,7 +31,5 @@ di 2 server, la corroborazione fallisce.
|
||||
scelti dall'operatore ([LIM-11]). Regole in
|
||||
[connessione-electrum.md](../connessione-electrum.md#5-il-quorum) (B-83, B-84).
|
||||
|
||||
*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) · *Oggi:* conferme, ripristini e abbandoni
|
||||
si decidono sul solo server attivo.
|
||||
*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/electrum/listener.py:ElectrumListener.corroborate_utxo_reinstatement` · `app/electrum/listener.py:ElectrumListener.confirmation_proven` · `app/main.py:lifespan` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_confirmation_proof.py`
|
||||
+1
-3
@@ -332,9 +332,7 @@ flowchart LR
|
||||
`user_id` sì, `round_id` no. Stessa transazione dell'annullamento e del
|
||||
ricalcolo del saldo.
|
||||
*Codice:* `app/deposits/service.py:reinstate_reappeared_utxos` ·
|
||||
*Test:* `tests/unit/test_deposits.py`
|
||||
*Stato:* da implementare (B-83), per la conferma del quorum · *Oggi:* basta
|
||||
la parola del server attivo.
|
||||
*Test:* `tests/unit/test_deposits.py`, `tests/unit/test_confirmation_proof.py`
|
||||
|
||||
- **[AUD-19]** `bet_placed` — la rete ha accettato la transazione di una
|
||||
scommessa. Payload `{"txid", "amount_sats"}`: `txid` è quello del primo
|
||||
|
||||
@@ -274,13 +274,12 @@ 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).
|
||||
|
||||
Oggi non è ancora così per tre decisioni: le conferme delle transazioni
|
||||
(`app/tx/confirmation.py:poll_once`), il reintegro di una moneta ricomparsa
|
||||
(`app/deposits/service.py:reinstate_reappeared_utxos`) e la lettura delle
|
||||
monete del montepremi per il pagamento
|
||||
(`app/rounds/scheduler.py:RoundScheduler._trigger_payout`) si basano sul solo
|
||||
server attivo; sono le righe [ELC-26], [ELC-25] e [ELC-27]. L'abbandono di una
|
||||
transazione ([ELC-28]) e l'invio ([TX-04], [TX-21]) usano già tutti i server.
|
||||
Oggi non è ancora così per una sola decisione: la lettura delle monete del
|
||||
montepremi per il pagamento
|
||||
(`app/rounds/scheduler.py:RoundScheduler._trigger_payout`) si basa sul solo
|
||||
server attivo, ed è la riga [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 già gli altri server.
|
||||
|
||||
- **[ELC-14]** La corroborazione interroga in parallelo tutti i server
|
||||
configurati tranne quello attivo, ciascuno con una connessione nuova e un
|
||||
@@ -308,8 +307,10 @@ transazione ([ELC-28]) e l'invio ([TX-04], [TX-21]) usano già tutti i server.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener.corroborate_header`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_spent`,
|
||||
`app/electrum/listener.py:ElectrumListener.corroborate_utxo_credit`,
|
||||
`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`
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_confirmation_proof.py`
|
||||
|
||||
Stato delle righe della tabella:
|
||||
|
||||
@@ -319,11 +320,6 @@ Stato delle righe della tabella:
|
||||
successivo invece di richiedere la stessa altezza.
|
||||
- [ELC-17] — *Stato:* da implementare (B-82) · *Oggi:* basta un'altezza
|
||||
maggiore di 0 (1 conferma).
|
||||
- [ELC-25] — *Stato:* da implementare (B-83) · *Oggi:* il reintegro si basa
|
||||
sulla sola risposta del server attivo.
|
||||
- [ELC-26] — *Stato:* da implementare (B-83) · *Oggi:* una transazione è
|
||||
confermata quando la cronologia dell'indirizzo sul server attivo la riporta
|
||||
con altezza maggiore di 0.
|
||||
- [ELC-27] — *Stato:* da implementare (B-82) · *Oggi:* il pagamento sceglie
|
||||
fra le monete che il server attivo elenca per il montepremi.
|
||||
|
||||
|
||||
+8
-10
@@ -37,7 +37,7 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
| [LIM-03] Attesa in `closing` senza limite di tempo | **Sì, in caso di congestione** | Resta senza limite di tempo per scelta ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)); con B-78 e B-87 finisce quando la rete decide |
|
||||
| [LIM-04] Prelievo e RBF mai provati in rete reale | **Sì, come verifica** | Da chiudere con la prova su rete reale ([operativita.md](operativita.md#prova-su-rete-reale)) prima di aprire al pubblico |
|
||||
| [LIM-10] Riorganizzazioni non gestite | **Sì** — una scommessa sparita rende il round impagabile | Chiuso da B-82 e B-85; resta [LIM-24] |
|
||||
| [LIM-11], [LIM-12] Quorum facoltativo e parziale | **Sì** — un solo server decide conferme e abbandoni | Chiusi da B-83 e B-84 |
|
||||
| [LIM-11], [LIM-12] Quorum facoltativo e parziale | **Sì** — un solo server decide conferme e abbandoni | Chiusi da B-83 e B-84 (resta la lettura delle monete del montepremi, B-82) |
|
||||
| [LIM-13] Estrazione manipolabile | **Sì** — per un giocatore che mina | Chiuso da B-73; resta [LIM-25] |
|
||||
| [LIM-14] Header senza difficoltà minima | **Sì** — un header falso congela ogni estrazione | Chiuso da B-74 per il blocco; resta il costo basso di un header a difficoltà minima |
|
||||
| [LIM-06] Token admin unico | No per il funzionamento, ma è il rischio di sicurezza più ampio | Invariato |
|
||||
@@ -276,18 +276,16 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener._corroborate_majority`
|
||||
|
||||
- **[LIM-12]** **Non tutte le letture di rete sono corroborate.** Passano dal
|
||||
solo server attivo: la conferma di scommesse, pagamenti e prelievi
|
||||
(`poll_once`), l'elenco delle monete del montepremi usato per il pagamento, e il
|
||||
ripristino di una moneta prima segnata come spesa all'esterno.
|
||||
- *Impatto:* un server ostile potrebbe far considerare confermata una
|
||||
scommessa che non lo è, lasciando il database in disaccordo con la
|
||||
catena; il ripristino non corroborato può reintegrare nel saldo una
|
||||
moneta davvero spesa.
|
||||
solo server attivo: l'elenco delle monete del montepremi usato per il
|
||||
pagamento.
|
||||
- *Impatto:* un server ostile potrebbe far scegliere al pagamento monete che
|
||||
non esistono (il pagamento verrebbe rifiutato dalla rete) o nasconderne
|
||||
di vere (il pagamento fallirebbe e verrebbe ritentato).
|
||||
- *Mitigazione attuale:* l'abbandono richiede una prova ([ADR-51](adr/ADR-51-dubbio-liberano-mai-fondi.md)) e l'invio
|
||||
arriva a tutti i server; un invio basato su monete inesistenti è rifiutato
|
||||
dalla rete.
|
||||
- *Stato:* da chiudere con B-83 (conferme e ripristini) e B-82 (monete del
|
||||
round prima del pagamento).
|
||||
- *Stato:* da chiudere con B-82 (monete del round prima del pagamento);
|
||||
conferme e ripristini sono chiusi da B-83.
|
||||
- *Per chiuderlo:* una conferma vale quando il quorum restituisce una prova
|
||||
merkle valida (`blockchain.transaction.get_merkle`) contro un header che
|
||||
possediamo; il ripristino richiede lo stesso quorum di un accredito;
|
||||
|
||||
@@ -390,9 +390,8 @@ del database sono tutti uguali, perché arrivano tutti dallo stesso percorso
|
||||
`spent_txid IS NULL`.
|
||||
*Codice:* le funzioni in tabella ·
|
||||
*Test:* `tests/unit/test_deposits.py`, `tests/unit/test_bets.py`,
|
||||
`tests/unit/test_withdrawals.py`, `tests/unit/test_reconcile.py`
|
||||
*Stato:* da implementare (B-83) · *Oggi:* il reintegro avviene sulla parola
|
||||
del solo server attivo.
|
||||
`tests/unit/test_withdrawals.py`, `tests/unit/test_reconcile.py`,
|
||||
`tests/unit/test_confirmation_proof.py`
|
||||
|
||||
- **[DAT-13]** Il riconciliatore libera un input solo se il suo `spent_txid` è
|
||||
uno dei txid della riga che sta abbandonando (`current_txid` o una sua
|
||||
|
||||
@@ -237,11 +237,7 @@ monete sotto la soglia non compaiono nel saldo mostrato.
|
||||
casi corroborati, ricontrollando lo stato di ogni moneta al salvataggio.
|
||||
Nessuna sessione del database resta aperta durante le chiamate di rete.
|
||||
*Codice:* `app/electrum/listener.py:ElectrumListener.refresh_user` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`
|
||||
*Stato:* da implementare (B-83) · *Oggi:* il reintegro delle monete
|
||||
ricomparse viene **scritto e committato già nella fase di lettura**, prima
|
||||
di qualunque corroborazione; solo accrediti e sparizioni sono corroborati
|
||||
prima di essere scritti.
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_confirmation_proof.py`
|
||||
- **[DEP-03]** **Soglia di conferme.** Una moneta è candidata all'accredito
|
||||
solo se è confermata (altezza maggiore di 0: le monete ancora in mempool
|
||||
sono ignorate), non è già registrata e ha raggiunto la sua soglia di
|
||||
@@ -313,9 +309,7 @@ monete sotto la soglia non compaiono nel saldo mostrato.
|
||||
si invia una notifica SSE. Se la verifica non riesce la moneta resta fuori
|
||||
dal saldo e viene riproposta all'aggiornamento successivo.
|
||||
*Codice:* `app/deposits/service.py:reinstate_reappeared_utxos`, `app/electrum/listener.py:ElectrumListener.refresh_user` ·
|
||||
*Test:* `tests/unit/test_deposits.py`
|
||||
*Stato:* da implementare (B-83) · *Oggi:* il reintegro si basa sulla sola
|
||||
risposta del server attivo, senza corroborazione.
|
||||
*Test:* `tests/unit/test_deposits.py`, `tests/unit/test_confirmation_proof.py`
|
||||
- **[DEP-09]** **Due vie, un solo codice.** Un aggiornamento parte quando il
|
||||
server notifica un cambiamento su un indirizzo osservato, alla
|
||||
(re)iscrizione di un indirizzo, e al giro periodico: tutte e tre usano la
|
||||
|
||||
+3
-3
@@ -231,7 +231,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
| B-80 | Un solo indirizzo del montepremi per tutti i round | Critica | *da registrare* | `test_round_pool_address.py`, `test_pending_address.py`, `test_scheduler.py` | RND, BET, WDR |
|
||||
| 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-83 | Conferme e reintegri si basano solo sul server attivo | Alta | *da registrare* | `test_confirmation_proof.py`, `test_confirmation.py` | ELC, TX, DEP |
|
||||
| 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 |
|
||||
@@ -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, B-84, B-76, B-77, B-78.
|
||||
come limite accettato; dalla revisione del 2026-09-28: B-88, B-75, B-97, B-80, B-84, B-76, B-77, B-78, B-83.
|
||||
- **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 (5 critici,
|
||||
7 alti, 6 medi), da implementare nell'ordine di
|
||||
6 alti, 6 medi), da implementare nell'ordine di
|
||||
[piano-correzioni.md](piano-correzioni.md).
|
||||
- **Prossimo numero libero:** `B-99`.
|
||||
@@ -167,9 +167,7 @@ monete:
|
||||
per indirizzo a ogni giro; un indirizzo che non si riesce a leggere non
|
||||
blocca gli altri.
|
||||
*Codice:* `app/tx/confirmation.py:poll_once`, `app/tx/pending_address.py:own_address_for` ·
|
||||
*Test:* `tests/unit/test_confirmation.py`, `tests/unit/test_round_pool_address.py`, `tests/unit/test_tx_versions.py`
|
||||
*Stato:* da implementare (B-83) · *Oggi:* un'altezza maggiore di 0
|
||||
riportata dal solo server attivo basta a confermare ([TX-22]).
|
||||
*Test:* `tests/unit/test_confirmation.py`, `tests/unit/test_round_pool_address.py`, `tests/unit/test_tx_versions.py`, `tests/unit/test_confirmation_proof.py`
|
||||
- **[TX-22]** **Conferma verificata.** Un candidato ([TX-06]) è confermato
|
||||
solo quando il quorum dei server
|
||||
([connessione-electrum.md](connessione-electrum.md) (B-84)) restituisce per
|
||||
@@ -179,11 +177,8 @@ monete:
|
||||
il listener non lo tiene più, è quello su cui il quorum concorda. Senza
|
||||
quorum, o con una prova non valida, la riga resta `pending` e si riprova al
|
||||
giro successivo.
|
||||
*Codice:* `app/tx/confirmation.py:poll_once`, `app/electrum/listener.py:ElectrumListener._corroborate_majority` ·
|
||||
*Test:* —
|
||||
*Stato:* da implementare (B-83) · *Oggi:* basta la parola del server
|
||||
attivo: un server ostile può dichiarare confermato un pagamento mai
|
||||
trasmesso.
|
||||
*Codice:* `app/tx/confirmation.py:poll_once`, `app/electrum/listener.py:ElectrumListener.confirmation_proven`, `app/electrum/merkle.py:proof_matches_header` ·
|
||||
*Test:* `tests/unit/test_confirmation_proof.py`
|
||||
- **[TX-07]** Alla conferma la riga passa a `confirmed` e, nella stessa
|
||||
transazione del database, viene chiamato il gestore del suo tipo; poi si
|
||||
invia una notifica SSE:
|
||||
@@ -381,10 +376,6 @@ riserva di [TX-11] serve proprio a dargli un'uscita da ridurre.
|
||||
riconciliatore non corregge da solo le righe collegate.
|
||||
*Codice:* `app/tx/reconcile.py:reconcile_once`, `app/tx/reconcile.py:_resurrect`, `app/rounds/pause.py:pause_automatically` ·
|
||||
*Test:* `tests/unit/test_reconcile_proof.py`, `tests/unit/test_pause_reason.py`
|
||||
*Stato:* da implementare (B-83), per la conferma con prova merkle · *Oggi:*
|
||||
la conferma di una riga `failed` è quella dei server **altri dall'attivo**:
|
||||
una maggioranza stretta di essi (almeno il minimo del quorum) elenca uno
|
||||
dei suoi txid in un blocco, senza prova merkle ([TX-22]).
|
||||
- **[TX-27]** Il riconciliatore invia una notifica SSE solo per le righe di
|
||||
cui ha cambiato qualcosa (promozione, abbandono, riallineamento del txid),
|
||||
non per ogni riga esaminata.
|
||||
|
||||
Reference in new issue
Block a user