2026-07-21 10:26:18 +02:00
|
|
|
import pytest
|
|
|
|
|
|
2026-07-27 10:07:21 +02:00
|
|
|
from app.rounds.draw import (
|
|
|
|
|
draw_winner,
|
|
|
|
|
header_hex_to_block_hash,
|
|
|
|
|
header_meets_its_own_target,
|
|
|
|
|
header_prev_hash,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Real PLM mainnet block 477486: header from blockchain.block.header, hash
|
|
|
|
|
# cross-checked against the blockhash reported by blockchain.transaction.get for a
|
|
|
|
|
# tx confirmed in that block.
|
|
|
|
|
_REAL_HEADER_HEX = (
|
|
|
|
|
"0020ed30fbc39ee45200d10214f5107c5f36e7bf753032fd1d3279810c170000000000"
|
|
|
|
|
"009a4ea723f732be4c538f3a2490837dd6560103d73ece606aa7d578a66700447b28875"
|
|
|
|
|
"e6a47a61b1ad8012582"
|
|
|
|
|
)
|
2026-07-21 10:26:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_hex_to_block_hash_matches_known_mainnet_block():
|
|
|
|
|
known_block_hash = "00000000000008788b55ade13b74d54ceffda9e54315b802411be1ca65064e86"
|
2026-07-27 10:07:21 +02:00
|
|
|
assert header_hex_to_block_hash(_REAL_HEADER_HEX) == known_block_hash
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_meets_its_own_target_accepts_a_real_mined_header():
|
|
|
|
|
"""B-28: a genuinely mined mainnet header must pass its own self-consistency
|
|
|
|
|
check — this isn't just a synthetic-header property."""
|
|
|
|
|
assert header_meets_its_own_target(_REAL_HEADER_HEX) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_meets_its_own_target_rejects_a_tampered_header():
|
|
|
|
|
"""Flipping a single nonce bit changes the hash completely (avalanche effect)
|
|
|
|
|
without changing the claimed difficulty, so a tampered-but-otherwise-real
|
|
|
|
|
header should almost certainly fail — this is what would catch a
|
|
|
|
|
hostile/MITM'd server replaying a real header with a doctored field."""
|
|
|
|
|
tampered = bytearray(bytes.fromhex(_REAL_HEADER_HEX))
|
|
|
|
|
tampered[-1] ^= 0xFF # flip the last byte of the nonce
|
|
|
|
|
assert header_meets_its_own_target(tampered.hex()) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_meets_its_own_target_rejects_wrong_length():
|
|
|
|
|
assert header_meets_its_own_target("aa" * 10) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_header_prev_hash_matches_the_known_previous_block():
|
|
|
|
|
# Block 477486's predecessor, 477485 — independently known from the same chain.
|
|
|
|
|
assert header_prev_hash(_REAL_HEADER_HEX) == "000000000000170c8179321dfd323075bfe7365f7c10f51402d10052e49ec3fb"
|
2026-07-21 10:26:18 +02:00
|
|
|
|
|
|
|
|
|
Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
2026-10-14 16:29:09 +02:00
|
|
|
# --- B-73 (RND-14, RND-17, RND-34): the commit-reveal draw -------------------------------
|
|
|
|
|
|
|
|
|
|
_SEED = "11" * 32
|
|
|
|
|
_BLOCK = "00" * 28 + "deadbeef"
|
|
|
|
|
_LIST_HASH = "22" * 32
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_draw_seed_is_the_sha256_of_the_three_32_byte_values_read_big_endian():
|
|
|
|
|
"""RND-17: the algorithm, written out here independently of the code under test."""
|
|
|
|
|
import hashlib
|
|
|
|
|
|
|
|
|
|
from app.rounds.draw import draw_seed
|
|
|
|
|
|
|
|
|
|
expected = int.from_bytes(
|
|
|
|
|
hashlib.sha256(bytes.fromhex(_SEED) + bytes.fromhex(_BLOCK) + bytes.fromhex(_LIST_HASH)).digest(), "big"
|
|
|
|
|
)
|
|
|
|
|
assert draw_seed(_SEED, _BLOCK, _LIST_HASH) == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_draw_seed_depends_on_all_three_inputs():
|
|
|
|
|
from app.rounds.draw import draw_seed
|
|
|
|
|
|
|
|
|
|
base = draw_seed(_SEED, _BLOCK, _LIST_HASH)
|
|
|
|
|
assert draw_seed("12" * 32, _BLOCK, _LIST_HASH) != base # without the secret nobody can predict it
|
|
|
|
|
assert draw_seed(_SEED, "00" * 28 + "deadbeee", _LIST_HASH) != base
|
|
|
|
|
assert draw_seed(_SEED, _BLOCK, "23" * 32) != base # nor can the list change after the block
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 10:26:18 +02:00
|
|
|
def test_draw_winner_is_deterministic_and_within_range():
|
Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
2026-10-14 16:29:09 +02:00
|
|
|
from app.rounds.draw import draw_index, draw_seed
|
|
|
|
|
|
2026-07-21 10:26:18 +02:00
|
|
|
participants = ["addrA", "addrB", "addrC"]
|
Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
2026-10-14 16:29:09 +02:00
|
|
|
winner = draw_winner(participants, _SEED, _BLOCK, _LIST_HASH)
|
|
|
|
|
assert winner == participants[draw_seed(_SEED, _BLOCK, _LIST_HASH) % 3]
|
|
|
|
|
assert winner == draw_winner(participants, _SEED, _BLOCK, _LIST_HASH)
|
|
|
|
|
assert 0 <= draw_index(draw_seed(_SEED, _BLOCK, _LIST_HASH), 3) < 3
|
2026-07-21 10:26:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_draw_winner_single_participant_always_wins():
|
Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
2026-10-14 16:29:09 +02:00
|
|
|
assert draw_winner(["only"], _SEED, "ff" * 32, _LIST_HASH) == "only"
|
2026-07-21 10:26:18 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_draw_winner_raises_on_empty_participants():
|
|
|
|
|
with pytest.raises(ValueError):
|
Make the draw a commit-reveal at a fixed, confirmed, quorum-agreed height with a public proof (B-73, RND-14 … RND-20, RND-33 … RND-37, DAT-61 … DAT-66, API-25, API-45, AUD-24 … AUD-27, AUD-37, ELC-15, SEC-56, ADR-24, ADR-25)
2026-10-14 16:29:09 +02:00
|
|
|
draw_winner([], _SEED, _BLOCK, _LIST_HASH)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_equal_odds_every_position_can_win():
|
|
|
|
|
"""RND-17: one position per participant, whatever the amount — the index is uniform
|
|
|
|
|
over 0..n-1 (checked over many seeds, not exactly)."""
|
|
|
|
|
from collections import Counter
|
|
|
|
|
|
|
|
|
|
from app.rounds.draw import draw_index, draw_seed
|
|
|
|
|
|
|
|
|
|
wins = Counter(draw_index(draw_seed(f"{i:064x}", _BLOCK, _LIST_HASH), 4) for i in range(2000))
|
|
|
|
|
assert set(wins) == {0, 1, 2, 3}
|
|
|
|
|
assert min(wins.values()) > 400 # about 500 each
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_server_seed_commitment_is_sha256_of_the_32_bytes():
|
|
|
|
|
import hashlib
|
|
|
|
|
|
|
|
|
|
from app.rounds.draw import server_seed_commitment
|
|
|
|
|
|
|
|
|
|
assert server_seed_commitment(_SEED) == hashlib.sha256(bytes.fromhex(_SEED)).hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_new_server_seeds_are_32_random_bytes_in_hex():
|
|
|
|
|
from app.rounds.draw import new_server_seed
|
|
|
|
|
|
|
|
|
|
seeds = {new_server_seed() for _ in range(20)}
|
|
|
|
|
assert len(seeds) == 20
|
|
|
|
|
assert all(len(seed) == 64 and bytes.fromhex(seed) for seed in seeds)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_participants_hash_encodes_one_txid_vout_address_line_per_participant():
|
|
|
|
|
"""RND-14: sha256 of `txid:vout:address\n` lines in draw_position order, UTF-8."""
|
|
|
|
|
import hashlib
|
|
|
|
|
|
|
|
|
|
from app.rounds.draw import participants_hash
|
|
|
|
|
|
|
|
|
|
rows = [("aa" * 32, 0, "plm1qfirst"), ("bb" * 32, 1, "plm1qsecond")]
|
|
|
|
|
text = f"{'aa' * 32}:0:plm1qfirst\n{'bb' * 32}:1:plm1qsecond\n"
|
|
|
|
|
assert participants_hash(rows) == hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
|
|
|
assert participants_hash(list(reversed(rows))) != participants_hash(rows) # the order is part of it
|
2026-10-14 15:45:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 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
|