#!/usr/bin/env node // Dry Run DR1 verifier. Zero dependencies (Node 18+). Checks one paper record row against its daily // Merkle root and the root's memo on Solana, without trusting dryrun.space for anything but the data. // // node verify.mjs [--api https://dryrun.space] [--rpc https://api.mainnet-beta.solana.com] // node verify.mjs --vectors (offline: the published test vectors) import { createHash } from "node:crypto"; const NODE = Buffer.from("DR1/node"); const MEMO_RE = /^dr1:root:(\d{4}-\d{2}-\d{2}):([0-9a-f]{64}):(\d+)$/; const sha = (...parts) => createHash("sha256").update(Buffer.concat(parts.map((p) => (typeof p === "string" ? Buffer.from(p, "utf8") : p)))).digest(); /** Canonical JSON: keys sorted, no whitespace. Leaf values are strings and integers only. */ function canonical(v) { if (v === null || typeof v !== "object") return JSON.stringify(v); if (Array.isArray(v)) return `[${v.map(canonical).join(",")}]`; return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`; } function node(a, b) { return Buffer.compare(a, b) <= 0 ? sha(NODE, a, b) : sha(NODE, b, a); } function fold(leaf, proof) { let h = leaf; for (const s of proof) h = node(h, Buffer.from(s, "hex")); return h.toString("hex"); } function rootOf(hashes) { let level = hashes; while (level.length > 1) { const next = []; for (let i = 0; i < level.length; i += 2) next.push(node(level[i], level[i + 1] ?? level[i])); level = next; } return level[0].toString("hex"); } const 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}" ]; const VECTOR_ROOT = "fd5ef3e1c89d1da3d0cb6ff1f0fedc4671d185bc33fbb578fa74b8ccf5a5a6aa"; const EMPTY_ROOT = "356b7d2552be077122a90f18acde60e021624bf900e05da361af9665d61ae8da"; function check(ok, text) { console.log(`${ok ? "✓" : "✗"} ${text}`); if (!ok) process.exitCode = 1; return ok; } async function vectors() { const hashes = VECTORS.map((j) => sha(j)); VECTORS.forEach((j, i) => check(canonical(JSON.parse(j)) === j, `vector ${i} is canonical; sha256 ${hashes[i].toString("hex")}`)); check(rootOf(hashes) === VECTOR_ROOT, `root ${rootOf(hashes)}`); check(sha("DR1/empty").toString("hex") === EMPTY_ROOT, `empty root ${EMPTY_ROOT}`); } async function main() { const args = process.argv.slice(2); if (args.includes("--vectors")) return vectors(); const opt = (k, d) => (args.includes(k) ? args[args.indexOf(k) + 1] : d); const id = args.find((a) => /^\d+$/.test(a)); if (!id) return console.log("usage: node verify.mjs [--api URL] [--rpc URL] | --vectors"); const api = opt("--api", "https://dryrun.space"); const rpc = opt("--rpc", "https://api.mainnet-beta.solana.com"); const p = await (await fetch(`${api}/api/v1/proof/${id}`)).json(); if (p.error) return check(false, p.error); console.log(`row #${p.signal_id}, ${p.date} UTC, leaf ${p.leaf_index + 1}/${p.leaf_count}\n${p.leaf_json}`); check(canonical(JSON.parse(p.leaf_json)) === p.leaf_json, "row is canonical JSON"); const leaf = sha(p.leaf_json); check(leaf.toString("hex") === p.leaf_hash, `sha256(row) = ${leaf.toString("hex")}`); const root = fold(leaf, p.proof); check(root === p.root, `proof folds to root ${root}`); if (!p.root_tx) return console.log(`… root not published on chain yet (${p.status})`); const tx = await (await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getTransaction", params: [p.root_tx.signature, { encoding: "jsonParsed", maxSupportedTransactionVersion: 0, commitment: "confirmed" }] }) })).json(); const memos = (tx.result?.transaction?.message?.instructions ?? []).filter((i) => i.program === "spl-memo").map((i) => i.parsed); const m = memos.map((x) => String(x).match(MEMO_RE)).find(Boolean); check(!!m && m[1] === p.date && m[2] === root && Number(m[3]) === p.leaf_count, `memo on chain: ${memos.join(" | ") || "none"} (tx ${p.root_tx.signature})`); const signer = tx.result?.transaction?.message?.accountKeys?.find((k) => k.signer)?.pubkey; console.log(` signed by ${signer} (the oracle wallet published on dryrun.space/method)`); } main().catch((e) => { console.error(e); process.exitCode = 1; });