#!/usr/bin/env python3
"""
Independent runner for https://aere.network/verifiable-claims.json

Runs the checkable claims in that file against the live Aere Network endpoints and
prints PASS or FAIL for each. It trusts nothing in the file except the addresses and
the expected values, which is the point: if Aere is lying, this script says FAIL.

Revision 2 of this script (2026-08-05) carried typed numbers for the things that
move (validator count, seal count, block interval, transaction pool) and went stale
in silence: on 2026-09-05 it failed four of its own checks against a healthy chain.
Since revision 3 those expectations are READ FROM THE CLAIMS FILE ITSELF, so the
file and this script cannot drift apart, and every one of them is a value the
claims file derives from the chain, not a number anyone typed. Everything fixed by
a standard (EVM gas costs, NIST vectors, EIP-1967 slots) stays fixed here.

  python verifiable-claims-check.py

No wallet, no key, no gas, no account. All calls are read-only.
Requires only the Python standard library.
"""
import json, urllib.request, hashlib, math, os, re, sys

ENDPOINTS = ["https://rpc.aere.network", "https://rpc2.aere.network"]
CLAIMS_URL = "https://aere.network/verifiable-claims.json"
UA = "aere-claims-check/3"          # NOTE: default Python-urllib UA is 403'd by the CDN
PASS, FAIL = [], []

def rpc(method, params, url=ENDPOINTS[0]):
    body = json.dumps({"jsonrpc": "2.0", "method": method, "params": params, "id": 1}).encode()
    req = urllib.request.Request(url, data=body,
                                 headers={"content-type": "application/json", "User-Agent": UA})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())

def check(name, got, want, note=""):
    ok = (got == want)
    (PASS if ok else FAIL).append(name)
    print(f"  [{'PASS' if ok else 'FAIL'}] {name}")
    if not ok:
        print(f"         expected {want!r}")
        print(f"         got      {got!r}")
    elif note:
        print(f"         {note}")
    return ok

# ------------------------------------------------------------ 0. the claims file
# Fetched with a cache-busting parameter so the CDN cannot hand back yesterday's copy.
print("\n0. The claims file this script checks against")
req = urllib.request.Request(f"{CLAIMS_URL}?o={os.getpid()}", headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=30) as r:
    CLAIMS = json.loads(r.read())
def claim(cid):
    for c in CLAIMS.get("claims", []) + CLAIMS.get("notClaimed", []):
        if c.get("id") == cid:
            return c
    raise SystemExit(f"claims file has no entry '{cid}'; this script and the file are out of step")
check("claims file parses and names its revision", isinstance(CLAIMS.get("revision"), int), True,
      f"revision {CLAIMS.get('revision')}, generated {CLAIMS.get('generatedAt')}, "
      f"{len(CLAIMS.get('claims', []))} claims, {len(CLAIMS.get('notClaimed', []))} things not claimed")

# ---------------------------------------------------------------- RLP (headers)
def rlp(b, i=0):
    p = b[i]
    if p <= 0x7f:  return ("s", b[i:i+1], i+1)
    if p <= 0xb7:  L = p-0x80; return ("s", b[i+1:i+1+L], i+1+L)
    if p <= 0xbf:
        n = p-0xb7; L = int.from_bytes(b[i+1:i+1+n], 'big')
        return ("s", b[i+1+n:i+1+n+L], i+1+n+L)
    if p <= 0xf7: n, L = 0, p-0xc0
    else:
        n = p-0xf7; L = int.from_bytes(b[i+1:i+1+n], 'big')
    start = i+1+n; end = start+L; items = []; j = start
    while j < end:
        t, v, j = rlp(b, j); items.append((t, v))
    return ("l", items, end)

def decode_extra(hexstr):
    # A QBFT extraData list has AT LEAST five items. Some headers carry a sixth (the
    # post-quantum certificate, see `pq-certificate-under-block-hash` in the claims
    # file); a decoder that insists on exactly five throws on those. Read the first
    # five, ignore any remainder.
    _, items, _ = rlp(bytes.fromhex(hexstr[2:]))
    return items[0][1], ["0x"+v[1].hex() for v in items[1][1]], items[4][1], len(items)

