Files
plm-lottery/app/rounds/draw.py
T

89 lines
4.2 KiB
Python
Raw Normal View History

import hashlib
# 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."""
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()
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
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]