#!/usr/bin/env python3
"""verify.py -- independently verify a pool.zcd.lol ledger day against the on-chain anchor.

No dependencies beyond the Python standard library. Reads ONLY:
  - the published day file (https://pool.zcd.lol/ledger/YYYY-MM-DD.jsonl)
  - the chain, through ANY Zycord node's read-only /storage endpoint

Checks, in order:
  1. day_hash    : sha256 of the day file bytes == dayHash cell
  2. arithmetic  : every line's split sums plus unattributed == amount;
                   file line count and total drops == dayMeta cell
  3. chain link  : sha256("zcd-pool-ledger-v1|D|day_hash|n|drops|prev_chain") == chainHash cell,
                   where prev_chain is read from the chain (zeroes for the genesis day)

Usage:
  python3 verify.py --day 2026-11-03
  python3 verify.py --day 2026-11-03 --node http://127.0.0.1:9420 \
                    --contract 0x05fd1af1dffa1f71313f092347ae7f51f39d50864e9a1d94d8cc8295e46b0337
  python3 verify.py --day 2026-11-03 --file ./2026-11-03.jsonl   # verify a local copy

Exit code 0 = every check passed.
"""
import argparse, hashlib, json, sys, urllib.request
from datetime import datetime, timezone

DAY_FILE_URL = "https://pool.zcd.lol/ledger/{day}.jsonl"
ANCHORS_URL = "https://pool.zcd.lol/api/v1/ledger/anchors.json"
RECIPE = "zcd-pool-ledger-v1"
SLOTS = {"dayHash": 1, "chainHash": 2, "dayMeta": 3}

# ------------------------------------------------------------------ keccak-256
def _keccak_f(state):
    RC = [0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000,
          0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
          0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
          0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003,
          0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A,
          0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008]
    R = [[0, 36, 3, 41, 18], [1, 44, 10, 45, 2], [62, 6, 43, 15, 61],
         [28, 55, 25, 21, 56], [27, 20, 39, 8, 14]]
    M = (1 << 64) - 1
    for rnd in range(24):
        C = [state[x][0] ^ state[x][1] ^ state[x][2] ^ state[x][3] ^ state[x][4] for x in range(5)]
        D = [C[(x - 1) % 5] ^ ((C[(x + 1) % 5] << 1) & M) ^ (C[(x + 1) % 5] >> 63) for x in range(5)]
        for x in range(5):
            for y in range(5):
                state[x][y] ^= D[x]
        B = [[0] * 5 for _ in range(5)]
        for x in range(5):
            for y in range(5):
                v = state[x][y]
                B[y][(2 * x + 3 * y) % 5] = ((v << R[x][y]) & M) ^ (v >> (64 - R[x][y]))
        for x in range(5):
            for y in range(5):
                state[x][y] = B[x][y] ^ ((~B[(x + 1) % 5][y]) & M) & B[(x + 2) % 5][y]
        state[0][0] ^= RC[rnd]
    return state

