#!/usr/bin/env python3
"""verify_donation_key.py -- check the PARALLAX "key that outlived its pool"
finding against YOUR OWN Bitcoin node. No trust in us required.

WHAT IT CHECKS
  1. Block 142,312 (2011-08-23) carries the donation key's first coinbase output.
  2. Block 302,467 (2014-05-24) -- the last P2PK coinbase output paying any
     OTHER key -- parses exactly as we published (a compressed key, 25.12 BTC).
  3. A deterministic 30-block sample of the 716 post-cutoff donation outputs:
     each block's coinbase must carry a P2PK output paying THE key, at the
     exact output index and satoshi value we published.
  4. THE RAZOR, live against your node's own UTXO set: the boundary outputs
     (516,592 spent / 519,817 unspent) and all 19 unspent outpoints via gettxout.

HONESTY NOTE
  The exclusivity claim (716 of 716) is an exhaustive census; checking it fully
  requires parsing every coinbase after block 302,467, which this script does
  not do -- it checks the sample above. The razor check, by contrast, is LIVE:
  if someone has spent a donation output since our stamp (2026-07-31, tip
  960,470), check 4 will fail. That would mean the razor MOVED after we
  published -- news, and grounds for our retraction policy -- not a parse error.

REQUIREMENTS: Bitcoin Core (txindex not required), Python 3.8+, stdlib only.
USAGE: python3 verify_donation_key.py [--rpcuser U --rpcpassword P]
AUTH order: CLI args, BITCOIN_RPC_USER/BITCOIN_RPC_PASSWORD env,
bitcoin.conf, .cookie. Exit 0 = every check passed against your node.
"""
import argparse, base64, json, os, sys, time, urllib.request

KEY = ("04ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d809"
       "0496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664b")
FIRST_USE = (142312, 25000008)          # height, sats
CUTOFF = (302467, 2511992894)           # last other-key P2PK coinbase output
CUTOFF_KEYPREFIX = "03fd358b50414a6d"   # compressed
BOUNDARY_SPENT = 516592                 # last spent donation output's block
BOUNDARY_UNSPENT = 519817               # first unspent donation output's block

# deterministic sample (every 24th) of the 716 post-cutoff donation outputs:
# (height, output_index, sats)
SAMPLE = [(302487, 377, 3359322), (307721, 406, 2641342), (311117, 452, 2186927),
          (314183, 511, 2136207), (315949, 405, 2232338), (317300, 406, 1409388),
          (319525, 348, 1062624), (321310, 309, 1641463), (323679, 385, 1882888),
          (325752, 351, 1577493), (326888, 343, 1727034), (328851, 344, 1589128),
          (330767, 337, 1631877), (333298, 323, 1935553), (335697, 334, 666208),
          (338163, 347, 1397327), (341607, 361, 1723105), (348720, 312, 1925456),
          (353812, 267, 2161576), (358009, 273, 1816861), (363253, 281, 2165092),
          (369162, 236, 2160715), (374733, 263, 3227591), (380228, 279, 3153256),
          (391859, 270, 4213276), (431114, 87, 798996), (448029, 161, 631551),
          (462490, 1, 1), (469190, 111, 1580158), (516592, 195, 98331)]

