#!/usr/bin/env python3
"""verify_receipts.py — own-implementation check of theattempt.org receipts.json.

For each row in the file:
  - RECEIPT rows: sha256(preimage) must equal the invoice's payment_hash
    (BOLT-11 'p' tagged field), and the invoice signature must recover to a
    pubkey (optionally matching the row's `signer` field).
  - SAME-CUSTODIAN rows: no preimage; invoice must decode and be signed by
    the custodian node given in the file's `custodian_node` field.
  - All rows: signature must be low-S (s <= n/2); a high-S malleated twin
    is reported as a bad row even though it verifies mathematically.

Stdlib only (pure-python bech32 + secp256k1 recovery).
Usage: python3 verify_receipts.py receipts.json
"""
import json, sys, hashlib

CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8


def bech32_decode(s):
    s = s.lower()
    pos = s.rfind("1")
    hrp, data = s[:pos], s[pos + 1:]
    return hrp, [CHARSET.index(c) for c in data[:-6]]   # strip 6-char checksum


def conv(data, frombits=5, tobits=8, pad=False):
    acc = bit = 0; out = []
    for v in data:
        acc = (acc << frombits) | v; bit += frombits
        while bit >= tobits:
            bit -= tobits; out.append((acc >> bit) & ((1 << tobits) - 1))
    if pad and bit: out.append((acc << (tobits - bit)) & ((1 << tobits) - 1))
    return bytes(out)


def tagged_fields(inv):
    hrp, data = bech32_decode(inv)
    ts = data[-104:-7]          # timestamp u5 x7? actually first 7 data words
    # BOLT11 data: [7 x u5 timestamp][tagged fields][104 x u5 signature]
    body = data[7:-104]
    tags = {}
    i = 0
    while i < len(body):
        t = body[i]; ln = (body[i+1] << 5) | body[i+2]
        tags[t] = conv(body[i+3:i+3+ln])
        i += 3 + ln
    sig65 = conv(data[-104:])         # 65 bytes: r||s||recid
    sig_r, recid = sig65[:64], sig65[64]
    return hrp, data, tags, sig_r, recid


def inv_msg(inv):
    hrp, data = bech32_decode(inv)
    raw = data[:-104]
    # BOLT-11: sha256 over hrp + data-part u5 stream bit-packed to bytes (0-pad)
    msg = hrp.encode() + conv(raw, 5, 8, pad=True)
    return msg


def modinv(a, m): return pow(a, -1, m)

def pt_add(Pt, Qt):
    if Pt is None: return Qt
    if Qt is None: return Pt
    x1, y1 = Pt; x2, y2 = Qt
    if x1 == x2 and (y1 + y2) % P == 0: return None
    if Pt == Qt:
        l = (3 * x1 * x1) * modinv(2 * y1 % P, P) % P
    else:
        l = (y2 - y1) * modinv((x2 - x1) % P, P) % P
    x3 = (l * l - x1 - x2) % P
    return x3, (l * (x1 - x3) - y1) % P

def pt_mul(k, Pt=(Gx, Gy)):
    R = None
    while k:
        if k & 1: R = pt_add(R, Pt)
        Pt = pt_add(Pt, Pt); k >>= 1
    return R

