#!/usr/bin/env python3 """Dry Run DR1 verifier. Zero dependencies (Python 3.8+). Checks one paper record row against its daily Merkle root and the root's memo on Solana. python3 verify.py [--api https://dryrun.space] [--rpc https://api.mainnet-beta.solana.com] python3 verify.py --vectors (offline: the published test vectors) """ import hashlib import json import re import sys import urllib.request NODE = b"DR1/node" MEMO_RE = re.compile(r"^dr1:root:(\d{4}-\d{2}-\d{2}):([0-9a-f]{64}):(\d+)$") VECTORS = [ "{\"candle_t\":1789999800,\"dsl_hash\":\"9f2c2a1b4bd3c0d4ffb2b4d3a7c1e0f5b6a9d8c7e6f5a4b3c2d1e0f9a8b7c6d5\",\"equity_usd\":\"100\",\"fee_usd\":\"0\",\"kind\":\"signal\",\"network_usd\":\"0\",\"pct\":\"0\",\"price\":\"0.00214\",\"reason\":\"rule\",\"recorded_at\":1790000160,\"rule\":1,\"seq\":1,\"side\":\"buy\",\"slippage_usd\":\"0\",\"spec\":\"dr1\",\"strategy_id\":\"s_vector2345\",\"t\":1790000100,\"tokens\":\"0\",\"usd\":\"25\",\"version\":1}", "{\"candle_t\":1790000100,\"dsl_hash\":\"9f2c2a1b4bd3c0d4ffb2b4d3a7c1e0f5b6a9d8c7e6f5a4b3c2d1e0f9a8b7c6d5\",\"equity_usd\":\"99.9124271488\",\"fee_usd\":\"0.06245\",\"kind\":\"fill\",\"network_usd\":\"0.02\",\"pct\":\"0\",\"price\":\"0.00213\",\"reason\":\"rule\",\"recorded_at\":1790000460,\"rule\":1,\"seq\":2,\"side\":\"buy\",\"slippage_usd\":\"0.0031228512\",\"spec\":\"dr1\",\"strategy_id\":\"s_vector2345\",\"t\":1790000100,\"tokens\":\"11695.9230932\",\"usd\":\"25\",\"version\":1}", "{\"candle_t\":1790003400,\"dsl_hash\":\"9f2c2a1b4bd3c0d4ffb2b4d3a7c1e0f5b6a9d8c7e6f5a4b3c2d1e0f9a8b7c6d5\",\"equity_usd\":\"104.356\",\"fee_usd\":\"0\",\"kind\":\"signal\",\"network_usd\":\"0\",\"pct\":\"100\",\"price\":\"0.00251\",\"reason\":\"take_profit\",\"recorded_at\":1790003760,\"rule\":0,\"seq\":3,\"side\":\"sell\",\"slippage_usd\":\"0\",\"spec\":\"dr1\",\"strategy_id\":\"s_vector2345\",\"t\":1790003700,\"tokens\":\"0\",\"usd\":\"0\",\"version\":1}" ] VECTOR_ROOT = "fd5ef3e1c89d1da3d0cb6ff1f0fedc4671d185bc33fbb578fa74b8ccf5a5a6aa" EMPTY_ROOT = "356b7d2552be077122a90f18acde60e021624bf900e05da361af9665d61ae8da" failed = False def sha(*parts): h = hashlib.sha256() for p in parts: h.update(p.encode("utf-8") if isinstance(p, str) else p) return h.digest() def canonical(v): # Keys sorted, no whitespace; leaf values are strings and integers only. return json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False) def node(a, b): return sha(NODE, a, b) if a <= b else sha(NODE, b, a) def fold(leaf, proof): h = leaf for s in proof: h = node(h, bytes.fromhex(s)) return h.hex() def root_of(hashes): level = hashes while len(level) > 1: level = [node(level[i], level[i + 1] if i + 1 < len(level) else level[i]) for i in range(0, len(level), 2)] return level[0].hex() def check(ok, text): global failed print(("✓ " if ok else "✗ ") + text) failed = failed or not ok return ok def get_json(url, body=None): req = urllib.request.Request(url, data=json.dumps(body).encode() if body else None, headers={"content-type": "application/json", "user-agent": "dryrun-verify"}) with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read()) def vectors(): hashes = [sha(j) for j in VECTORS] for i, j in enumerate(VECTORS): check(canonical(json.loads(j)) == j, "vector %d is canonical; sha256 %s" % (i, hashes[i].hex())) check(root_of(hashes) == VECTOR_ROOT, "root " + root_of(hashes)) check(sha("DR1/empty").hex() == EMPTY_ROOT, "empty root " + EMPTY_ROOT) def main(argv): if "--vectors" in argv: return vectors() opt = lambda k, d: argv[argv.index(k) + 1] if k in argv else d ids = [a for a in argv if a.isdigit()] if not ids: print("usage: python3 verify.py [--api URL] [--rpc URL] | --vectors") return api = opt("--api", "https://dryrun.space") rpc = opt("--rpc", "https://api.mainnet-beta.solana.com") p = get_json("%s/api/v1/proof/%s" % (api, ids[0])) if "error" in p: return check(False, p["error"]) print("row #%s, %s UTC, leaf %d/%d\n%s" % (p["signal_id"], p["date"], p["leaf_index"] + 1, p["leaf_count"], p["leaf_json"])) check(canonical(json.loads(p["leaf_json"])) == p["leaf_json"], "row is canonical JSON") leaf = sha(p["leaf_json"]) check(leaf.hex() == p["leaf_hash"], "sha256(row) = " + leaf.hex()) root = fold(leaf, p["proof"]) check(root == p["root"], "proof folds to root " + root) if not p.get("root_tx"): print("… root not published on chain yet (%s)" % p.get("status")) return tx = get_json(rpc, {"jsonrpc": "2.0", "id": 1, "method": "getTransaction", "params": [p["root_tx"]["signature"], {"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0, "commitment": "confirmed"}]}) ixs = (((tx.get("result") or {}).get("transaction") or {}).get("message") or {}).get("instructions") or [] memos = [i.get("parsed") for i in ixs if i.get("program") == "spl-memo"] m = next((MEMO_RE.match(str(x)) for x in memos if MEMO_RE.match(str(x))), None) check(bool(m) and m.group(1) == p["date"] and m.group(2) == root and int(m.group(3)) == p["leaf_count"], "memo on chain: %s (tx %s)" % (" | ".join(map(str, memos)) or "none", p["root_tx"]["signature"])) if __name__ == "__main__": main(sys.argv[1:]) sys.exit(1 if failed else 0)