def keccak256(data: bytes) -> bytes:
    rate = 136
    state = [[0] * 5 for _ in range(5)]
    padded = bytearray(data)
    padded.append(0x01)
    while len(padded) % rate != 0:
        padded.append(0x00)
    padded[-1] ^= 0x80
    for off in range(0, len(padded), rate):
        blk = padded[off:off + rate]
        for i in range(rate // 8):
            state[i % 5][i // 5] ^= int.from_bytes(blk[i * 8:(i + 1) * 8], "little")
        _keccak_f(state)
    out = bytearray()
    for i in range(4):
        out += state[i % 5][i // 5].to_bytes(8, "little")
    return bytes(out)

assert keccak256(b"abc").hex() == "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"

# ------------------------------------------------------------------ helpers
def pad32(v: int) -> bytes:
    return v.to_bytes(32, "big")

def mapping_key(slot: int, key: int) -> str:
    return keccak256(pad32(key) + pad32(slot)).hex()

def fetch(url: str, timeout: int = 30) -> bytes:
    with urllib.request.urlopen(url, timeout=timeout) as r:
        return r.read()

def storage_value(node: str, contract: str, key_hex: str) -> int:
    d = json.loads(fetch(f"{node}/storage?addr={contract}&key=0x{key_hex}"))
    v = d.get("value")
    return 0 if v in (None, "", "0") else int(v)

def day_number(day: str) -> int:
    dt = datetime.strptime(day, "%Y-%m-%d").replace(tzinfo=timezone.utc)
    return int(dt.timestamp()) // 86400

# ------------------------------------------------------------------ checks
def main():
    ap = argparse.ArgumentParser(description="verify a pool.zcd.lol ledger day on-chain")
    ap.add_argument("--day", required=True, help="UTC day, YYYY-MM-DD")
    ap.add_argument("--node", default="http://127.0.0.1:9420", help="any Zycord node RPC (read-only)")
    ap.add_argument("--contract", default=None, help="PoolLedger contract (default: from anchors.json)")
    ap.add_argument("--file", default=None, help="verify a local copy of the day file")
    a = ap.parse_args()

    day_num = day_number(a.day)
    contract = a.contract
    if not contract:
        contract = json.loads(fetch(ANCHORS_URL))["contract"]
        print(f"contract (from anchors.json): {contract}")

    if a.file:
        blob = open(a.file, "rb").read()
        print(f"day file: {a.file} (local, {len(blob)} bytes)")
    else:
        url = DAY_FILE_URL.format(day=a.day)
        blob = fetch(url)
        print(f"day file: {url} ({len(blob)} bytes)")

    day_hash = hashlib.sha256(blob).hexdigest()
    print(f"day_hash: {day_hash}")

    on_day = storage_value(a.node, contract, mapping_key(SLOTS["dayHash"], day_num))
    print(f"[1/3] dayHash cell: {on_day:064x}")
    if on_day != int(day_hash, 16):
        print("FAIL: published day file hash does NOT match the on-chain anchor")
        sys.exit(1)
    print("      PASS")

    lines = [json.loads(l) for l in blob.decode().splitlines() if l.strip()]
    bad = [l for l in lines
           if sum(int(v) for v in (l.get("split") or {}).values()) + int(l.get("unattributed") or 0)
              != int(l["amount"])]
    n, drops = len(lines), sum(int(l["amount"]) for l in lines)
    if bad:
        print(f"FAIL: {len(bad)} lines where split+unattributed != amount")
        sys.exit(1)
    print(f"[2/3] arithmetic: {n} lines, {drops} drops, every split reconciles to the drop")
    on_n, on_drops = 0, 0
    meta = storage_value(a.node, contract, mapping_key(SLOTS["dayMeta"], day_num))
    on_n, on_drops = meta >> 128, meta & ((1 << 128) - 1)
    print(f"      dayMeta cell: payouts={on_n} drops={on_drops}")
    if (on_n, on_drops) != (n, drops):
        print("FAIL: file totals do not match the dayMeta cell")
        sys.exit(1)
    print("      PASS")

    prev = storage_value(a.node, contract, mapping_key(SLOTS["chainHash"], day_num - 1))
    rec = f"{RECIPE}|{day_num}|{day_hash}|{n}|{drops}|{prev:064x}"
    want = hashlib.sha256(rec.encode()).hexdigest()
    on_chain = storage_value(a.node, contract, mapping_key(SLOTS["chainHash"], day_num))
    print(f"[3/3] chain link: prev chainHash(D-1) = {prev:064x}")
    print(f"      chainHash cell: {on_chain:064x}")
    print(f"      recomputed:     {want}")
    if on_chain != int(want, 16):
        print("FAIL: chain hash does not link to the previous day as published")
        sys.exit(1)
    print("      PASS")

    print(f"\nALL CHECKS PASSED: day {a.day}'s published ledger is byte-for-byte what the chain anchors,")
    print("and its chain hash links to the previous day -- history cannot be rewritten silently.")
    print("This run read the chain through " + a.node + "; point --node at any node you trust.")

if __name__ == "__main__":
    main()
