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:
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.
|
||||
@@ -216,7 +216,7 @@ sono vuoti e non sono elencati.
|
||||
| `app/withdrawals/confirmation.py` | Gestore di conferma per il tipo `withdrawal` |
|
||||
| `app/rounds/service.py` | Round attivo, scadenza, accettazione scommesse, `rounds_can_open`, apertura del round, quota del vincitore |
|
||||
| `app/rounds/scheduler.py` | `RoundScheduler`: chiusura, estrazione, pagamento e suoi tentativi |
|
||||
| `app/rounds/draw.py` | Matematica degli header (hash, prev-hash, target) e scelta del vincitore. *Da implementare:* seme commit-reveal e impronta dei partecipanti (B-73), confronto del bersaglio con `powLimit` (B-74) |
|
||||
| `app/rounds/draw.py` | Matematica degli header (hash, prev-hash, target) e scelta del vincitore. *Da implementare:* seme commit-reveal e impronta dei partecipanti (B-73) |
|
||||
| `app/rounds/config.py` | `get_round_config`: la riga unica di `round_config`, creata se manca |
|
||||
| `app/rounds/confirmation.py` | Gestore di conferma per il tipo `payout` |
|
||||
| `app/rounds/events.py` | `RoundEventBroadcaster` e il singleton `broadcaster` (SSE) |
|
||||
@@ -539,9 +539,10 @@ implementare» non esistono ancora nel codice (vedi [ARC-60] e [ARC-22]).
|
||||
`app/electrum/listener.py:ElectrumListener._apply_header` ·
|
||||
`app/electrum/client.py:ElectrumClient.connect` ·
|
||||
*Test:* `tests/unit/test_electrum_listener.py`, `tests/unit/test_electrum_client.py`
|
||||
*Stato:* da implementare (B-74, B-85, B-86) · *Oggi:* il listener
|
||||
tiene solo la punta, chiude la sessione su ogni header che non si concatena
|
||||
e non confronta la propria punta con gli altri server.
|
||||
*Stato:* da implementare (B-85, B-86) · *Oggi:* una riorganizzazione
|
||||
è seguita ma non registrata né segnalata con una pausa, un header
|
||||
concorrente alla stessa altezza è ignorato, e il listener non confronta la
|
||||
propria punta con gli altri server.
|
||||
- **[ARC-27]** **`RoundScheduler`.** A ogni giro (5s) chiama
|
||||
`open_new_round_if_needed` e poi avanza il round attivo, in qualunque stato
|
||||
sia, `drawing` compreso: per un round in `drawing` il giro rientra
|
||||
|
||||
@@ -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
|
||||
|
||||
+7
-9
@@ -324,16 +324,14 @@ implementato il [piano delle correzioni](piano-correzioni.md).
|
||||
*Codice:* `app/rounds/draw.py:draw_winner` · `app/rounds/scheduler.py:RoundScheduler._close_and_draw`
|
||||
|
||||
- **[LIM-14]** **La validazione degli header non ricostruisce la difficoltà
|
||||
reale.** Oggi il bersaglio non è confrontato con il limite di rete: un server
|
||||
può dichiarare `bits=0x207fffff` e l'header passa **senza alcun lavoro**
|
||||
(la versione precedente di questa voce parlava di «hardware modesto»,
|
||||
sottostimando il problema). La concatenazione si verifica solo per un
|
||||
avanzamento di un blocco esatto, quindi un header falso a un'altezza enorme
|
||||
blocca `tip_height`, che non regredisce, e con esso ogni estrazione.
|
||||
reale.** Il bersaglio dichiarato non può essere più facile del limite di rete
|
||||
(B-74, [ELC-11](connessione-electrum.md#4-header-validati-non-creduti)) e
|
||||
ogni salto di più blocchi è verificato come catena, quindi un header falso a
|
||||
un'altezza enorme non blocca più `tip_height`.
|
||||
- *Mitigazione attuale:* la corroborazione del blocco usato per l'estrazione
|
||||
([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md), [ADR-25](adr/ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md)); non protegge dal blocco di `tip_height`.
|
||||
- *Stato:* da chiudere con B-74.
|
||||
- *Per chiuderlo:* bersaglio non più facile di `powLimit` e catena
|
||||
([ADR-07](adr/ADR-07-ogni-decisione-sposta-denaro-base-risposta.md), [ADR-25](adr/ADR-25-blocco-estrazione-fissato-altezza-successivo-chiusura.md)); non proteggeva dal blocco di `tip_height`, ora chiuso da B-74.
|
||||
- *Stato:* chiuso da B-74.
|
||||
- *Come è chiuso:* bersaglio non più facile di `powLimit` e catena
|
||||
verificata su ogni salto ([ADR-26](adr/ADR-26-validazione-degli-header-concatenata-senza-ricalcolo.md)). Resta accettato dopo il piano: senza
|
||||
il ricalcolo della difficoltà, un header a difficoltà minima (`powLimit`,
|
||||
in media 2³² hash) costa una frazione di secondo a un miner ASIC e pochi
|
||||
|
||||
+3
-4
@@ -66,10 +66,9 @@ ricompensa di quel blocco.
|
||||
confermato dal quorum ([ELC-15](connessione-electrum.md#5-il-quorum)).
|
||||
*Oggi* (B-73): il seme è l'hash del primo blocco visto dopo la chiusura,
|
||||
senza segreto né impegno, e l'estrazione è manipolabile da chi mina.
|
||||
- **Header falsi.** Oggi l'unico controllo di lavoro su un header è che il
|
||||
suo hash rispetti il bersaglio che l'header stesso dichiara nel campo
|
||||
`bits`: poiché è chi fabbrica l'header a scegliere `bits`, un header mai
|
||||
minato con `bits = 0x207fffff` lo passa con probabilità 1/2. Dopo B-74 il
|
||||
- **Header falsi.** Il bersaglio che un header dichiara nel campo `bits` lo sceglie chi
|
||||
fabbrica l'header (con `bits = 0x207fffff` un header mai minato passava con
|
||||
probabilità 1/2): per questo dal B-74 il
|
||||
bersaglio non può essere più facile del powLimit di PLM, `0x1d00ffff`
|
||||
([ELC-11](connessione-electrum.md#4-header-validati-non-creduti)), e ogni
|
||||
salto di più blocchi è verificato come catena. Anche al powLimit un header
|
||||
|
||||
+3
-3
@@ -222,7 +222,7 @@ cifrava di nuovo il file sbagliato (vedi [operativita.md](operativita.md)).
|
||||
| B-nn | Difetto | Gravità | Commit | Test di regressione | Area |
|
||||
|---|---|---|---|---|---|
|
||||
| B-73 | L'estrazione dipende solo dall'hash del blocco: un minatore con biglietti o il server Electrum attivo possono pilotarla; ordine dei partecipanti non verificabile | Critica | **aperto** | — | RND, ELC |
|
||||
| B-74 | Header accettati senza controllo della difficoltà minima; un salto di più blocchi non viene collegato; una riorganizzazione di un blocco chiude la sessione | Critica | **aperto** | — | ELC |
|
||||
| B-74 | Header accettati senza controllo della difficoltà minima; un salto di più blocchi non viene collegato; una riorganizzazione di un blocco chiude la sessione | Critica | *da registrare* | `test_electrum_listener.py`, `test_draw.py` | ELC |
|
||||
| B-75 | Una risposta Electrum oltre 64 KiB chiude la connessione, e la cronologia dell'indirizzo del montepremi la supera dopo ~800 transazioni | Critica | *da registrare* | `test_electrum_client.py`, `test_deposits.py`, `test_psbt_builder.py` | ELC, TX |
|
||||
| B-76 | Un invio ambiguo (timeout, connessione caduta) viene annullato come se fosse rifiutato | Critica | *da registrare* | `test_broadcast_outcome.py`, `test_electrum_client.py` | TX, BET, WDR |
|
||||
| B-77 | Si ricorda solo l'ultimo txid di una transazione accelerata, e la sostituzione viene inviata prima di essere salvata | Critica | *da registrare* | `test_tx_versions.py`, `test_migrations.py`, `test_broadcast.py` | TX |
|
||||
@@ -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, B-83.
|
||||
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, B-74.
|
||||
- **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,
|
||||
B-73 … B-98 dalla revisione del 2026-09-28 tranne i corretti (4 critici,
|
||||
6 alti, 6 medi), da implementare nell'ordine di
|
||||
[piano-correzioni.md](piano-correzioni.md).
|
||||
- **Prossimo numero libero:** `B-99`.
|
||||
Reference in new issue
Block a user