def ecdsa_recover(msg32, r, s, recid):
    if not (1 <= r < N and 1 <= s < N): return None
    x = r + (recid // 2) * N
    if x >= P: return None
    # y from parity
    yy = (pow(x, 3, P) + 7) % P
    y = pow(yy, (P + 1) // 4, P)
    if (y & 1) != (recid & 1): y = P - y
    R = (x, y)
    e = int.from_bytes(msg32, "big")
    rinv = modinv(r, N)
    # Q = r^-1 (s*R - e*G)
    sR = pt_mul(s, R); eG = pt_mul(e % N)
    neg_eG = (eG[0], (-eG[1]) % P)
    Q = pt_mul(rinv, pt_add(sR, neg_eG))
    return Q

def ser33(Pt):
    return bytes([2 + (Pt[1] & 1)]) + Pt[0].to_bytes(32, "big")


def ecdsa_verify(msg32, r, s, Q):
    if not (1 <= r < N and 1 <= s < N): return False
    e = int.from_bytes(msg32, "big")
    w = modinv(s, N)
    P1 = pt_mul((e * w) % N)
    P2 = pt_mul((r * w) % N, Q)
    R = pt_add(P1, P2)
    return R is not None and R[0] % N == r


def check_invoice(inv, want_signer=None):
    """returns (ok, detail, signer_hex_or_None, payhash_hex_or_None, high_s)"""
    try:
        hrp, data, tags, sig, recid = tagged_fields(inv)
    except Exception as e:
        return False, "undecodable: %s" % e, None, None, False
    ph = tags.get(CHARSET.index('p'))             # tag 'p' = payment_hash
    ph_hex = ph.hex() if ph else None
    msg = inv_msg(inv)
    m = hashlib.sha256(msg).digest()
    r, s = int.from_bytes(sig[:32], "big"), int.from_bytes(sig[32:], "big")
    high_s = s > N // 2
    ntag = tags.get(CHARSET.index('n'))
    if ntag:
        x = int.from_bytes(ntag[1:], "big")
        yy = (pow(x, 3, P) + 7) % P
        y = pow(yy, (P + 1) // 4, P)
        if (y & 1) != (ntag[0] & 1): y = P - y
        ok = ecdsa_verify(m, r, s, (x, y))
        signer = ntag.hex()
        if not ok:
            return False, "sig fails vs n-tag" + (" (high-S)" if high_s else ""), signer, ph_hex, high_s
        return True, "sig ok (n-tag)", signer, ph_hex, high_s
    Q = ecdsa_recover(m, r, s, recid)
    signer = ser33(Q).hex() if Q else None
    if signer is None:
        return False, "signature does not recover", None, ph_hex, high_s
    if want_signer and signer != want_signer:
        return False, "signer %s != %s" % (signer[:20], want_signer[:20]), signer, ph_hex, high_s
    return True, "sig ok", signer, ph_hex, high_s


def main():
    d = json.load(open(sys.argv[1]))
    cust = d.get("custodian_node")
    rows = d["rows"]
    counts = {}
    bad = []
    vcounts = {}
    highs = []
    for i, r in enumerate(rows):
        v = r.get("verdict"); counts[v] = counts.get(v, 0) + 1
        ok, det, signer, ph, high_s = check_invoice(r.get("invoice") or "")
        if high_s:
            highs.append(i)
            bad.append((i, "high-S signature (malleated form)")); continue
        if v == "RECEIPT":
            pre = r.get("preimage") or ""
            if not (len(pre) == 64 and all(c in "0123456789abcdef" for c in pre.lower())):
                bad.append((i, "bad preimage format")); continue
            h = hashlib.sha256(bytes.fromhex(pre)).hexdigest()
            if h != ph:
                bad.append((i, "sha256(preimage) != invoice payment_hash")); continue
            if not ok:
                bad.append((i, "sig: " + det)); continue
            if signer != r.get("signer"):
                bad.append((i, "signer field mismatch %s vs %s" % (signer[:16], (r.get("signer") or "")[:16]))); continue
        elif v == "SAME-CUSTODIAN":
            if r.get("preimage"): bad.append((i, "has preimage but same-custodian")); continue
            if not ok: bad.append((i, "invoice " + det)); continue
            elif cust and signer != cust: bad.append((i, "not custodian-signed")); continue
        else:
            bad.append((i, "unexpected verdict " + str(v))); continue
        vcounts[v] = vcounts.get(v, 0) + 1
    print("file generated:", d.get("generated"))
    print("rows:", len(rows), "declared counts:", counts, "verified counts:", vcounts)
    print("custodian_node:", cust)
    print("high-S rows:", len(highs))
    print("bad rows:", len(bad))
    for i, why in bad[:15]:
        print("  row", i, why)


if __name__ == "__main__":
    main()