# ------------------------------------------------------------------ 1. identity
print("\n1. Chain identity")
check("chainId == 0xaf0", rpc("eth_chainId", [])["result"], "0xaf0")
check("net_version == 2800", rpc("net_version", [])["result"], "2800")
check("not syncing", rpc("eth_syncing", [])["result"], False)
cv = rpc("web3_clientVersion", [])["result"]
check("client is Besu", cv.startswith("besu/"), True, cv)

# --------------------------------------------------------------- 2. block time
# The band is the one the claims file published from its own 100,000-block
# measurement, on the date it names. A band typed here went stale within a month.
print("\n2. Block interval over 100,000 blocks")
head = rpc("eth_getBlockByNumber", ["latest", False])["result"]
n = int(head["number"], 16)
old = rpc("eth_getBlockByNumber", [hex(n-100000), False])["result"]
dt = (int(head["timestamp"], 16) - int(old["timestamp"], 16)) / 100000
band = re.search(r"between\s+([0-9.]+)\s+and\s+([0-9.]+)", str(claim("block-interval")["verify"]["expect"]))
if band:
    lo, hi = float(band.group(1)), float(band.group(2))
    check(f"{lo:.2f} <= mean block interval <= {hi:.2f} s (band published by the claims file)",
          lo <= dt <= hi, True, f"measured {dt:.4f} s/block over blocks {n-100000:,} to {n:,}")
else:
    check("claims file states a block-interval band", "no band found", "between A and B")

# ------------------------------------------------- 3. validator set from header
# The count and the seal count are what the claims file derived from a header on
# its own run; the quorum is recomputed here as ceil(2n/3), not copied.
print("\n3. QBFT validator set and quorum, decoded from the block header")
exp = claim("qbft-validator-set-from-header")["verify"]["expect"]
want_n, want_seals, want_bytes = exp["validators_count"], exp["committedSeals_count"], exp["each_seal_bytes"]
vanity, vals, seals, nitems = decode_extra(head["extraData"])
check(f"validator count == {want_n} (the count the claims file derived from a header)", len(vals), want_n)
q = math.ceil(2*len(vals)/3)
check(f"quorum ceil(2n/3) recomputed locally == committed seals count the claims file derived", q, want_seals)
check(f"committed seals on the head >= quorum {q}", len(seals) >= q, True, f"{len(seals)} seals on block {n:,}")
check(f"every seal is {want_bytes} bytes", {len(s[1]) for s in seals}, {want_bytes})
sets, sealcounts, sixth = set(), set(), 0
for k in range(0, 200, 10):
    b = rpc("eth_getBlockByNumber", [hex(n-k), False])["result"]
    _, vv, ss, ni = decode_extra(b["extraData"])
    sets.add(tuple(vv)); sealcounts.add(len(ss)); sixth += (ni >= 6)
check("validator set identical across 20 sampled blocks", len(sets), 1)
check(f"seal count >= quorum {q} on every sampled block", min(sealcounts) >= q, True,
      f"counts seen: {sorted(sealcounts)}; {sixth} of the 20 sampled headers carried a sixth extraData item")

# ------------------------------------------------------ 4. precompiles declared
print("\n4. Post-quantum precompiles declared by the node")
cfg = rpc("eth_config", [])["result"]["current"]
pc = cfg["precompiles"]
for nm, ad in [("AereFalcon512", "ae1"), ("AereFalcon1024", "ae2"), ("AereMLDSA44", "ae3"),
               ("AereSLHDSA128s", "ae4"), ("AereSHAKE256", "ae5")]:
    check(f"{nm} at 0x0{ad.upper()}", pc.get(nm), "0x"+"0"*37+ad)
check("0x0AE6 / 0x0AE7 NOT declared (testnet only)",
      [k for k in pc if k in ("AereMLKEM", "AereHashToPoint")], [])
check("fork activationTime", cfg["activationTime"], 1783820272)

# ------------------------------------------------------------ 5. SHAKE256 KAT
print("\n5. NIST SHAKE256 known-answer test at 0x0AE5")
want = "0x" + hashlib.shake_256(b"").hexdigest(32)   # computed locally, not taken from Aere
for ep in ENDPOINTS:
    got = rpc("eth_call", [{"to": "0x"+"0"*37+"AE5",
                            "data": "0x"+"0"*62+"20"}, "latest"], ep)["result"]
    check(f"SHAKE256(empty,32) matches local computation [{ep.split('//')[1]}]", got, want)

