niko_trust/tools/reference/pow_reference.py
Niko Marmeladkov 20cc52c3a5 feat: network layer — PoW, checkpoint chain, gossip, light node, WS, delegation, rotation, BFT
- BLAKE3 keyed proof-of-work on object storage and auth challenges,
  with frozen vectors cross-checked against an independent Python
  reference implementing the single-block hash it needs.
- Sparse Merkle trie over object IDs: order-independent roots,
  inclusion and absence proofs (internal/smt).
- Signed checkpoint chain per relay: transport key amendment to INV-1,
  /v1/checkpoint/* and inclusion/absence proof endpoints, restart-safe
  epoch continuity (internal/checkpoint).
- Head gossip with TOFU pinning and equivocation detection; light node
  (cmd/lightnode) that stores no history: quorum of pinned relays,
  every served object proven against the agreed root, LRU disk cache.
- WebSocket streaming on relay and light node (coder/websocket):
  scoped channels mirroring REST, raw envelopes verified client-side;
  light node marks streamed objects unproven until checkpoint coverage.
- Protocol v1 additions: DelegationClaim tag 0x07 with deterministic
  chain resolution in verify.Graph, KeyRotationRequest/Confirm tags
  0x08/0x09 with hash-bound two-sided consent and Policy.RotationMaxAge;
  spec sections, frozen vectors appended byte-identically, Python
  reference extended.
- Optional permissioned BFT finality over gossip (internal/bft):
  prevote/precommit with quorum certificates verifiable offline.
- Quick wins: Policy.TrustedIssuers, per-type stored metrics,
  batch fetch, lexicographic lists with stable cursor pagination.
- Security review of the network layer (docs/SECURITY-REVIEW.md) with
  findings F-01..F-09; hub send/close race and unstable pagination
  fixed under review.

12 packages green, vet/gofmt clean, protocol fuzzing stable.
2026-08-25 20:38:40 +03:00

230 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""Reference implementation of the relay's proof-of-work admission control.
Companion to docs/POW.md and the Go package internal/pow, with the same two
purposes as tce_reference.py: generate the frozen vectors in
testdata/vectors/pow_vectors.json, and be an independent implementation so
that agreement means "two readings of the specification coincide".
Scheme:
hash = BLAKE3_keyed(key, DOMAIN || target || counter_be)
valid when `hash` has at least `difficulty` leading zero bits.
Every message here is at most len(DOMAIN)+32+4 = 57 bytes, so the reference
implements exactly the BLAKE3 rule set for single-block inputs: key words as
the chaining value, one compression call over one padded 64-byte block, with
CHUNK_START | CHUNK_END | KEYED_HASH flags. That is complete for this input
class, not an approximation.
Run: python3 tools/reference/pow_reference.py --emit testdata/vectors
"""
from __future__ import annotations
import argparse
import json
import struct
from pathlib import Path
DOMAIN = b"trust.n1ko.dev/pow/1"
KEY_SIZE = 32
TARGET_SIZE = 32
MAX_DIFFICULTY = 30
# BLAKE3 flags for a keyed hash whose entire input is one block: the chunk
# starts and ends here, the whole input is the root node, and the hash is
# keyed.
FLAG_CHUNK_START = 1 << 0
FLAG_CHUNK_END = 1 << 1
FLAG_ROOT = 1 << 3
FLAG_KEYED_HASH = 1 << 4
SINGLE_BLOCK_FLAGS = FLAG_CHUNK_START | FLAG_CHUNK_END | FLAG_ROOT | FLAG_KEYED_HASH
IV = (
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
)
MSG_PERMUTATION = (2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8)
def _rotr(x: int, n: int) -> int:
return ((x >> n) | (x << (32 - n))) & 0xFFFFFFFF
def _g(s: list[int], a: int, b: int, c: int, d: int, mx: int, my: int) -> None:
s[a] = (s[a] + s[b] + mx) & 0xFFFFFFFF
s[d] = _rotr(s[d] ^ s[a], 16)
s[c] = (s[c] + s[d]) & 0xFFFFFFFF
s[b] = _rotr(s[b] ^ s[c], 12)
s[a] = (s[a] + s[b] + my) & 0xFFFFFFFF
s[d] = _rotr(s[d] ^ s[a], 8)
s[c] = (s[c] + s[d]) & 0xFFFFFFFF
s[b] = _rotr(s[b] ^ s[c], 7)
def _compress(cv: tuple[int, ...], block_words: list[int], block_len: int, flags: int) -> list[int]:
counter = 0 # first and only chunk
state = list(cv) + list(IV[:4]) + [
counter & 0xFFFFFFFF,
(counter >> 32) & 0xFFFFFFFF,
block_len,
flags,
]
m = list(block_words)
for r in range(7):
if r > 0:
m = [m[MSG_PERMUTATION[i]] for i in range(16)]
_g(state, 0, 4, 8, 12, m[0], m[1])
_g(state, 1, 5, 9, 13, m[2], m[3])
_g(state, 2, 6, 10, 14, m[4], m[5])
_g(state, 3, 7, 11, 15, m[6], m[7])
_g(state, 0, 5, 10, 15, m[8], m[9])
_g(state, 1, 6, 11, 12, m[10], m[11])
_g(state, 2, 7, 8, 13, m[12], m[13])
_g(state, 3, 4, 9, 14, m[14], m[15])
# Final transform: the first half becomes the new chaining value, the
# second half the block output. For a single-chunk root input the digest
# is the new chaining value: root_output_bytes streams all 16 words, and
# a 32-byte digest reads exactly the first eight.
for i in range(8):
state[i] ^= state[i + 8]
state[i + 8] ^= cv[i]
return state
def pow_hash(key: bytes, target: bytes, counter: int) -> bytes:
"""BLAKE3_keyed(key, DOMAIN || target || counter_be), 32-byte digest."""
if len(key) != KEY_SIZE:
raise ValueError("key must be 32 bytes")
if len(target) != TARGET_SIZE:
raise ValueError("target must be 32 bytes")
msg = DOMAIN + target + struct.pack(">I", counter)
if len(msg) > 64:
raise ValueError("reference covers single-block inputs only")
padded = msg + bytes(64 - len(msg))
block = list(struct.unpack("<16I", padded))
cv = struct.unpack("<8I", key)
words = _compress(cv, block, len(msg), SINGLE_BLOCK_FLAGS)
return struct.pack("<8I", *words[:8])
def leading_zero_bits(digest: bytes) -> int:
n = 0
for byte in digest:
if byte == 0:
n += 8
continue
mask = 0x80
while mask and not byte & mask:
n += 1
mask >>= 1
break
return n
def solve(key: bytes, target: bytes, difficulty: int) -> int:
counter = 0
while True:
if leading_zero_bits(pow_hash(key, target, counter)) >= difficulty:
return counter
counter += 1
# --------------------------------------------------------------------------
# Vector generation
# --------------------------------------------------------------------------
def fixture(seed: int) -> bytes:
return bytes([seed]) * KEY_SIZE
def emit(out_dir: Path) -> None:
vectors = []
rejects = []
# (key_seed, target_seed or None for zeros, difficulty)
cases = [
(1, 2, 1),
(1, 2, 4),
(1, 2, 8),
(2, 0, 8), # zero target: the authentication binding
(2, 3, 12),
(4, 5, 16),
]
for key_seed, target_seed, difficulty in cases:
key = fixture(key_seed)
target = bytes(TARGET_SIZE) if target_seed == 0 else fixture(target_seed)
counter = solve(key, target, difficulty)
name = "auth/zero-target" if target_seed == 0 else (
f"put/key-seed{key_seed:02x}-target-seed{target_seed:02x}"
)
vectors.append({
"name": f"{name}/diff{difficulty}",
"key_hex": key.hex(),
"target_hex": target.hex(),
"difficulty": difficulty,
"counter": counter,
"sum_hex": pow_hash(key, target, counter).hex(),
"leading_zero_bits": leading_zero_bits(pow_hash(key, target, counter)),
})
# Rejects derived from the first vector: off-by-one counter and wrong key.
base = cases[2]
key = fixture(base[0])
target = fixture(base[1])
good = solve(key, target, base[2])
bad_counter = good + 1
while leading_zero_bits(pow_hash(key, target, bad_counter)) >= base[2]:
bad_counter += 1
rejects.append({
"name": "counter-misses-threshold",
"key_hex": key.hex(),
"target_hex": target.hex(),
"difficulty": base[2],
"counter": bad_counter,
"reason": "leading_zero_bits < difficulty",
})
rejects.append({
"name": "wrong-key",
"key_hex": fixture(0xFF).hex(),
"target_hex": target.hex(),
"difficulty": base[2],
"counter": good,
"reason": "hash under a different key does not meet the target",
})
config_rejects = [
{
"name": "difficulty-above-max",
"difficulty": MAX_DIFFICULTY + 1,
"reason": "configuration above MaxDifficulty is clamped/rejected",
},
{
"name": "negative-difficulty",
"difficulty": -1,
"reason": "negative configuration is treated as disabled, never as a pass-all proof tier",
},
]
doc = {
"domain": DOMAIN.decode(),
"hash_spec": "BLAKE3_keyed(key, domain || target || counter_be); valid iff leading_zero_bits(sum) >= difficulty",
"max_difficulty": MAX_DIFFICULTY,
"vectors": vectors,
"rejects": rejects,
"config_rejects": config_rejects,
}
out_dir.mkdir(parents=True, exist_ok=True)
path = out_dir / "pow_vectors.json"
path.write_text(json.dumps(doc, indent=2) + "\n")
print(f"wrote {path} ({len(vectors)} vectors, {len(rejects)} rejects)")
if __name__ == "__main__":
ap = argparse.ArgumentParser()
ap.add_argument("--emit", default="testdata/vectors", help="output directory")
args = ap.parse_args()
emit(Path(args.emit))