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
@@ -61,3 +61,42 @@ def test_draw_winner_single_participant_always_wins():
|
||||
def test_draw_winner_raises_on_empty_participants():
|
||||
with pytest.raises(ValueError):
|
||||
draw_winner([], "00" * 32)
|
||||
|
||||
|
||||
# --- B-74 (ELC-11): the claimed target must not be easier than the powLimit ------------
|
||||
|
||||
|
||||
def test_pow_limit_is_the_mainnet_constant():
|
||||
"""B-74, ELC-11: PLM mainnet's `consensus.powLimit`, 00000000ffff…ffff."""
|
||||
from app.rounds import draw
|
||||
|
||||
assert draw.MAINNET_POW_LIMIT == int("00000000" + "ff" * 28, 16)
|
||||
assert draw.MAINNET_POW_LIMIT == (1 << 224) - 1
|
||||
|
||||
|
||||
def test_a_header_below_the_pow_limit_is_rejected_even_if_it_meets_its_own_target(monkeypatch):
|
||||
"""B-74, ELC-11: a header whose hash satisfies the target *it declares* (bits =
|
||||
0x207fffff, a target of about 2^255) used to pass — and whoever fabricates a header
|
||||
picks its bits, so a random header passed with probability 1/2. With the mainnet
|
||||
powLimit in force it is rejected however its hash falls."""
|
||||
import struct
|
||||
|
||||
from app.rounds import draw
|
||||
|
||||
def build(nonce: int) -> str:
|
||||
return (
|
||||
struct.pack("<I", 1) + bytes(32) + bytes(32) + struct.pack("<II", 0, 0x207FFFFF) + struct.pack("<I", nonce)
|
||||
).hex()
|
||||
|
||||
# Mined at the trivial target, so it satisfies its own claim ...
|
||||
header = next(h for h in map(build, range(1000)) if header_meets_its_own_target(h)) # regtest limit (conftest)
|
||||
# ... and is nonetheless refused once the real limit is back.
|
||||
monkeypatch.setattr(draw, "POW_LIMIT", draw.MAINNET_POW_LIMIT)
|
||||
assert header_meets_its_own_target(header) is False
|
||||
|
||||
|
||||
def test_a_real_mainnet_header_passes_with_the_mainnet_pow_limit(monkeypatch):
|
||||
from app.rounds import draw
|
||||
|
||||
monkeypatch.setattr(draw, "POW_LIMIT", draw.MAINNET_POW_LIMIT)
|
||||
assert header_meets_its_own_target(_REAL_HEADER_HEX) is True
|
||||
Reference in new issue
Block a user