# ------------------------------------------- 6. precompiles execute (gas probe)
#
# One address literal, so there is nothing to substitute twice:
#   GAS; STATICCALL(0x0f4240, target, args 0..32, ret 0..32); POP; GAS; SWAP1; SUB;
#   PUSH1 0; MSTORE; RETURN 32
#
# The probe's own overhead is NOT one number. EIP-2929 pre-warms precompiles 0x01
# to 0x09 (access cost 100); every other address is cold (access cost 2600). The
# Aere band at 0x0AE1+ is NOT in the pre-warmed set, so it pays the cold price.
# Compare each target only against a control of the same warmth, or you will read
# the identity precompile as 2,482 gas CHEAPER than an empty address and conclude
# something silly.
print("\n6. Precompiles consume gas (positive proof of execution)")
def gas(addr20, ep=ENDPOINTS[0]):
    code = ("0x5a" "6020" "6000" "6020" "6000" "73" + addr20 + "620f4240" "fa"
            "50" "5a" "9003" "600052" "60206000f3")
    r = rpc("eth_call", [{"to": "0x"+"0"*36+"b0b0", "data": "0x", "gas": "0x1c9c380"},
                         "latest", {"0x"+"0"*36+"b0b0": {"code": code}}], ep)
    return int(r["result"], 16)

WARM, COLD = 125, gas("0"*36+"dead")
check("codeless control (cold address) costs the bare probe overhead", COLD, 2625)
# Calibration. These two costs are fixed by the EVM specification, not by Aere.
# A probe nobody has calibrated is not a measurement, it is a number.
check("IDENTITY 0x04 costs exactly 18 (EVM spec: 15+3*1)",  gas("0"*39+"4") - WARM, 18)
check("SHA-256  0x02 costs exactly 72 (EVM spec: 60+12*1)", gas("0"*39+"2") - WARM, 72)
for nm, ad, exp in [("Falcon-512 0x0AE1",   "0"*37+"ae1", 40000),
                    ("Falcon-1024 0x0AE2",  "0"*37+"ae2", 75000),
                    ("ML-DSA-44 0x0AE3",    "0"*37+"ae3", 55000),
                    ("SLH-DSA-128s 0x0AE4", "0"*37+"ae4", 350000),
                    ("SHAKE256 0x0AE5",     "0"*37+"ae5", 60)]:
    check(f"{nm} consumes {exp} gas above the cold control", gas(ad) - COLD, exp)
# Honest scope. A zero here is CONSISTENT with absence and is not proof of it: a
# precompile that charged nothing would read the same. Revision 1 of the claims
# file called this proof of absence; that was an overstatement and is withdrawn.
# The positive statement of absence is the eth_config enumeration in section 4.
for ad in ("0"*37+"ae6", "0"*37+"ae7"):
    check(f"0x{ad[-3:].upper()} costs 0 above control (consistent with absence, NOT proof)",
          gas(ad) - COLD, 0, "absence is asserted from eth_config in section 4, not from this")

# ------------------------------------------------------- 7. EVM opcode parity
print("\n7. Cancun opcode set, with a negative control")
A = "0x" + "0"*38 + "c0"
def run(code):
    r = rpc("eth_call", [{"to": A, "data": "0x"}, "latest", {A: {"code": code}}])
    return r.get("result"), r.get("error", {}).get("message", "")
for nm, code, exp in [("PUSH0 (EIP-3855)",           "0x602a5f5260205ff3",            0x2a),
                      ("TSTORE/TLOAD (EIP-1153)",    "0x604260015d60015c60005260206000f3", 0x42),
                      ("MCOPY (EIP-5656)",           "0x60426020526020602060005e60206000f3", 0x42),
                      ("CHAINID (EIP-1344)",         "0x4660005260206000f3",          2800)]:
    res, err = run(code)
    check(nm, int(res, 16) if res else err, exp)
for nm, code in [("BLOBBASEFEE (EIP-7516) executes", "0x4a60005260206000f3"),
                 ("BLOBHASH (EIP-4844) executes",    "0x60004960005260206000f3")]:
    res, err = run(code)
    check(nm, "Invalid opcode" not in err and res is not None, True, "no Invalid-opcode error")
for op in ("0c", "21", "a5"):
    res, err = run("0x"+op+"60005260206000f3")
    check(f"NEGATIVE CONTROL: undefined opcode 0x{op} must fail", "Invalid opcode" in err, True)

