Files
plm-lottery/docs/adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md
T
davideandClaude Sonnet 5.5 a3c51719bd 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>
2026-10-14 15:29:30 +02:00

2.6 KiB

ADR-07 — Ogni decisione che sposta denaro in base a una risposta di rete è corroborata da un quorum degli altri server configurati

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

Decisione

Ogni decisione che sposta denaro in base a una risposta di rete è corroborata da un quorum degli altri server configurati: il blocco che semina l'estrazione, una moneta data per spesa all'esterno, un nuovo deposito da accreditare, il ripristino di una moneta, la conferma di una transazione (con prova merkle contro un header che possediamo), l'abbandono di una transazione, le monete del round verificate prima del pagamento. Si interrogano solo gli altri server, mai quello attivo (che è ciò che un attaccante controllerebbe); servono almeno 2 risposte e la maggioranza stretta di chi risponde. Il server rifiuta di partire con meno di 2 server di riserva, salvo ALLOW_NO_QUORUM=true (solo sviluppo). Se rispondono meno di 2 server, la corroborazione fallisce.

  • Motivazione: un solo server ostile non deve poter scegliere il vincitore (B-28), azzerare un saldo (B-29) o gonfiarlo (B-59), né far credere confermato un pagamento mai trasmesso (B-83). I saldi si muovono in entrambe le direzioni, quindi entrambe richiedono il quorum. Con 0 o 1 server di riserva la «maggioranza» è una sola voce, o nessuna (B-84).
  • Alternative scartate: quorum facoltativo, che passa sempre con la lista vuota (la regola precedente: il default di .env.example spegneva ogni difesa senza avvisare).
  • Conseguenze: una corroborazione fallita ritarda, non perde e non libera fondi (ADR-51): il deposito è riproposto al refresh successivo, l'estrazione richiede di nuovo lo stesso blocco. Il quorum resta fra server scelti dall'operatore ([LIM-11]). Regole in connessione-electrum.md (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/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