# the 19 unspent outpoints at our stamp (2026-07-31, tip 960,470): txid, vout, sats
UNSPENT = [
    ("4c048f80ae2e428f7b38da39f81094eeb4f18c8456a4f6fcf2bb80845b3988d2", 182, 181285),
    ("2b4580182f754ac58d76f9afb2b8058a82e15d25b9c8de3b39fae1209cc95f5e", 125, 209004),
    ("2fd900f736516e3ab7eabec6ac06443a7fed46f17e693d84ad02001355a6a57b", 110, 55543),
    ("e925aa5cd7bde031a9ee19e1bbae6ee8d6b9039fda58554ac5135d21e88637cf", 78, 55723),
    ("822f27a57ce98ad00e38634fd809a35f150002c6b0a0553f1a60151f6e66da9f", 144, 60797),
    ("603596f6f3621a1cc8f60786a5bb84cc32e9bfbede2d33d1f8c756cabdb7d160", 92, 10250),
    ("0d529bdd54bc450cb2aeaff65ab3fda7a755d69819934235fe420c4abbead41b", 69, 7618815),
    ("88e7cbcc53690e802472037443118febd91acf13236f6131307c365336029540", 88, 10696212),
    ("ea10c72d2568eb7e7504577c37c0767f201d6a4143fd060e016df7281e0d5334", 89, 10820972),
    ("12e24c4e8eee97202e1b0749123b293accc382d1e44d8b1674305c840343c4b3", 81, 10068189),
    ("db234ad5e0c21c481cde6030f581381d57ad761de0638757301806ffcb014ad7", 65, 11468809),
    ("b91d053ccd8e65482965110e11cabf5fae6d53765b10b10d2752eb50131a9dd4", 79, 11328891),
    ("62347d788a3f7002302470f378c05ed937107a45bcc338ae6e98d39bd37d9f50", 26, 2215957),
    ("9649e94d718a2b0d0693c024c15941c17576fe7ec44f436889416b72e650c927", 18, 8),
    ("18ec61988fa38475356105d46da7a7f9bcd02b8634eb2588f8e6905ac8c5991d", 39, 20),
    ("5a7508175f3d5645c961954c070a39fad802b7a483467ea16616cb69c6a87ec8", 52, 30),
    ("a5ec0fc307bec8caf43b5f21fc77614ff7bad2416e1cc6df8c25b2a97d809897", 46, 23),
    ("dee35c099d6c58f33c5810b76918cf2b4f0187507f29d80b22d0f5da3ce9f4f7", 33, 20),
    ("a5e166f25f3130acb114a32b166d6f2a1c05dec74d71fcdf2dab09f6d461dcf9", 53, 27),
]
UNSPENT_TOTAL = 64790575


def read_conf(datadir):
    out = {}
    try:
        for line in open(os.path.join(datadir, "bitcoin.conf")):
            line = line.strip()
            if line and not line.startswith("#") and "=" in line:
                k, v = line.split("=", 1)
                out[k.strip()] = v.strip()
    except OSError:
        pass
    return out


def resolve_auth(a, conf):
    if a.rpcuser and a.rpcpassword:
        return "%s:%s" % (a.rpcuser, a.rpcpassword), "command line"
    u, p = os.environ.get("BITCOIN_RPC_USER"), os.environ.get("BITCOIN_RPC_PASSWORD")
    if u and p:
        return "%s:%s" % (u, p), "environment"
    if conf.get("rpcuser") and conf.get("rpcpassword"):
        return "%s:%s" % (conf["rpcuser"], conf["rpcpassword"]), "bitcoin.conf"
    for c in (os.path.join(a.datadir, ".cookie"),
              os.path.join(a.datadir, "bitcoin", ".cookie")):
        if os.path.exists(c):
            try:
                return open(c).read().strip(), "cookie file"
            except OSError:
                pass
    return None, None


class RPC:
    def __init__(self, url, auth):
        self.url, self.n = url, 0
        self.auth = base64.b64encode(auth.encode()).decode()

    def call(self, method, params):
        body = json.dumps({"jsonrpc": "2.0", "id": 0, "method": method,
                           "params": params}).encode()
        req = urllib.request.Request(self.url, data=body,
                                     headers={"Authorization": "Basic " + self.auth,
                                              "Content-Type": "application/json"})
        with urllib.request.urlopen(req, timeout=180) as r:
            out = json.load(r)
        self.n += 1
        if out.get("error"):
            raise RuntimeError("RPC error: %s" % out["error"])
        return out["result"]