# --------------------------------------------------- 8. contract code + proxies
print("\n8. Deployed bytecode and EIP-1967 proxy slots")
SLOTS = {"implementation": "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
         "admin":          "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103",
         "beacon":         "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"}
CONTRACTS = {
    "AereFeeBurnVault":       ("0x696afDF4f814e6Fd6aa45CE14C498ed9375fB2c6", 1313),
    "AereSink":               ("0x69581B86A48161b067Ff4E01544780625B231676", 4164),
    "AereCoinbaseSplitter":   ("0xb4b0eCe9011613A5b84248a9B42a0f309E6F01Ec", 2614),
    "AereFalcon512Verifier":  ("0x4E8e9682329e646784fB3bd01430aA4bA54D8fFC", 7333),
    "AereFalcon1024Verifier": ("0xF0aFA59BaB2058e4B6e6B424b7f76750F1F66e36", 7334),
    "AereMLDSA44Verifier":    ("0xf1F7A6Acd82D5DAf9AF3166a2F736EE52C5F85AE", 8654),
    "AereCancunCanary":       ("0x8DbFC002bB23124cBeCd7B4916c179D2AFd65498", 1541),
    "AereKZGVerifier":        ("0x6596307BD8f54d9A91FE364EBC3e594F200AC862", 1988),
    "AerePQCAttestation":     ("0x465d9e3b476bf98aa1393079e240db5d2a9bea6a", 4402),
}
zero = "0x" + "0"*64
for nm, (ad, size) in CONTRACTS.items():
    code = rpc("eth_getCode", [ad, "latest"])["result"]
    check(f"{nm} code is {size} bytes", len(code)//2 - 1, size)
    slots = [rpc("eth_getStorageAt", [ad, s, "latest"])["result"] for s in SLOTS.values()]
    check(f"{nm} is not an EIP-1967 proxy", set(slots), {zero})

# ------------------------------------------------- 9. burn vault has no owner
print("\n9. Burn vault administration surface")
V = "0x696afDF4f814e6Fd6aa45CE14C498ed9375fB2c6"
code = rpc("eth_getCode", [V, "latest"])["result"]
def push4(c):
    b = bytes.fromhex(c[2:]); out = set(); i = 0
    while i < len(b):
        op = b[i]
        if op == 0x63 and i+5 <= len(b): out.add(b[i+1:i+5].hex()); i += 5; continue
        if 0x60 <= op <= 0x7f: i += 1 + (op-0x5f); continue
        i += 1
    return out
sels = push4(code)
for nm, s in [("owner()", "8da5cb5b"), ("transferOwnership(address)", "f2fde38b"),
              ("renounceOwnership()", "715018a6"), ("upgradeTo(address)", "3659cfe6"),
              ("admin()", "f851a440")]:
    check(f"burn vault does NOT expose {nm}", s in sels, False)
check("burn vault DOES expose sweepToZero()", "579663e2" in sels, True)
r = rpc("eth_call", [{"to": V, "data": "0x579663e2",
                      "from": "0x"+"0"*36+"dEaD"}, "latest"])
check("sweepToZero() is permissionless (no revert from an arbitrary caller)",
      "error" not in r, True)

# -------------------------------------- 10. burn rate is a parameter, not a law
print("\n10. Burn rate is owner-settable (this REFUTES 'no admin' copy)")
SP = "0xb4b0eCe9011613A5b84248a9B42a0f309E6F01Ec"
spc = rpc("eth_getCode", [SP, "latest"])["result"]
ssel = push4(spc)
check("splitter EXPOSES owner()", "8da5cb5b" in ssel, True)
check("splitter EXPOSES transferOwnership()", "f2fde38b" in ssel, True)
own = rpc("eth_call", [{"to": SP, "data": "0x8da5cb5b"}, "latest"])["result"]
owner = "0x" + own[-40:]
check("splitter owner is 0x0243a4f4..f3c3", owner, "0x0243a4f47d44b40b65d33f20329de20d00c6f3c3")
ocode = rpc("eth_getCode", [owner, "latest"])["result"]
check("splitter owner is a plain EOA, NOT a multisig or timelock", ocode, "0x",
      "a single key can change the burn rate")

# ------------------------------------------- 11. burn is not currently flowing
print("\n11. Is the burn engine actually burning? (we claim it is NOT)")
# The state window is 512 blocks and its edge is a RACE: `n` was captured when this
# script started, so n-500 may already be out of window by the time we get here.
# Re-read the head NOW and step back 400, safely inside the window.
hnow = int(rpc("eth_blockNumber", [])["result"], 16)
now = int(rpc("eth_getBalance", [V, "latest"])["result"], 16)
was = rpc("eth_getBalance", [V, hex(hnow-400)])["result"]
if was is None:
    check("burn vault balance comparison", "state pruned, NOT MEASURED", "a value", "")
else:
    check("burn vault balance UNCHANGED over 400 blocks",
          now - int(was, 16), 0,
          f"balance {now} wei at both ends; nothing is being routed in")
check("block proposer is a validator, not the splitter",
      head["miner"].lower() == SP.lower(), False, f"miner = {head['miner']}")

# -------------------------------------------------------- 12. receipts survive
print("\n12. Historical receipts (full history, beyond the 512-block state window)")
for h, blk in [("0x3a1838deb9f1b4b26f7af5ef1b4609a8aea77a3ebe654e38151caeadfff4fd5c", 8735205),
               ("0xcd1350372f0a61e26e8a3228da599be9211839f378eef0ea49f2025a20920660", 9200532)]:
    r = rpc("eth_getTransactionReceipt", [h])["result"]
    check(f"receipt {h[:12]}... status success at block {blk}",
          (r["status"], int(r["blockNumber"], 16)) if r else None, ("0x1", blk))

# ------------------------------------- 13. the chain is idle, and the TPS number
# Added in revision 2, after a hostile read of revision 1 pointed out that every
# performance-adjacent figure here was measured on an empty chain and never said
# so. These checks PASS by confirming things that are bad for Aere. That is the
# point: a file that can only produce good news cannot be used to catch us.
# Revision 3: the transaction-pool expectation is read from the claims file, which
# states what txpool_status returned on its own run; revision 2 typed "a backlog
# exists" and that stopped being true.
print("\n13. Load and throughput (we claim the chain is IDLE and the TPS number is not derivable)")
h13 = int(rpc("eth_blockNumber", [])["result"], 16)
tot, empty, sampled = 0, 0, 0
for i in range(0, 3000, 250):
    b = rpc("eth_getBlockByNumber", [hex(h13-i), False])["result"]
    sampled += 1; tot += len(b["transactions"])
    if b["gasUsed"] == "0x0": empty += 1
check("sampled blocks are nearly all empty (idle chain)", empty >= sampled - 1, True,
      f"{sampled} headers across the most recent 3,000 blocks, {empty} with gasUsed 0x0, {tot} transactions in total")
tp = rpc("txpool_status", [])["result"]
pool = re.search(r"pending\s+(0x[0-9a-f]+)\s+and\s+queued\s+(0x[0-9a-f]+)", str(claim("no-mainnet-traffic").get("theTruth", "")))
if pool:
    check(f"txpool_status matches what the claims file measured (pending {pool.group(1)}, queued {pool.group(2)})",
          (int(tp["pending"], 16), int(tp["queued"], 16)), (int(pool.group(1), 16), int(pool.group(2), 16)),
          f"pending={int(tp['pending'],16)}, queued={int(tp['queued'],16)}")
else:
    check("claims file states a txpool measurement", "not found", "pending 0x.. and queued 0x..")
gl = int(rpc("eth_getBlockByNumber", ["latest", False])["result"]["gasLimit"], 16)
check("gasLimit is 2^53-1, a JavaScript artifact and not an engineering parameter",
      gl, 2**53-1,
      "so a 'gas limit / block time' TPS derivation does not produce any published "
      "number; it produces ~8e11. No throughput number is claimed.")

# ------------------------------------------------------------------ verdict
print("\n" + "="*66)
print(f"  PASS {len(PASS)}   FAIL {len(FAIL)}")
if FAIL:
    print("  FAILED:", ", ".join(FAIL))
print("="*66)
print("\n  Sections 10, 11 and 13 are deliberately unflattering to Aere: they PASS by")
print("  confirming that the burn rate is settable by one key, that nothing is being")
print("  burned, and that the chain is idle. A claims file that can only produce good")
print("  news is a brochure. This one is checkable in both directions.\n")
sys.exit(1 if FAIL else 0)
