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

+8 -24
View File
@@ -151,10 +151,6 @@ flowchart TB
F -- "Non trovato" --> X
```
*Comportamento atteso.* Oggi un salto di più blocchi non viene collegato, la
difficoltà non è confrontata con il powLimit, e un header che non si aggancia
chiude la sessione.
Tre casi vengono semplicemente **ignorati**, con un avviso nei log e senza
chiudere la sessione, perché non indicano un server ostile:
@@ -197,11 +193,6 @@ passare a un altro server.
([ELC-15]).
*Codice:* `app/rounds/draw.py:header_meets_its_own_target`, `app/electrum/listener.py:ElectrumListener._apply_header` ·
*Test:* `tests/unit/test_draw.py`, `tests/unit/test_electrum_listener.py`
*Stato:* da implementare (B-74), per il confronto con il powLimit ·
*Oggi:* si verifica solo che l'hash rispetti il bersaglio che l'header
stesso dichiara. Poiché è chi fabbrica l'header a scegliere `bits`, il
controllo non respinge un header mai minato: con `bits = 0x207fffff`
(bersaglio `0x7fffff · 2²³²`, appena sotto 2²⁵⁵) lo passa un header casuale con probabilità 1/2.
- **[ELC-12]** **Ogni** avanzamento della punta deve essere collegato alla
punta corrente. Un avanzamento di un blocco deve avere come `prev_hash`
l'hash della punta. Per un avanzamento di più blocchi, dalla punta `T` a
@@ -213,16 +204,8 @@ passare a un altro server.
(`T+1`, o `H` stesso per un avanzamento di un blocco) a non agganciarsi
alla punta, non si chiude la sessione: è una possibile riorganizzazione e
si segue [ELC-23].
*Codice:* `app/rounds/draw.py:header_prev_hash`, `app/electrum/listener.py:ElectrumListener._apply_header`, `app/electrum/client.py:ElectrumClient.request` ·
*Codice:* `app/rounds/draw.py:header_prev_hash`, `app/electrum/listener.py:ElectrumListener._apply_header`, `app/electrum/listener.py:ElectrumListener._fetch_headers`, `app/electrum/client.py:ElectrumClient.get_block_headers` ·
*Test:* `tests/unit/test_draw.py`, `tests/unit/test_electrum_listener.py`
*Stato:* da implementare (B-74) · *Oggi:* il collegamento si verifica solo
per un avanzamento di un blocco; un salto di più blocchi è accettato con il
solo controllo di difficoltà. Un avanzamento di un blocco che non si
aggancia chiude la sessione: per questo una normale riorganizzazione di un
blocco (la nostra punta `N` diventa orfana) fa chiudere la sessione su
**ogni** server — ciascuno annuncia `N+1` sul nuovo ramo, che non si
aggancia alla nostra `N` — finché l'arrivo di `N+2` non rende l'avanzamento
un salto di due blocchi, accettato senza controllo di collegamento.
- **[ELC-13]** Un header accettato aggiorna insieme altezza e contenuto della
punta (mai uno senza l'altro) e la finestra di [ELC-22]. Ogni notifica di
nuovi header invia una notifica SSE, perché è proprio ciò che l'estrazione
@@ -236,9 +219,8 @@ passare a un altro server.
superare [ELC-11] ed essere collegati fra loro e al primo, altrimenti la
sessione si chiude. La finestra sopravvive alle riconnessioni, non ai
riavvii.
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header`, `app/electrum/listener.py:ElectrumListener._run_once` · *Test:* —
*Stato:* da implementare (B-85) · *Oggi:* si tiene solo la punta
(`tip_height`, `tip_header_hex`).
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header`, `app/electrum/listener.py:ElectrumListener._adopt_first_header`, `app/electrum/listener.py:ElectrumListener._extend_window` ·
*Test:* `tests/unit/test_electrum_listener.py`
### Riorganizzazioni
@@ -252,9 +234,11 @@ passare a un altro server.
il punto di biforcazione e il nuovo header diventa la punta. Se non lo
trova entro 10 header (una biforcazione più profonda, o un server su
un'altra catena), la sessione si chiude come per [ELC-11].
*Codice:* `app/electrum/listener.py:ElectrumListener._apply_header` · *Test:* —
*Stato:* da implementare (B-74, B-85) · *Oggi:* nessuna riorganizzazione è
rilevata né seguita; vedi l'*Oggi* di [ELC-12].
*Codice:* `app/electrum/listener.py:ElectrumListener._follow_reorg` ·
*Test:* `tests/unit/test_electrum_listener.py`
*Stato:* da implementare (B-85), per l'header con hash diverso alla stessa
altezza della punta · *Oggi:* un header concorrente alla stessa altezza è
ignorato ([ELC-09]); gli altri casi sono seguiti.
- **[ELC-24]** Ogni riorganizzazione seguita scrive l'evento
`chain_reorg_detected` con la profondità (numero dei nostri blocchi
sostituiti, cioè vecchia altezza della punta meno altezza del punto di