#!/usr/bin/env python3
"""
verify-anchor.py — independently verify an Origin Neural discovery anchor.

Usage:
    python3 verify-anchor.py <bsv-txid>
    python3 verify-anchor.py            # verifies a known sample anchor

What it does, with no dependencies beyond the Python standard library:

  1. Fetches the transaction from a public BSV API (WhatsOnChain).
  2. Decodes the OP_RETURN payload: batch id, Merkle root, record count, timestamp.
  3. Recovers the public key from the AIP ECDSA signature over that payload,
     derives its Bitcoin address, and compares it to the address the payload claims.

A VALID result proves the payload was authored by the holder of the named key and
has been immutable since its block was mined. It proves nothing about whether the
predictions inside the batch are biologically correct.

Deliberately dependency-free so you can audit every line before running it.
Public domain — reuse freely.
"""

import base64
import hashlib
import json
import sys
import urllib.request

API = "https://api.whatsonchain.com/v1/bsv/main/tx/hash/"
SAMPLE = "beddf6af2913496df900aff5585399581f384b500ac3422dad1d6973fe61100b"

# --------------------------------------------------------------------------
# secp256k1 — public key recovery
# --------------------------------------------------------------------------
P = 2**256 - 2**32 - 977
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8


def _inv(a, m):
    return pow(a, m - 2, m)


def _add(p1, p2):
    if p1 is None:
        return p2
    if p2 is None:
        return p1
    x1, y1 = p1
    x2, y2 = p2
    if x1 == x2 and (y1 + y2) % P == 0:
        return None
    if p1 == p2:
        lam = (3 * x1 * x1) * _inv(2 * y1, P) % P
    else:
        lam = (y2 - y1) * _inv((x2 - x1) % P, P) % P
    x3 = (lam * lam - x1 - x2) % P
    return (x3, (lam * (x1 - x3) - y1) % P)


def _mul(k, pt):
    r = None
    while k:
        if k & 1:
            r = _add(r, pt)
        pt = _add(pt, pt)
        k >>= 1
    return r


