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:
davideandClaude Sonnet 5.5 committed 2026-10-14 15:29:30 +02:00
1 parent a4b160e240
commit a3c51719bd
23 files changed
+745 -119

No files matched your search

+9 -13
View File
@@ -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.