2026-07-21 10:26:18 +02:00
|
|
|
import hashlib
|
|
|
|
|
|
2026-07-27 10:07:21 +02:00
|
|
|
# Byte offsets of a standard 80-byte block header: version(4) + prev_block(32) +
|
|
|
|
|
# merkle_root(32) + timestamp(4) + bits(4) + nonce(4).
|
|
|
|
|
_HEADER_LENGTH_BYTES = 80
|
|
|
|
|
_PREV_BLOCK_OFFSET = 4
|
|
|
|
|
_PREV_BLOCK_LENGTH = 32
|
|
|
|
|
_BITS_OFFSET = 72
|
|
|
|
|
_BITS_LENGTH = 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HeaderValidationError(Exception):
|
|
|
|
|
"""Raised by ElectrumListener._apply_header (B-28) when a header either doesn't
|
|
|
|
|
satisfy the difficulty target it claims for itself, or doesn't chain from the
|
|
|
|
|
previously accepted tip. Letting this propagate out of the header-consuming
|
|
|
|
|
task ends the current Electrum session the same way a dropped connection would
|
|
|
|
|
(see ElectrumListener._run_once), so the listener rotates to the next
|
|
|
|
|
configured server instead of trusting a header a server just forged."""
|
|
|
|
|
|
2026-07-21 10:26:18 +02:00
|
|
|
|
|
|
|
|
def header_hex_to_block_hash(header_hex: str) -> str:
|
|
|
|
|
"""Block hash from a raw Electrum header: sha256d, byte-reversed, hex.
|
|
|
|
|
Verified against a real mainnet block (blockchain.transaction.get's own
|
|
|
|
|
reported blockhash) during development."""
|
|
|
|
|
header_bytes = bytes.fromhex(header_hex)
|
|
|
|
|
digest = hashlib.sha256(hashlib.sha256(header_bytes).digest()).digest()
|
|
|
|
|
return digest[::-1].hex()
|
|
|
|
|
|
|
|
|
|
|
2026-07-27 10:07:21 +02:00
|
|
|
def header_prev_hash(header_hex: str) -> str:
|
|
|
|
|
"""The header's `prev_block` field, byte-reversed to the same conventional
|
|
|
|
|
(display) order as header_hex_to_block_hash's return value, so the two can be
|
|
|
|
|
compared directly to check that one header actually chains from another."""
|
|
|
|
|
header_bytes = bytes.fromhex(header_hex)
|
|
|
|
|
prev = header_bytes[_PREV_BLOCK_OFFSET : _PREV_BLOCK_OFFSET + _PREV_BLOCK_LENGTH]
|
|
|
|
|
return prev[::-1].hex()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _target_from_bits(bits: int) -> int:
|
|
|
|
|
"""Decompress Bitcoin-style compact `nBits` difficulty encoding into the full
|
|
|
|
|
256-bit target a valid header's hash must be less than or equal to."""
|
|
|
|
|
exponent = bits >> 24
|
|
|
|
|
mantissa = bits & 0xFFFFFF
|
|
|
|
|
if exponent <= 3:
|
|
|
|
|
return mantissa >> (8 * (3 - exponent))
|
|
|
|
|
return mantissa << (8 * (exponent - 3))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def header_meets_its_own_target(header_hex: str) -> bool:
|
|
|
|
|
"""Whether this header's hash satisfies the difficulty target *it claims for
|
|
|
|
|
itself* (the `bits` field). Rejects a header that was never actually mined —
|
|
|
|
|
e.g. one fabricated wholesale by a hostile or MITM'd Electrum server (B-28),
|
|
|
|
|
since satisfying a self-chosen target still requires real proof-of-work.
|
|
|
|
|
|
|
|
|
|
This does NOT — and, short of downloading and validating the full header
|
|
|
|
|
chain's difficulty-retarget history, cannot — catch a header honestly mined at
|
|
|
|
|
a real but implausibly low self-chosen difficulty: a server could still declare
|
|
|
|
|
an easy target and grind it out with modest hardware. That residual risk is why
|
|
|
|
|
the draw additionally requires the winning block's header to be corroborated by
|
|
|
|
|
the *other* configured servers before using it as the seed (see
|
|
|
|
|
ElectrumListener.corroborate_header and rounds/scheduler.py:_wait_for_next_block)
|
|
|
|
|
rather than relying on this check alone.
|
|
|
|
|
"""
|
|
|
|
|
header_bytes = bytes.fromhex(header_hex)
|
|
|
|
|
if len(header_bytes) != _HEADER_LENGTH_BYTES:
|
|
|
|
|
return False
|
|
|
|
|
bits = int.from_bytes(header_bytes[_BITS_OFFSET : _BITS_OFFSET + _BITS_LENGTH], "little")
|
|
|
|
|
target = _target_from_bits(bits)
|
|
|
|
|
if target <= 0:
|
|
|
|
|
return False
|
|
|
|
|
digest = hashlib.sha256(hashlib.sha256(header_bytes).digest()).digest()
|
|
|
|
|
# The hash as the integer comparable against `target`: this is the same digest
|
|
|
|
|
# header_hex_to_block_hash reverses into the conventional display hex, so
|
|
|
|
|
# reading it byte-reversed as a big-endian int is equivalent to reading the
|
|
|
|
|
# original digest bytes as little-endian — both give the same integer.
|
|
|
|
|
hash_int = int.from_bytes(digest, "little")
|
|
|
|
|
return hash_int <= target
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 10:26:18 +02:00
|
|
|
def draw_winner(participants: list[str], block_hash_hex: str) -> str:
|
|
|
|
|
"""v1 draw algorithm (flowchart.mmd, node R): seed = block hash as an integer,
|
|
|
|
|
index = seed mod participant_count, winner = participants[index]. Anyone can
|
|
|
|
|
recompute and verify it from public data. Deliberately simple/replaceable."""
|
|
|
|
|
if not participants:
|
|
|
|
|
raise ValueError("no participants to draw from")
|
|
|
|
|
seed = int(block_hash_hex, 16)
|
|
|
|
|
index = seed % len(participants)
|
|
|
|
|
return participants[index]
|