def _decompress(x, odd):
    y2 = (pow(x, 3, P) + 7) % P
    y = pow(y2, (P + 1) // 4, P)
    if (y & 1) != odd:
        y = P - y
    if (y * y - y2) % P != 0:
        raise ValueError("recovered point is not on the curve")
    return (x, y)


# --------------------------------------------------------------------------
# Bitcoin address + signed-message helpers
# --------------------------------------------------------------------------
_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"


def _b58check(payload: bytes) -> str:
    chk = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
    raw = payload + chk
    num = int.from_bytes(raw, "big")
    out = ""
    while num:
        num, rem = divmod(num, 58)
        out = _B58[rem] + out
    return "1" * (len(raw) - len(raw.lstrip(b"\x00"))) + out


def _address(pubkey: bytes) -> str:
    h = hashlib.new("ripemd160", hashlib.sha256(pubkey).digest()).digest()
    return _b58check(b"\x00" + h)


def _varint(n: int) -> bytes:
    if n < 0xFD:
        return bytes([n])
    if n <= 0xFFFF:
        return b"\xfd" + n.to_bytes(2, "little")
    if n <= 0xFFFFFFFF:
        return b"\xfe" + n.to_bytes(4, "little")
    return b"\xff" + n.to_bytes(8, "little")


def _msg_hash(msg: bytes) -> bytes:
    prefix = b"\x18Bitcoin Signed Message:\n"
    return hashlib.sha256(hashlib.sha256(prefix + _varint(len(msg)) + msg).digest()).digest()


def recover_address(msg: bytes, sig_b64: str) -> str:
    """Recover the signing address from a Bitcoin signed-message signature."""
    sig = base64.b64decode(sig_b64)
    if len(sig) != 65:
        raise ValueError(f"expected 65-byte signature, got {len(sig)}")
    header = sig[0]
    if not 27 <= header <= 34:
        raise ValueError(f"unexpected signature header byte {header}")

    recid = (header - 27) & 3
    compressed = ((header - 27) & 4) != 0
    r = int.from_bytes(sig[1:33], "big")
    s = int.from_bytes(sig[33:65], "big")
    e = int.from_bytes(_msg_hash(msg), "big")

    R = _decompress(r + (recid >> 1) * N, recid & 1)
    Q = _mul(_inv(r, N), _add(_mul(s, R), _mul((-e) % N, (GX, GY))))
    if Q is None:
        raise ValueError("public key recovery failed")

    qx, qy = Q
    pub = (bytes([2 + (qy & 1)]) + qx.to_bytes(32, "big")) if compressed \
        else (b"\x04" + qx.to_bytes(32, "big") + qy.to_bytes(32, "big"))
    return _address(pub)


# --------------------------------------------------------------------------
# Anchor parsing
# --------------------------------------------------------------------------
def _pushes(asm: str):
    """WhatsOnChain renders pushdata as hex, but small ints in decimal."""
    out = []
    for tok in asm.split():
        if tok.startswith("OP_"):
            continue
        try:
            out.append(bytes.fromhex(tok))
        except ValueError:
            out.append(bytes([int(tok)]))
    return out


def verify(txid: str) -> bool:
    req = urllib.request.Request(API + txid, headers={"User-Agent": "origin-verify/1.0"})
    try:
        tx = json.loads(urllib.request.urlopen(req, timeout=30).read().decode())
    except Exception as exc:
        print(f"could not fetch transaction: {exc}")
        print("(public APIs rate-limit; wait a few seconds and retry)")
        return False

    nulldata = [o for o in tx.get("vout", [])
                if o["scriptPubKey"].get("type") == "nulldata"
                or "OP_RETURN" in o["scriptPubKey"].get("asm", "")]
    if not nulldata:
        print("no OP_RETURN output in this transaction — not an anchor")
        return False

    pushes = _pushes(nulldata[0]["scriptPubKey"]["asm"])
    if b"|" not in pushes:
        print("no AIP separator found — payload is unsigned")
        return False

    pipe = pushes.index(b"|")
    signed = b"".join(pushes[:pipe])
    aip = pushes[pipe + 1:]
    if len(aip) < 4:
        print("malformed AIP signature block")
        return False

    algorithm = aip[1].decode(errors="replace")
    claimed = aip[2].decode(errors="replace")
    signature = aip[3].decode(errors="replace")

    try:
        payload = json.loads(pushes[1].decode())
    except Exception:
        print("payload is not valid JSON")
        return False

    import time as _t
    created = _t.strftime("%Y-%m-%d %H:%M:%S UTC", _t.gmtime(payload.get("ts", 0)))

    print()
    print(f"protocol  : {payload.get('protocol')} v{payload.get('v')}")
    print(f"batch     : {payload.get('batch')}")
    print(f"root      : {payload.get('root')}")
    print(f"records   : {payload.get('n')}")
    print(f"created   : {created}")
    print(f"block     : {tx.get('blockheight')}  ({tx.get('confirmations')} confirmations)")
    print(f"algorithm : {algorithm}")
    print(f"claimed   : {claimed}")

    try:
        recovered = recover_address(signed, signature)
    except Exception as exc:
        print(f"recovered : verification error — {exc}")
        print("\nSIGNATURE COULD NOT BE VERIFIED")
        return False

    print(f"recovered : {recovered}")
    print()
    if recovered == claimed:
        print("SIGNATURE VALID — payload authored by the claimed address")
        print("This proves authorship and immutability. It says nothing about")
        print("whether the predictions in this batch are biologically correct.")
        return True

    print("SIGNATURE MISMATCH — recovered address does not match the claim")
    return False


if __name__ == "__main__":
    txid = sys.argv[1] if len(sys.argv) > 1 else SAMPLE
    if len(sys.argv) <= 1:
        print(f"no txid supplied — verifying sample anchor {txid[:16]}…")
    sys.exit(0 if verify(txid.strip()) else 1)
