#!/usr/bin/env python3
"""Independent checker for the solidarity levy ledger. Standard library only.

Written separately from the page's JavaScript so that two implementations, in
two languages, have to agree. Trees follow RFC 6962 (Certificate Transparency):
leaf = SHA-256(0x00 || line), node = SHA-256(0x01 || left || right).

  python3 verify.py batches.json            # rebuild every batch root
  python3 verify.py receipt.json            # check one owner's receipt
  python3 verify.py --selftest              # RFC 6962 test vectors

Both files come from the ledger page's download buttons.
"""
import hashlib, json, sys

def leaf_hash(b): return hashlib.sha256(b'\x00' + b).digest()
def node_hash(l, r): return hashlib.sha256(b'\x01' + l + r).digest()

def mth(leaves):
    n = len(leaves)
    if n == 0: return hashlib.sha256(b'').digest()
    if n == 1: return leaf_hash(leaves[0])
    k = 1
    while k * 2 < n: k *= 2
    return node_hash(mth(leaves[:k]), mth(leaves[k:]))

def verify_inclusion(leaf, index, size, proof, root):
    if index >= size: return False
    fn, sn, r = index, size - 1, leaf_hash(leaf)
    for p in proof:
        if sn == 0: return False
        if fn & 1 or fn == sn:
            r = node_hash(p, r)
            if not fn & 1:
                while not fn & 1 and fn != 0:
                    fn >>= 1; sn >>= 1
        else:
            r = node_hash(r, p)
        fn >>= 1; sn >>= 1
    return sn == 0 and r == root

VECTORS = (['', '00', '10', '2021', '3031', '40414243', '5051525354555657', '606162636465666768696a6b6c6d6e6f'],
           ['6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d', 'fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125',
            'aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77', 'd37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7',
            '4e3bbb1f7b478dcfe71fb631631519a3bca12c9aefca1612bfce4c13a86264d4', '76e67dadbcdf1e10e1b74ddc608abd2f98dfb16fbce75277b5232a127f2087ef',
            'ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c', '5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328'])

def main(argv):
    if argv[1:] == ['--selftest']:
        leaves = [bytes.fromhex(h) for h in VECTORS[0]]
        ok = all(mth(leaves[:n]).hex() == VECTORS[1][n - 1] for n in range(1, 9))
        print('RFC 6962 test vectors:', 'all 8 match' if ok else 'MISMATCH'); return 0 if ok else 1
    doc = json.load(open(argv[1]))
    if 'batches' in doc:
        bad = 0
        for b in doc['batches']:
            lines = [l.encode() for l in b['lines']]
            total = sum(int(l.split('|')[3]) for l in b['lines'])
            ok = mth(lines).hex() == b['merkle_root'] and total == b['total_micro_usd']
            bad += not ok
            print(f"{b['batch']}: {len(lines)} lines, total {total / 1e6:.6f} USD, root {'matches' if ok else 'DOES NOT MATCH'}")
        return 1 if bad else 0
    if 'lines' in doc and 'owner' in doc:
        bad = 0
        for l in doc['lines']:
            ok = verify_inclusion(l['line'].encode(), l['index'], l['batch_size'], [bytes.fromhex(p) for p in l['proof']], bytes.fromhex(l['root']))
            bad += not ok
            print(f"{l['line']}: {'included' if ok else 'NOT INCLUDED'} in {l['batch']}")
        given = sum(int(l['line'].split('|')[3]) for l in doc['lines'])
        print(f"{doc['owner']} gave {given / 1e6:.6f} USD across {len(doc['lines'])} lines; {len(doc['lines']) - bad} proofs check out")
        return 1 if bad else 0
    print('unrecognised file'); return 2

if __name__ == '__main__':
    sys.exit(main(sys.argv))