def p2pk_outputs(txjson):
    """(index, key_hex, sats) for every P2PK output, both forms."""
    outs = []
    for i, o in enumerate(txjson["vout"]):
        spk = o["scriptPubKey"]["hex"]
        if ((len(spk) == 134 and spk.startswith("41") and spk.endswith("ac")) or
                (len(spk) == 70 and spk.startswith("21") and spk.endswith("ac"))):
            outs.append((i, spk[2:-2], int(round(o["value"] * 1e8))))
    return outs


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--rpcport", type=int, default=8332)
    ap.add_argument("--rpchost", default="127.0.0.1")
    ap.add_argument("--rpcuser")
    ap.add_argument("--rpcpassword")
    ap.add_argument("--datadir", default=os.path.expanduser("~/.bitcoin"))
    a = ap.parse_args()

    conf = read_conf(a.datadir)
    auth, src = resolve_auth(a, conf)
    if not auth:
        print("no RPC credentials found (tried CLI, env, bitcoin.conf, cookie)")
        return 2
    host = conf.get("rpcconnect") if (a.rpchost == "127.0.0.1" and conf.get("rpcconnect")) \
        else a.rpchost
    rpc = RPC("http://%s:%d/" % (host, a.rpcport or int(conf.get("rpcport", 8332))), auth)
    print("auth from: %s" % src)
    info = rpc.call("getblockchaininfo", [])
    print("your node: chain=%s blocks=%d pruned=%s"
          % (info["chain"], info["blocks"], info.get("pruned")))
    failures = []
    t0 = time.time()

    def coinbase(h):
        return rpc.call("getblock", [rpc.call("getblockhash", [h]), 2])["tx"][0]

    # 1. first use
    outs = p2pk_outputs(coinbase(FIRST_USE[0]))
    hit = [o for o in outs if o[1] == KEY and o[2] == FIRST_USE[1]]
    print("\n[%s] block %d carries the donation key's first coinbase output (%d sats)"
          % ("PASS" if hit else "FAIL", FIRST_USE[0], FIRST_USE[1]))
    if not hit:
        failures.append("first use")

    # 2. the cutoff block
    outs = p2pk_outputs(coinbase(CUTOFF[0]))
    ok = (len(outs) == 1 and outs[0][1].startswith(CUTOFF_KEYPREFIX)
          and outs[0][2] == CUTOFF[1] and outs[0][1] != KEY)
    print("[%s] block %d: one P2PK output, %d sats, compressed key %s..., NOT the donation key"
          % ("PASS" if ok else "FAIL", CUTOFF[0], CUTOFF[1], CUTOFF_KEYPREFIX[:8]))
    if not ok:
        failures.append("cutoff block")

    # 3. the sample of the 716
    print("\nchecking a deterministic sample of the 716 post-cutoff donation outputs ...")
    bad = 0
    for h, idx, sats in SAMPLE:
        outs = p2pk_outputs(coinbase(h))
        hit = [o for o in outs if o[0] == idx and o[1] == KEY and o[2] == sats]
        if not hit:
            bad += 1
            print("   MISMATCH at h=%d (expected idx=%d sats=%d)" % (h, idx, sats))
    print("[%s] sample: %d/%d blocks carry the donation key exactly as published"
          % ("PASS" if bad == 0 else "FAIL", len(SAMPLE) - bad, len(SAMPLE)))
    if bad:
        failures.append("sample")

    # 4. the razor, LIVE against your UTXO set
    print("\nthe razor, against YOUR node's live UTXO set ...")
    spent_cb = coinbase(BOUNDARY_SPENT)
    spent_out = [o for o in p2pk_outputs(spent_cb) if o[1] == KEY]
    r = rpc.call("gettxout", [spent_cb["txid"], spent_out[0][0]]) if spent_out else "?"
    ok = spent_out and (r is None)
    print("[%s] block %d's donation output is SPENT (gettxout: %s)"
          % ("PASS" if ok else "FAIL", BOUNDARY_SPENT, "null" if r is None else "present"))
    if not ok:
        failures.append("spent boundary")

    bad, badkey, total = 0, 0, 0
    for txid, vout, sats in UNSPENT:
        r = rpc.call("gettxout", [txid, vout])
        if r is None:
            bad += 1
            print("   SPENT SINCE OUR STAMP: %s:%d (%d sats)" % (txid[:16], vout, sats))
        else:
            if KEY not in r["scriptPubKey"]["hex"]:
                badkey += 1
            total += int(round(r["value"] * 1e8))
    ok = (bad == 0 and badkey == 0 and total == UNSPENT_TOTAL)
    print("[%s] all 19 unspent outpoints still unspent, still the key, total %d sats "
          "(expected %d)" % ("PASS" if ok else "FAIL", total, UNSPENT_TOTAL))
    if bad:
        print("      NOTE: a spend AFTER our stamp (2026-07-31, tip 960,470) means the razor "
              "MOVED after publication -- that is news, not a parse error. See the finding's "
              "retraction policy.")
        failures.append("razor: %d outpoint(s) spent since our stamp" % bad)
    elif not ok:
        failures.append("razor: key or total mismatch")

    print("\n%s" % ("-" * 72))
    print("RPC calls: %d   %.0fs" % (rpc.n, time.time() - t0))
    print("NOTE: the 716/716 exclusivity claim is an exhaustive census; this script "
          "checks a 30-block sample of it plus the live razor. See the finding page "
          "for the full reproduction path.")
    if failures:
        print("RESULT: FAILED -- %s" % "; ".join(failures))
        return 1
    print("RESULT: ALL CHECKS PASSED against your own node.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
