diff --git a/tools/proof-vectors/01-proof-of-software.json b/tools/proof-vectors/01-proof-of-software.json new file mode 100644 index 0000000..cbf2890 --- /dev/null +++ b/tools/proof-vectors/01-proof-of-software.json @@ -0,0 +1,45 @@ +{ + "v": 1, + "kind": "aere-proof-of-software-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-software", + "tool": "aere-proof-of-software/1.1.0", + "subject": { + "name": "@aere/cloud", + "version": "1.2.1" + }, + "artifacts": [ + { + "name": "aere-cloud-1.2.1.tgz", + "sha256": "0x2c2c2cde3f332b9dfe38ee2fe1338d89308e256a514b4b2078a40548936ac000", + "bytes": 5453 + } + ], + "sbom": { + "name": "aere-cloud-1.2.1.sbom.cdx.json", + "sha256": "0x7c5c9ab7f7b0b405ca1e6e89b01b6acec0bc2bc141a07c6cb12bde9f5b154463", + "bytes": 1352 + }, + "source": { + "commit": "45028076fc3c6a874cd972aeba177a65c7c73b75", + "remote": "https://git.aere.network/aere-network/aerenew.git", + "dirty": true, + "path": "sdk", + "tree": "827ff46ed12ce62b625ced8a629a28a378afba55", + "pathDirty": false + }, + "build": { + "kind": "npm-pack", + "npm": "11.11.0" + }, + "builder": { + "platform": "win32/x64", + "node": "v24.14.1" + }, + "createdAt": "2026-09-18T08:46:06.510Z" + }, + "statementHash": "0x4c7ffa2c737166cfcbe96f637fe4b48ed921a153e7c6bca22e38459588e951c6", + "signature": null, + "notarization": null +} diff --git a/tools/proof-vectors/02-tampered-statement.json b/tools/proof-vectors/02-tampered-statement.json new file mode 100644 index 0000000..4f23744 --- /dev/null +++ b/tools/proof-vectors/02-tampered-statement.json @@ -0,0 +1,45 @@ +{ + "v": 1, + "kind": "aere-proof-of-software-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-software", + "tool": "aere-proof-of-software/1.1.0", + "subject": { + "name": "@aere/cloud", + "version": "1.2.1" + }, + "artifacts": [ + { + "name": "aere-cloud-1.2.1.tgz", + "sha256": "0x2c2c2cde3f332b9dfe38ee2fe1338d89308e256a514b4b2078a40548936ac000", + "bytes": 5453 + } + ], + "sbom": { + "name": "aere-cloud-1.2.1.sbom.cdx.json", + "sha256": "0x7c5c9ab7f7b0b405ca1e6e89b01b6acec0bc2bc141a07c6cb12bde9f5b154463", + "bytes": 1352 + }, + "source": { + "commit": "45028076fc3c6a874cd972aeba177a65c7c73b75", + "remote": "https://git.aere.network/aere-network/aerenew.git", + "dirty": true, + "path": "sdk", + "tree": "827ff46ed12ce62b625ced8a629a28a378afba55", + "pathDirty": false + }, + "build": { + "kind": "npm-pack", + "npm": "11.11.0" + }, + "builder": { + "platform": "win32/x64", + "node": "v24.14.1" + }, + "createdAt": "1999-01-01T00:00:00.000Z" + }, + "statementHash": "0x4c7ffa2c737166cfcbe96f637fe4b48ed921a153e7c6bca22e38459588e951c6", + "signature": null, + "notarization": null +} diff --git a/tools/proof-vectors/03-tampered-hash.json b/tools/proof-vectors/03-tampered-hash.json new file mode 100644 index 0000000..a4150f9 --- /dev/null +++ b/tools/proof-vectors/03-tampered-hash.json @@ -0,0 +1,45 @@ +{ + "v": 1, + "kind": "aere-proof-of-software-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-software", + "tool": "aere-proof-of-software/1.1.0", + "subject": { + "name": "@aere/cloud", + "version": "1.2.1" + }, + "artifacts": [ + { + "name": "aere-cloud-1.2.1.tgz", + "sha256": "0x2c2c2cde3f332b9dfe38ee2fe1338d89308e256a514b4b2078a40548936ac000", + "bytes": 5453 + } + ], + "sbom": { + "name": "aere-cloud-1.2.1.sbom.cdx.json", + "sha256": "0x7c5c9ab7f7b0b405ca1e6e89b01b6acec0bc2bc141a07c6cb12bde9f5b154463", + "bytes": 1352 + }, + "source": { + "commit": "45028076fc3c6a874cd972aeba177a65c7c73b75", + "remote": "https://git.aere.network/aere-network/aerenew.git", + "dirty": true, + "path": "sdk", + "tree": "827ff46ed12ce62b625ced8a629a28a378afba55", + "pathDirty": false + }, + "build": { + "kind": "npm-pack", + "npm": "11.11.0" + }, + "builder": { + "platform": "win32/x64", + "node": "v24.14.1" + }, + "createdAt": "2026-09-18T08:46:06.510Z" + }, + "statementHash": "0x4c7ffa2c737166cfcbe96f637fe4b48ed921a153e7c6bca22e38459588e951c0", + "signature": null, + "notarization": null +} diff --git a/tools/proof-vectors/04-minimal-synthetic.json b/tools/proof-vectors/04-minimal-synthetic.json new file mode 100644 index 0000000..977706c --- /dev/null +++ b/tools/proof-vectors/04-minimal-synthetic.json @@ -0,0 +1,14 @@ +{ + "v": 1, + "kind": "aere-proof-of-data-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-data", + "subject": { + "name": "example.csv" + }, + "sha256": "0x1111111111111111111111111111111111111111111111111111111111111111", + "createdAt": "2026-09-26T00:00:00.000Z" + }, + "statementHash": "0xdd9b05ae223aa6d1c59e18fd094476cea7c5b4c67ba9c037b2795c4eee0c97fa" +} diff --git a/tools/proof-vectors/05-no-statement-hash.json b/tools/proof-vectors/05-no-statement-hash.json new file mode 100644 index 0000000..97d730d --- /dev/null +++ b/tools/proof-vectors/05-no-statement-hash.json @@ -0,0 +1,7 @@ +{ + "v": 1, + "kind": "x", + "statement": { + "a": 1 + } +} diff --git a/tools/proof-vectors/06-notarized-28001.json b/tools/proof-vectors/06-notarized-28001.json new file mode 100644 index 0000000..d8f7367 --- /dev/null +++ b/tools/proof-vectors/06-notarized-28001.json @@ -0,0 +1,16 @@ +{ + "v": 1, + "kind": "aere-proof-of-data-attestation", + "statement": { + "v": 1, + "kind": "aere-proof-of-data", + "name": "aere-research/aips/aip22-conformance/VECTORS.json", + "sha256": "0xacf4c765585037c49c2b990eb02a659e9545b98d4094fb87ad5976540fc4899f", + "bytes": 2090589, + "source": "https://git.aere.network/aere-network/aere-research/raw/branch/main/aips/aip22-conformance/VECTORS.json", + "createdAt": "2026-09-27T03:15:00Z" + }, + "statementHash": "0x63d55d23e50df62dfec0e8d861eefd126f5484d77de1a80fc92f5fb1636420a4", + "signature": null, + "notarization": null +} diff --git a/tools/proof-vectors/README.md b/tools/proof-vectors/README.md new file mode 100644 index 0000000..a6d731e --- /dev/null +++ b/tools/proof-vectors/README.md @@ -0,0 +1,24 @@ +# AERE Proof Protocol conformance vectors (AIP-23) + +Run the reference verifier on each envelope and compare the verdict: + +``` +npm install +node ../verify-proof.mjs 01-proof-of-software.json +node ../verify-proof.mjs 06-notarized-28001.json --chain 28001 --rpc https://testnet-rpc.aere.network +``` + +| envelope | offline (no --rpc) | what it tests | +| --- | --- | --- | +| 01-proof-of-software.json | VALID | a real Proof of Software attestation: form and integrity | +| 02-tampered-statement.json | INVALID (integrity) | one field of the statement changed after hashing | +| 03-tampered-hash.json | INVALID (integrity) | one byte of the statementHash changed | +| 04-minimal-synthetic.json | VALID | the smallest envelope the protocol accepts | +| 05-no-statement-hash.json | INVALID (form) | an envelope without a statementHash | +| 06-notarized-28001.json | VALID | form and integrity; with `--chain 28001 --rpc ...` also finality | + +With `--chain 28001 --rpc https://testnet-rpc.aere.network`, 06 gives finality `post-quantum`: the state of the parent of the most +recent anchor, whose certificate verify-anchor.mjs checks, records its statementHash in the testnet notary as first seen at +1790478623 (2026-09-27T03:10:23Z), first in block 3,618,722. Its statement describes the AIP-22 conformance corpus file as it was +served at that time (eight of nine kinds); the file has changed since, the statement has not. With `--rpc` on chain 2800, where +nothing is notarized yet, 01 gives `not notarized`, proven absent from the certified state. diff --git a/tools/verify-proof.mjs b/tools/verify-proof.mjs new file mode 100644 index 0000000..a75b4c7 --- /dev/null +++ b/tools/verify-proof.mjs @@ -0,0 +1,239 @@ +#!/usr/bin/env node +// verify-proof.mjs - reference verifier of the AERE Proof Protocol (AIP-23). +// +// A proof is an envelope { v, kind, statement, statementHash, signature?, notarization? }. This tool checks it without trusting the +// producer and, when asked to check finality, without trusting the RPC endpoint either: +// +// 1 form the envelope has a statement (object) and a statementHash (0x + 64 hex) +// 2 integrity sha256(JSON.stringify(statement)) == statementHash +// 3 signature present ? an ml-dsa-{44,65,87} signature over the canonical statement text, checked with node:crypto +// (OpenSSL 3.5); a scheme this tool cannot check is reported UNMEASURED, never passed : absent +// 4 finality (--rpc given, or the envelope claims a notarization) the chain's most recent anchor is verified by +// verify-anchor.mjs (beside this file: every post-quantum seal of its certificate checked against the published +// manifests); the header of the anchor's parent is re-encoded and must hash to the parent hash those seals signed; +// that header's state root then anchors a Merkle proof (eth_getProof) that the AereNotary contract (its code hash +// pinned below) holds this statementHash, and the time it was first seen. The endpoint is only a courier: a header, +// a proof or a value it altered would not match the certified hash. +// +// Usage: node verify-proof.mjs [--rpc ] [--chain 2800|28001] [--notary
] [--manifests ] [--json] +// Exit: 0 every level present passed; 1 a level present failed; 2 a level present could not be measured here (or is pending). +// +// The notary stores the block TIME of the first sighting (block.timestamp), not a block number. The block that holds the first +// sighting is located through the endpoint's logs and reported as such (it is not part of what is certified). +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { keccak_256 } from '@noble/hashes/sha3'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const NOTARY = { 2800: '0x4aB392c4Aca7D9D4C16c0b60a9514c5025bd58c7', 28001: '0x70099E62735500AA2F85B60C518551a57B202d54' }; +const RPC_DEFAULT = { 2800: 'https://rpc.aere.network', 28001: 'https://testnet-rpc.aere.network' }; +const MANIFESTS_DEFAULT = { 2800: 'https://aere.network/pq/manifests', 28001: 'https://testnet-rpc.aere.network/pq/manifests' }; +// keccak256 of the AereNotary runtime code (the same bytes on 2800 and 28001): an address whose account proves another code hash +// is not the notary, whatever the endpoint says +const NOTARY_CODEHASH_FULL = '8eaa924081e9718fcee05ae64fadaa1815437ebbbcef5b87317c5879050452ce'; + +class Stop extends Error {} +process.on('uncaughtException', (e) => { if (e instanceof Stop) return; console.error(e); process.exit(2); }); +const stop = (code) => { process.exitCode = code; throw new Stop(); }; + +// ---------- minimal RLP and hashing ---------- +const hexb = (h) => Buffer.from(String(h).replace(/^0x/, '').replace(/^(.(..)*)$/, '0$1'), 'hex'); +const keccak = (b) => Buffer.from(keccak_256(b)); +const encLen = (n, off) => { if (n < 56) return Buffer.from([off + n]); const l = Buffer.from(n.toString(16).replace(/^(.(..)*)$/, '0$1'), 'hex'); return Buffer.concat([Buffer.from([off + 55 + l.length]), l]); }; +function rlpEncode(x) { + if (Array.isArray(x)) { const body = Buffer.concat(x.map(rlpEncode)); return Buffer.concat([encLen(body.length, 0xc0), body]); } + const b = Buffer.from(x); + if (b.length === 1 && b[0] < 0x80) return b; + return Buffer.concat([encLen(b.length, 0x80), b]); +} +const scalar = (h) => { const n = BigInt(h); return n === 0n ? Buffer.alloc(0) : hexb(n.toString(16)); }; +function rlpDecode(buf) { + const [v, end] = dec(buf, 0); + if (end !== buf.length) throw new Error('trailing bytes after an RLP item'); + return v; + function dec(b, o) { + const p = b[o]; + if (p === undefined) throw new Error('RLP runs past its input'); + if (p < 0x80) return [b.subarray(o, o + 1), o + 1]; + if (p < 0xb8) return [b.subarray(o + 1, o + 1 + p - 0x80), o + 1 + p - 0x80]; + if (p < 0xc0) { const l = p - 0xb7; const n = Number(BigInt('0x' + b.subarray(o + 1, o + 1 + l).toString('hex'))); return [b.subarray(o + 1 + l, o + 1 + l + n), o + 1 + l + n]; } + let n, s; + if (p < 0xf8) { n = p - 0xc0; s = o + 1; } else { const l = p - 0xf7; n = Number(BigInt('0x' + b.subarray(o + 1, o + 1 + l).toString('hex'))); s = o + 1 + l; } + const out = []; let q = s; + while (q < s + n) { const [x, r] = dec(b, q); out.push(x); q = r; } + if (q !== s + n) throw new Error('RLP list length mismatch'); + return [out, s + n]; + } +} +// The on-chain hash of a QBFT header: extraData re-encoded as [vanity, validators, vote, round 0, no commit seals], the certificate +// and the proposer seal left out (they are outside the hash). This mirrors the re-encoding in verify-anchor.mjs; if the two ever +// drifted, the check below would FAIL (the hash would not match), never pass. +function onchainHash(b) { + const e = rlpDecode(hexb(b.extraData)); + const extra = rlpEncode([e[0], e[1], e[2], Buffer.alloc(0), []]); + const f = [hexb(b.parentHash), hexb(b.sha3Uncles), hexb(b.miner), hexb(b.stateRoot), hexb(b.transactionsRoot), hexb(b.receiptsRoot), + hexb(b.logsBloom), scalar(b.difficulty), scalar(b.number), scalar(b.gasLimit), scalar(b.gasUsed), scalar(b.timestamp), extra, + hexb(b.mixHash), hexb(b.nonce)]; + if (b.baseFeePerGas != null) f.push(scalar(b.baseFeePerGas)); + if (b.withdrawalsRoot != null) f.push(hexb(b.withdrawalsRoot)); + if (b.blobGasUsed != null) { f.push(scalar(b.blobGasUsed)); f.push(scalar(b.excessBlobGas)); } + if (b.parentBeaconBlockRoot != null) f.push(hexb(b.parentBeaconBlockRoot)); + if (b.requestsHash != null) f.push(hexb(b.requestsHash)); + if (b.balHash != null) f.push(hexb(b.balHash)); + return '0x' + keccak(rlpEncode(f)).toString('hex'); +} +// A Merkle-Patricia proof (eth_getProof): the value at keccak(key) under root, or null when the proof shows it absent. Every node +// is looked up by its own hash, starting from the root, so a node the endpoint added or changed is simply never reached. +function mptGet(root, hashedKey, proof) { + const byHash = new Map(proof.map((p) => { const b = hexb(p); return [keccak(b).toString('hex'), b]; })); + const nib = (buf) => { const o = []; for (const x of buf) o.push(x >> 4, x & 15); return o; }; + let key = nib(hashedKey), ref = hexb(root); + for (let depth = 0; depth < 70; depth++) { + let node; + if (Buffer.isBuffer(ref)) { + const raw = byHash.get(ref.toString('hex')); + if (!raw) throw new Error('the proof is missing the node ' + ref.toString('hex').slice(0, 16) + '..'); + node = rlpDecode(raw); + } else node = ref; + if (!Array.isArray(node)) throw new Error('a proof node is not a list'); + if (node.length === 17) { + if (key.length === 0) return node[16].length ? node[16] : null; + const next = node[key[0]]; key = key.slice(1); + if (Array.isArray(next)) { ref = next; continue; } + if (next.length === 0) return null; + if (next.length !== 32) throw new Error('a branch child is neither a hash nor an inline node'); + ref = next; continue; + } + if (node.length === 2) { + const enc = nib(node[0]); const flag = enc[0]; + if (flag > 3) throw new Error('bad hex-prefix flag'); + const p = enc.slice(flag % 2 === 1 ? 1 : 2); + const matches = key.length >= p.length && p.every((n, i) => key[i] === n); + if (flag >= 2) return matches && key.length === p.length ? node[1] : null; + if (!matches) return null; + key = key.slice(p.length); + const next = node[1]; + if (Array.isArray(next)) { ref = next; continue; } + if (next.length !== 32) throw new Error('an extension child is not a hash'); + ref = next; continue; + } + throw new Error('a proof node with ' + node.length + ' items'); + } + throw new Error('the proof is deeper than any key'); +} +const level = (name, state, detail) => ({ level: name, state, detail }); + +// ---------- levels 1 and 2 ---------- +function local(env) { + const out = []; + const formOk = env && typeof env === 'object' && env.statement && typeof env.statement === 'object' + && typeof env.statementHash === 'string' && /^0x[0-9a-fA-F]{64}$/.test(env.statementHash); + out.push(level('form', formOk ? 'PASSED' : 'FAILED', formOk ? `kind=${env.kind || env.statement.kind || '?'}` : 'the envelope lacks {statement, statementHash 0x+64 hex}')); + if (!formOk) return { out, text: null }; + const text = JSON.stringify(env.statement); + const h = '0x' + crypto.createHash('sha256').update(Buffer.from(text, 'utf8')).digest('hex'); + const ok = h === env.statementHash.toLowerCase(); + out.push(level('integrity', ok ? 'PASSED' : 'FAILED', ok ? `sha256(statement) == statementHash (${h.slice(0, 18)}..)` : `sha256(statement) is ${h.slice(0, 18)}.., not the statementHash ${env.statementHash.slice(0, 18)}..`)); + return { out, text: ok ? text : null }; +} + +// ---------- level 3 ---------- +function signature(env, text) { + if (!env.signature) return level('signature', 'ABSENT', 'the envelope carries no signature'); + const s = env.signature; const scheme = String(s.scheme || s.alg || '').toLowerCase(); + if (['ml-dsa-44', 'ml-dsa-65', 'ml-dsa-87'].includes(scheme) && s.publicKey && s.signature) { + try { + const pub = String(s.publicKey).includes('BEGIN') ? crypto.createPublicKey(s.publicKey) + : crypto.createPublicKey({ key: Buffer.from(s.publicKey, 'base64'), format: 'der', type: 'spki' }); + const sig = /^[0-9a-fA-F]+$/.test(s.signature) ? Buffer.from(s.signature, 'hex') : Buffer.from(s.signature, 'base64'); + const ok = crypto.verify(null, Buffer.from(text, 'utf8'), pub, sig); + return level('signature', ok ? 'PASSED' : 'FAILED', `${scheme}: ${ok ? 'verifies' : 'does not verify'} over the canonical statement text`); + } catch (e) { return level('signature', 'UNMEASURED', `${scheme} present, but node:crypto cannot check it here (${String(e.message).split('\n')[0].slice(0, 60)}); it needs OpenSSL 3.5`); } + } + return level('signature', 'UNMEASURED', `a "${scheme || '?'}" signature is not checkable by this reference verifier (the hybrid form is checked by @aere/pq-sign)`); +} + +// ---------- level 4 ---------- +async function finality(env, o) { + if (!o.rpcGiven && !env.notarization) return level('finality', 'ABSENT', 'no notarization claimed and no --rpc given'); + const rpc = async (method, params) => { + const r = await fetch(o.rpc, { method: 'POST', headers: { 'content-type': 'application/json', 'user-agent': 'aere-verify-proof/1' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); + const j = await r.json(); if (j.error) throw new Error(method + ': ' + (j.error.message || JSON.stringify(j.error))); return j.result; + }; + // (a) the chain's most recent anchor, verified by verify-anchor.mjs, which reports the parent hash its seals signed + const report = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'aere-proof-')), 'anchor.json'); + const va = await new Promise((ok) => { + const c = spawn(process.execPath, [path.join(HERE, 'verify-anchor.mjs')], { cwd: HERE, + env: { ...process.env, RPC: o.rpc, CHAIN_ID: String(o.chain), MANIFESTS: o.manifests, ANCHORS: '1', PROPOSER_HEADERS: '2', ANCHOR_REPORT: report } }); + let out = ''; c.stdout.on('data', (d) => { out += d; }); c.stderr.on('data', (d) => { out += d; }); + c.on('error', (e) => ok({ code: null, out: String(e.message) })); c.on('close', (code) => ok({ code, out })); + }); + let rep = null; try { rep = JSON.parse(fs.readFileSync(report, 'utf8')); } catch { rep = null; } + const tail = va.out.trim().split('\n').slice(-2).join(' | ').slice(0, 220); + if (va.code === 1) return level('finality', 'FAILED', 'the chain\'s most recent anchor did not verify: ' + tail); + if (va.code !== 0 || !rep || !rep.parentHash || rep.chainId !== o.chain) return level('finality', 'UNMEASURED', 'the anchor could not be verified here (verify-anchor.mjs exit ' + va.code + '): ' + tail); + // (b) the parent header, bound to the certified hash by re-encoding + const X = rep.parentNumber; + let hdr; try { hdr = await rpc('eth_getBlockByNumber', ['0x' + X.toString(16), false]); } catch (e) { return level('finality', 'UNMEASURED', 'block ' + X + ' could not be read: ' + e.message); } + if (!hdr) return level('finality', 'UNMEASURED', 'the endpoint returned no header for block ' + X); + if (onchainHash(hdr) !== rep.parentHash) return level('finality', 'FAILED', `the header the endpoint served for block ${X} does not hash to ${rep.parentHash.slice(0, 18)}.., the hash anchor ${rep.anchor}'s certificate signed`); + // (c) the notary's storage at that header's state root: mapping(bytes32 => uint64) firstSeen at slot 0 + const slot = '0x' + keccak(Buffer.concat([hexb(env.statementHash), Buffer.alloc(32)])).toString('hex'); + let pr; try { pr = await rpc('eth_getProof', [o.notary, [slot], '0x' + X.toString(16)]); } catch (e) { return level('finality', 'UNMEASURED', 'eth_getProof at block ' + X + ' failed (the endpoint may not keep that state): ' + e.message); } + let acct, val; + try { + const a = mptGet(hdr.stateRoot, keccak(hexb(o.notary)), pr.accountProof || []); + if (!a) return level('finality', 'FAILED', `the state of block ${X} has no account at ${o.notary}`); + acct = rlpDecode(a); + if (keccak(Buffer.alloc(0)).equals(acct[3]) || acct[3].toString('hex') !== NOTARY_CODEHASH_FULL) return level('finality', 'FAILED', `the account at ${o.notary} in the state of block ${X} is not the AereNotary contract (code hash 0x${acct[3].toString('hex').slice(0, 16)}..)`); + const s = (pr.storageProof || [])[0]; + if (!s) throw new Error('no storage proof returned'); + const v = mptGet('0x' + acct[2].toString('hex'), keccak(hexb(slot)), s.proof || []); + val = v ? BigInt('0x' + (rlpDecode(v).toString('hex') || '0')) : 0n; + } catch (e) { return level('finality', 'FAILED', 'the Merkle proof for block ' + X + ' does not hold: ' + e.message); } + const certified = `the state of block ${X}, the parent of anchor ${rep.anchor} whose post-quantum certificate verified here`; + if (val === 0n) { + let later = 0n; try { later = BigInt(await rpc('eth_call', [{ to: o.notary, data: '0x' + keccak(Buffer.from('proofOf(bytes32)')).subarray(0, 4).toString('hex') + env.statementHash.slice(2) }, 'latest'])); } catch { later = 0n; } + if (later > 0n) return level('finality', 'PENDING', `notarized at ${new Date(Number(later) * 1000).toISOString()} (endpoint), after ${certified}; the next anchor will cover it`); + return level('finality', env.notarization ? 'FAILED' : 'ABSENT', `not notarized: ${certified} holds no entry for this statementHash`); + } + const t = Number(val); + let where = ''; + try { + const topic = '0x' + keccak(Buffer.from('Notarized(bytes32,address,uint64,bool)')).toString('hex'); + let lo = 0, hi = X; + while (lo < hi) { const mid = Math.floor((lo + hi) / 2); const b = await rpc('eth_getBlockByNumber', ['0x' + mid.toString(16), false]); if (Number(BigInt(b.timestamp)) >= t) hi = mid; else lo = mid + 1; } + const logs = await rpc('eth_getLogs', [{ fromBlock: '0x' + Math.max(0, lo - 8).toString(16), toBlock: '0x' + Math.min(X, lo + 8).toString(16), address: o.notary, topics: [topic, env.statementHash] }]); + const first = (logs || []).find((l) => /1$/.test(l.data)) || (logs || [])[0]; + if (first) where = `; first seen in block ${Number(BigInt(first.blockNumber))}, transaction ${first.transactionHash.slice(0, 18)}.. (located through the endpoint's logs, not part of what is certified)`; + } catch { where = ''; } + return level('finality', 'PASSED', `post-quantum: ${certified} records this statementHash in AereNotary ${o.notary} as first seen at ${new Date(t * 1000).toISOString()} (${t})${where}`); +} + +// ---------- main ---------- +const args = process.argv.slice(2); +const opt = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : null; }; +const file = args.find((a, i) => !a.startsWith('--') && !(i > 0 && args[i - 1].startsWith('--') && args[i - 1] !== '--json')); +if (!file) { console.error('usage: node verify-proof.mjs [--rpc ] [--chain 2800|28001] [--notary
] [--manifests ] [--json]'); stop(2); } +const chain = Number(opt('--chain') || 2800); +if (!NOTARY[chain]) { console.error('unknown chain ' + chain + ' (2800 or 28001)'); stop(2); } +let env; try { env = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { console.error('the envelope cannot be read: ' + e.message); stop(2); } +const o = { chain, rpcGiven: !!opt('--rpc'), rpc: opt('--rpc') || RPC_DEFAULT[chain], notary: (opt('--notary') || NOTARY[chain]).toLowerCase(), + manifests: opt('--manifests') || MANIFESTS_DEFAULT[chain] }; +const l = local(env); +const levels = [...l.out]; +if (l.text) { levels.push(signature(env, l.text)); levels.push(await finality(env, o)); } +const failed = levels.some((x) => x.state === 'FAILED'); +const unmeasured = levels.some((x) => x.state === 'UNMEASURED' || x.state === 'PENDING'); +const verdict = failed ? 'INVALID' : unmeasured ? 'PARTIAL' : 'VALID'; +if (args.includes('--json')) console.log(JSON.stringify({ verdict, statementHash: env.statementHash, levels }, null, 1)); +else { + console.log(`AERE Proof Protocol - ${path.basename(file)}`); + for (const x of levels) console.log(` [${x.state.padEnd(10)}] ${x.level}: ${x.detail}`); + console.log(`VERDICT: ${verdict}`); +} +process.exitCode = failed ? 1 : unmeasured ? 2 : 0;