The two Mermaid diagrams under flowchart/ described the intended flow but had drifted from the code and carried no link back to it. docs/ now holds the specification instead: 15 Italian documents (flows, architecture, data model, API, security, audit log, operations, decisions and limits, bug history) with 472 numbered requirements, each pointing at the function that implements it and the test that covers it. docs/processo-di-modifica.md defines the ID convention and the spec -> code -> test change workflow, and CLAUDE.md now names docs/ as authoritative. Writing the spec from the code also corrected stale claims in CLAUDE.md (B-41's get_history check, BUGS.md still tracking B-71/B-72, Cache-Control scope, the error contract's bare-string exceptions) and in the operator guides (payouts do retry, only `drawing` fails to resume, rounds do not open without fee_address). The B-70 doc test now pins the prose in docs/scommesse-e-prelievi.md instead of the deleted diagram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
4.2 KiB
Python
89 lines
4.2 KiB
Python
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 (docs/ciclo-di-vita-del-round.md): 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]
|