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>
35 lines
1.4 KiB
Python
35 lines
1.4 KiB
Python
"""Every test runs against a throwaway master key, never the operator's real one.
|
|
|
|
B-80: opening a round now derives its pool address, so code paths that merely open a
|
|
round (the scheduler, `POST /bets`, the round route) need a master key — and without
|
|
this they silently read whatever `MASTER_KEY_PATH`/`XPRV_ENCRYPTION_KEY` of the local
|
|
`.env` point at, i.e. the real encrypted key on a developer machine and nothing at all
|
|
on a clean checkout. Tests that build their own key (most of them) simply override it.
|
|
"""
|
|
|
|
import pytest
|
|
from cryptography.fernet import Fernet
|
|
|
|
from app.config import settings
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _throwaway_master_key(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(settings, "master_key_path", str(tmp_path / "autouse-master.xprv.enc"))
|
|
monkeypatch.setattr(settings, "xprv_encryption_key", Fernet.generate_key().decode())
|
|
from app.wallet import hd
|
|
|
|
hd._account_key = None
|
|
hd.generate_master_key()
|
|
yield
|
|
hd._account_key = None
|
|
|
|
|
|
# B-74: the real powLimit needs ~2^32 hashes per header, so tests mine headers at a trivial
|
|
# target (`0x207fffff`) and this swaps in a limit that allows it. Done at import, not in a
|
|
# fixture, because some test modules mine headers while they are being collected. The
|
|
# tests that exercise the limit itself put `MAINNET_POW_LIMIT` back with monkeypatch.
|
|
from app.rounds import draw as _draw # noqa: E402
|
|
|
|
_draw.POW_LIMIT = (1 << 256) - 1
|