Require real proof of work in headers and link every advance to the tip (B-74, ELC-11, ELC-12, ELC-22, ELC-23)

A header only had to meet the target it declared for itself, so a fabricated one with
`bits = 0x207fffff` passed half the time, a jump of several blocks was accepted without
being linked to the tip (a fake header at a huge height locked `tip_height` and froze every
draw), and an ordinary one-block reorganisation closed the session on every server.

- draw.py: MAINNET_POW_LIMIT (00000000ffff...ffff); header_meets_its_own_target also
  refuses a claimed target easier than POW_LIMIT.
- ElectrumListener._apply_header is async and takes the active client. A single block must
  chain by prev_hash; a jump downloads the headers in between (blockchain.block.headers,
  at most 2016 per request, ElectrumClient.get_block_headers) and each must carry valid
  work and chain, otherwise HeaderValidationError ends the session.
- The listener keeps the last 10 headers (`_window`), built from the process's first header
  and its 9 predecessors (which must chain into it). A header that does not join the tip is
  walked back to its fork point inside the window and the server's branch is followed
  instead of ending the session; a fork older than the window, or a branch that does not
  end at the announced header, still ends it. The audit row and the pause for deep reorgs
  are B-85; a competing header at the tip's own height is still ignored until then.
- _header_at also serves older heights from the window.

Tests changed because the spec changed: the mainnet powLimit cannot be mined in a test, so
tests/unit/conftest.py swaps in a regtest limit at import time (the new tests put the real
one back); the _apply_header tests are async; the two _run_once tests serve the first
header's 9 predecessors; test_apply_header_skips_linkage_check_across_a_height_gap is
replaced by the jump tests, since that behaviour is exactly what the finding removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5.5 committed 2026-10-14 15:45:34 +02:00
1 parent a3c51719bd
commit ca50ef62f3
16 files changed
+494 -159

No files matched your search

@@ -1,6 +1,6 @@
# ADR-26 — Validazione degli header concatenata, senza ricalcolo della difficoltà
- **Stato:** Accettata — implementazione da completare (B-74, B-85)
- **Stato:** Accettata — implementazione da completare (B-85)
- **Area:** Round ed estrazione
- **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*.
@@ -32,8 +32,7 @@ in [connessione-electrum.md](../connessione-electrum.md#4-header-validati-non-cr
quorum ([ADR-07](ADR-07-ogni-decisione-sposta-denaro-base-risposta.md)) e il fatto che il seme dipende anche da `server_seed`.
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · `app/rounds/draw.py:header_meets_its_own_target` ·
*Test:* `tests/unit/test_electrum_listener.py::test_apply_header_rejects_one_that_does_not_chain_from_the_tip`, `tests/unit/test_electrum_listener.py::test_apply_header_ignores_a_competing_header_at_the_current_tip_height`
*Stato:* da implementare (B-74, B-85) · *Oggi:* nessun confronto con
`powLimit`, concatenazione verificata solo sull'avanzamento di un blocco
esatto, un header concorrente alla stessa altezza ignorato, un header che non
si concatena chiude la sessione.
*Test:* `tests/unit/test_electrum_listener.py::test_a_multi_block_jump_is_validated_by_fetching_the_headers_in_between`, `tests/unit/test_electrum_listener.py::test_a_fork_is_walked_back_and_followed_instead_of_ending_the_session`, `tests/unit/test_draw.py::test_a_header_below_the_pow_limit_is_rejected_even_if_it_meets_its_own_target`
*Stato:* da implementare (B-85), per l'header concorrente alla stessa
altezza · *Oggi:* un header concorrente alla stessa altezza della punta è
ignorato invece di essere seguito.
@@ -44,6 +44,7 @@ e [ciclo-di-vita-del-round.md](../ciclo-di-vita-del-round.md#4--pagamento)
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · `app/rounds/scheduler.py:RoundScheduler._trigger_payout` · `app/rounds/service.py:open_new_round_if_needed` ·
*Test:* —
*Stato:* da implementare (B-82, B-85) · *Oggi:* nessuna riorganizzazione è
rilevata; un header concorrente è ignorato, uno che non si concatena chiude
la sessione; il pagamento non verifica le monete del round.
*Stato:* da implementare (B-82, B-85) · *Oggi:* una riorganizzazione è
seguita ma non registrata e non mette in pausa la lotteria; un header
concorrente alla stessa altezza è ignorato; il pagamento non verifica le
monete del round.