From ad913360d4e7d884a9c3035365565a8ea57948a1 Mon Sep 17 00:00:00 2001 From: Zk-nd3r Date: Mon, 3 Aug 2026 17:20:10 -0700 Subject: [PATCH] fix: verify current ZAP1 proof bundles --- .gitattributes | 3 + .github/workflows/ci.yml | 27 +- .gitignore | 3 + CHANGELOG.md | 18 + PROVENANCE.md | 33 ++ README.md | 54 ++- dist/blake2b.js | 333 +++++++++++++++++ dist/index.d.ts | 123 ++++--- dist/index.js | 445 +++++++++++++++++----- frontier-compute-zap1-0.1.0.tgz | Bin 43959 -> 0 bytes package-lock.json | 66 ++++ package.json | 33 +- scripts/build.mjs | 9 + scripts/test-packed.mjs | 155 ++++++++ src/blake2b.js | 333 +++++++++++++++++ src/index.d.ts | 123 ++++--- src/index.js | 445 +++++++++++++++++----- test/browser-test.mjs | 120 ++++++ test/test.js | 629 ++++++++++++++++++++++++-------- wasm/zap1_verify_wasm.js | 526 -------------------------- wasm/zap1_verify_wasm_bg.wasm | Bin 83444 -> 0 bytes 21 files changed, 2476 insertions(+), 1002 deletions(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 PROVENANCE.md create mode 100644 dist/blake2b.js delete mode 100644 frontier-compute-zap1-0.1.0.tgz create mode 100644 package-lock.json create mode 100644 scripts/build.mjs create mode 100644 scripts/test-packed.mjs create mode 100644 src/blake2b.js create mode 100644 test/browser-test.mjs delete mode 100644 wasm/zap1_verify_wasm.js delete mode 100644 wasm/zap1_verify_wasm_bg.wasm diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b9ded9d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.tgz binary +*.wasm binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e072c91..e238307 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,16 +7,29 @@ on: workflow_dispatch: jobs: - test: + node: runs-on: ubuntu-latest + strategy: + matrix: + node: [18, 20, 22] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: "20" + node-version: ${{ matrix.node }} + cache: npm + - run: npm ci + - run: npm test + - run: npm pack --dry-run - - name: Install dependencies - run: npm install - - - name: Run tests - run: node test/test.js + browser: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx --no-install playwright install --with-deps chromium + - run: npm run test:packed diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4efdfe7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.tgz +npm-debug.log* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c07b3dc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +## 0.2.0 + +- Verify current `ZAP1_COUNT_BOUND_V2` bundles with mandatory leaf-count + binding. +- Gate historical `ZAP1_LEGACY_DUPLICATE_ODD` verification behind an explicit + caller option and the frozen anchor-height cutoff. +- Replace the unreproducible generated WASM artifact with auditable, + zero-runtime-dependency JavaScript. +- Reject malformed digests, ambiguous proof positions, oversized proofs, + unsafe leaf counts, impossible tree paths, missing schemes, internal-node + substitution, and silent legacy downgrade. +- Test Node 18, 20, and 22 plus a real Chromium ESM import. + +Version `0.1.3` remains the historical March-May artifact. Version `0.2.0` +is post-window compatibility maintenance for current count-bound proof bundles. +It does not change the application period, amount, or deliverables. diff --git a/PROVENANCE.md b/PROVENANCE.md new file mode 100644 index 0000000..58a97b3 --- /dev/null +++ b/PROVENANCE.md @@ -0,0 +1,33 @@ +# Release provenance + +## 0.2.0 + +The release source is the commit bearing the annotated repository tag `v0.2.0`. +The npm artifact must be built from a clean checkout of that tag. + +The JavaScript BLAKE2b implementation in `src/blake2b.js` is derived from: + +- repository: `Frontier-Compute/zap1` +- commit: `63448237dc13e9199303f37c995294b2a56132b1` +- path: `verify-widget/blake2b.js` +- source SHA-256: + `D31E528C51BEF4D98D9A8B5EEA64550018DCE47EAD9AF8D3AD5D301A791A2BCA` + +The release file is not byte-identical to that source. Its SHA-256 is +`5546B1C22EB13F60E9F45C1B358CB4FDCD8CCC77C3F2E039BBC5E3C057D39FC5`. +The delta adds strict type, length, digest, proof, leaf-count, and historical +height validation (59 inserted lines and 7 removed lines); the BLAKE2b +compression and personalization core is unchanged. + +`test/test.js` includes independent Python `hashlib.blake2b` boundary +vectors at 0, 1, 3, 127, 128, 129, 255, 256, 257, and 1024 bytes, with and +without the protocol personalization. The release gate also: + +- rebuilds `dist/` from source during `prepack`; +- asserts the exact tarball allowlist; +- installs that tarball into a fresh project with scripts disabled; +- tests current and historical proof semantics in Node and Chromium; and +- runs the source suite on Node 18, 20, and 22 in CI. + +Version `0.1.3` remains the historical March-May artifact. Version `0.2.0` +is post-window compatibility and verifier-hardening maintenance. diff --git a/README.md b/README.md index 11eb3cf..416e070 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,17 @@ [**Dashboard**](https://frontiercompute.cash/dashboard.html) | [npm](https://www.npmjs.com/package/@frontiercompute/zap1) | [GitHub](https://github.com/Frontier-Compute/zap1-js) -ZAP1 Merkle proof verification for JavaScript and TypeScript. WASM-powered BLAKE2b-256 with ZAP1 domain-separated personalizations. - -Client-side verification of on-chain commitments. All cryptography runs in WebAssembly compiled from the `zap1-verify` Rust crate. +Zero-runtime-dependency ZAP1 Merkle proof verification for JavaScript and +TypeScript. The cryptographic primitive is auditable JavaScript with no native +or WASM loader. +The verifier supports current `ZAP1_COUNT_BOUND_V2` proof bundles and an +explicitly gated historical legacy profile. + +This package verifies Merkle-bundle consistency against a supplied root. It +does not prove that an encrypted Zcash memo contains that root, and it does not +prove the truth of the event represented by a leaf. A transaction ID is a +recorded reference until transaction existence and memo binding are checked by +separate evidence. ## Install @@ -24,12 +32,15 @@ npm install @frontiercompute/zap1 ```js import { verifyProof, parseBundle } from "@frontiercompute/zap1"; -const res = await fetch("https://pay.frontiercompute.io/verify/075b00df.../proof.json"); +const leafHash = "your_64_character_leaf_hash"; +const res = await fetch( + `https://api.frontiercompute.cash/verify/${leafHash}/proof.json` +); const raw = await res.json(); const bundle = parseBundle(raw); const valid = await verifyProof(bundle); -console.log(valid ? "VERIFIED" : "FAILED"); +console.log(valid ? "BUNDLE CONSISTENT" : "BUNDLE INVALID"); ``` ### Compute a leaf hash @@ -56,12 +67,32 @@ const parent = await nodeHash(leftHex, rightHex); | Function | Description | |----------|-------------| -| `init()` | Preload the WASM module (called automatically on first use) | +| `init()` | Compatibility no-op; retained for 0.1.x callers | | `computeLeafHash(type, payload)` | Compute leaf hash for PROGRAM_ENTRY or OWNERSHIP_ATTEST | -| `verifyProof(bundle)` | Verify a Merkle inclusion proof | +| `verifyProof(bundle, options)` | Verify V2; historical legacy requires `allowHistoricalLegacy: true` | | `nodeHash(left, right)` | Compute a Merkle node hash | +| `commitRoot(rawRoot, leafCount)` | Bind a raw root to its positive leaf count | | `parseBundle(json)` | Parse and normalize an API proof bundle | -| `EVENT_TYPES` | Array of all 18 ZAP1 event types | +| `EVENT_TYPES` | The 18 defined protocol event types | +| `LEAF_HASH_TYPES` | The 2 event types with client-side typed hash formulas | + +Proof-path verification is event-type agnostic. Typed leaf reconstruction is +currently available only for `PROGRAM_ENTRY` and `OWNERSHIP_ATTEST`. + +### Root schemes + +- `ZAP1_COUNT_BOUND_V2`: default and required for current bundles. The + committed root binds `leaf_count`. +- `ZAP1_LEGACY_DUPLICATE_ODD`: accepted only when the caller sets + `{ allowHistoricalLegacy: true }`, the bundle labels itself legacy, and the recorded + anchor height is at or below the frozen historical cutoff. A positive + `leaf_count` and an exact duplicate-odd proof shape are mandatory. + +Missing counts, unknown schemes, malformed hashes, invalid proof positions, +and out-of-window legacy bundles fail closed. +When present, envelope metadata must use `protocol: "ZAP1"` and +`version: "2"` for either root scheme; the legacy label describes the root +construction, not an envelope-version downgrade. ## Personalizations (protocol constants) @@ -69,17 +100,20 @@ const parent = await nodeHash(leftHex, rightHex); |---------|-----------------| | Leaf hash | `NordicShield_\x00\x00\x00` | | Node hash | `NordicShield_MRK` | +| Root commitment | `NordicShield_RTK` | ## Protocol -See [ONCHAIN_PROTOCOL.md](https://github.com/Frontier-Compute/zap1/blob/main/ONCHAIN_PROTOCOL.md) for the full ZAP1 specification. +See [ONCHAIN_PROTOCOL.md](https://github.com/Frontier-Compute/zap1/blob/main/ONCHAIN_PROTOCOL.md) +for the deployed protocol description. The document distinguishes bundle +verification, transaction existence, encrypted-memo binding, and event truth. ## Related Packages | Package | What it does | |---------|-------------| | [@frontiercompute/zcash-ika](https://www.npmjs.com/package/@frontiercompute/zcash-ika) | Zcash + Bitcoin signing via Ika 2PC-MPC | -| [@frontiercompute/zcash-mcp](https://www.npmjs.com/package/@frontiercompute/zcash-mcp) | MCP server for Zcash (22 tools) | +| [@frontiercompute/zcash-mcp](https://www.npmjs.com/package/@frontiercompute/zcash-mcp) | MCP server for Zcash | | [@frontiercompute/openclaw-zap1](https://www.npmjs.com/package/@frontiercompute/openclaw-zap1) | OpenClaw skill for ZAP1 attestation | | [@frontiercompute/silo-zap1](https://www.npmjs.com/package/@frontiercompute/silo-zap1) | Silo agent attestation via ZAP1 | diff --git a/dist/blake2b.js b/dist/blake2b.js new file mode 100644 index 0000000..adba8c7 --- /dev/null +++ b/dist/blake2b.js @@ -0,0 +1,333 @@ +/** + * blake2b.js - Pure JS BLAKE2b-256 with personalization support + * Compatible with Python hashlib.blake2b and Rust blake2b_simd + * No WASM, no native modules, no dependencies. + */ + +const MASK64 = (1n << 64n) - 1n; + +const IV = [ + 0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, + 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n, + 0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, + 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n, +]; + +const SIGMA = [ + [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], + [14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3], + [11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4], + [7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8], + [9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13], + [2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9], + [12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11], + [13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10], + [6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5], + [10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0], +]; + +function rotr64(x, n) { + const bn = BigInt(n); + return ((x >> bn) | (x << (64n - bn))) & MASK64; +} + +function readLE64(buf, off) { + let v = 0n; + for (let i = 0; i < 8; i++) v |= BigInt(buf[off + i]) << BigInt(8 * i); + return v; +} + +function writeLE64(buf, off, val) { + for (let i = 0; i < 8; i++) buf[off + i] = Number((val >> BigInt(8 * i)) & 0xffn); +} + +function compress(h, block, t, last) { + const v = new Array(16); + for (let i = 0; i < 8; i++) { v[i] = h[i]; v[i + 8] = IV[i]; } + v[12] ^= t & MASK64; + v[13] ^= (t >> 64n) & MASK64; + if (last) v[14] ^= MASK64; + + const m = new Array(16); + for (let i = 0; i < 16; i++) m[i] = readLE64(block, i * 8); + + function G(a, b, c, d, x, y) { + v[a] = (v[a] + v[b] + x) & MASK64; + v[d] = rotr64(v[d] ^ v[a], 32); + v[c] = (v[c] + v[d]) & MASK64; + v[b] = rotr64(v[b] ^ v[c], 24); + v[a] = (v[a] + v[b] + y) & MASK64; + v[d] = rotr64(v[d] ^ v[a], 16); + v[c] = (v[c] + v[d]) & MASK64; + v[b] = rotr64(v[b] ^ v[c], 63); + } + + for (let r = 0; r < 12; r++) { + const s = SIGMA[r % 10]; + G(0,4, 8,12, m[s[ 0]], m[s[ 1]]); + G(1,5, 9,13, m[s[ 2]], m[s[ 3]]); + G(2,6,10,14, m[s[ 4]], m[s[ 5]]); + G(3,7,11,15, m[s[ 6]], m[s[ 7]]); + G(0,5,10,15, m[s[ 8]], m[s[ 9]]); + G(1,6,11,12, m[s[10]], m[s[11]]); + G(2,7, 8,13, m[s[12]], m[s[13]]); + G(3,4, 9,14, m[s[14]], m[s[15]]); + } + + for (let i = 0; i < 8; i++) h[i] = h[i] ^ v[i] ^ v[i + 8]; +} + +/** + * BLAKE2b-256 hash with optional 16-byte personalization. + * @param {Uint8Array} input + * @param {Uint8Array} [personalization] - 16 bytes + * @returns {Uint8Array} 32-byte digest + */ +export function blake2b256(input, personalization) { + const p = new Uint8Array(64); + p[0] = 32; // digest length + p[2] = 1; // fanout + p[3] = 1; // max depth + if (personalization) { + for (let i = 0; i < 16; i++) p[48 + i] = personalization[i] || 0; + } + + const h = new Array(8); + for (let i = 0; i < 8; i++) h[i] = IV[i] ^ readLE64(p, i * 8); + + let t = 0n; + let off = 0; + + if (input.length === 0) { + compress(h, new Uint8Array(128), 0n, true); + } else { + while (off + 128 < input.length) { + t += 128n; + compress(h, input.subarray(off, off + 128), t, false); + off += 128; + } + const last = new Uint8Array(128); + last.set(input.subarray(off)); + t += BigInt(input.length - off); + compress(h, last, t, true); + } + + const out = new Uint8Array(32); + for (let i = 0; i < 4; i++) writeLE64(out, i * 8, h[i]); + return out; +} + +// Hex utilities + +export function hexToBytes(hex) { + if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) { + throw new TypeError("hex must be a non-empty even-length string"); + } + if (!/^[0-9a-fA-F]+$/.test(hex)) { + throw new TypeError("hex contains non-hexadecimal characters"); + } + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return out; +} + +export function bytesToHex(bytes) { + let hex = ""; + for (let i = 0; i < bytes.length; i++) { + hex += bytes[i].toString(16).padStart(2, "0"); + } + return hex; +} + +// ZAP1 BLAKE2b personalizations + +// "NordicShield_\x00\x00\x00" (13 chars + 3 null = 16 bytes) +const LEAF_PERSONAL = new Uint8Array([ + 0x4e,0x6f,0x72,0x64,0x69,0x63,0x53,0x68, + 0x69,0x65,0x6c,0x64,0x5f,0x00,0x00,0x00, +]); + +// "NordicShield_MRK" (16 bytes) +const NODE_PERSONAL = new Uint8Array([ + 0x4e,0x6f,0x72,0x64,0x69,0x63,0x53,0x68, + 0x69,0x65,0x6c,0x64,0x5f,0x4d,0x52,0x4b, +]); + +// "NordicShield_RTK" (16 bytes) +const ROOT_PERSONAL = new Uint8Array([ + 0x4e,0x6f,0x72,0x64,0x69,0x63,0x53,0x68, + 0x69,0x65,0x6c,0x64,0x5f,0x52,0x54,0x4b, +]); + +const ENCODER = new TextEncoder(); +export const COUNT_BOUND_SCHEME = "ZAP1_COUNT_BOUND_V2"; +export const LEGACY_SCHEME = "ZAP1_LEGACY_DUPLICATE_ODD"; +export const LEGACY_ROOT_MAX_ANCHOR_HEIGHT = 3317133; + +// Event-type prefix bytes (known types) +const EVENT_PREFIX = { + PROGRAM_ENTRY: 0x01, + OWNERSHIP_ATTEST: 0x02, +}; + +/** + * Recompute a leaf hash from event data. + * Returns null if the event type's hash formula is unknown. + */ +export function computeLeafHash(eventType, walletHash, serialNumber) { + const prefix = EVENT_PREFIX[eventType]; + if (prefix === undefined) return null; + if (typeof walletHash !== "string") { + throw new TypeError("walletHash must be a string"); + } + + if (eventType === "PROGRAM_ENTRY") { + const walletBytes = ENCODER.encode(walletHash); + if (walletBytes.length > 65535) { + throw new RangeError("walletHash exceeds 65535 UTF-8 bytes"); + } + const input = new Uint8Array(1 + walletBytes.length); + input[0] = prefix; + input.set(walletBytes, 1); + return blake2b256(input, LEAF_PERSONAL); + } + + if (eventType === "OWNERSHIP_ATTEST") { + if (typeof serialNumber !== "string") { + throw new TypeError("serialNumber must be a string"); + } + const walletBytes = ENCODER.encode(walletHash); + const serialBytes = ENCODER.encode(serialNumber); + if (walletBytes.length > 65535 || serialBytes.length > 65535) { + throw new RangeError("ownership fields exceed 65535 UTF-8 bytes"); + } + const input = new Uint8Array(1 + 2 + walletBytes.length + 2 + serialBytes.length); + let off = 0; + input[off++] = prefix; + input[off++] = (walletBytes.length >> 8) & 0xff; + input[off++] = walletBytes.length & 0xff; + input.set(walletBytes, off); off += walletBytes.length; + input[off++] = (serialBytes.length >> 8) & 0xff; + input[off++] = serialBytes.length & 0xff; + input.set(serialBytes, off); + return blake2b256(input, LEAF_PERSONAL); + } + + return null; +} + +/** + * Hash two 32-byte children into a Merkle node. + */ +export function nodeHash(left, right) { + if (!(left instanceof Uint8Array) || left.length !== 32) { + throw new TypeError("left child must be 32 bytes"); + } + if (!(right instanceof Uint8Array) || right.length !== 32) { + throw new TypeError("right child must be 32 bytes"); + } + const input = new Uint8Array(64); + input.set(left, 0); + input.set(right, 32); + return blake2b256(input, NODE_PERSONAL); +} + +export function commitRoot(leafCount, rawRoot) { + if (!(rawRoot instanceof Uint8Array) || rawRoot.length !== 32) { + throw new TypeError("raw root must be 32 bytes"); + } + let count; + if (typeof leafCount === "bigint") { + count = leafCount; + } else if (typeof leafCount === "number" && Number.isSafeInteger(leafCount)) { + count = BigInt(leafCount); + } else { + throw new TypeError("leaf_count must be a safe integer number or bigint"); + } + if (count <= 0n) throw new RangeError("leaf_count must be positive"); + if (count > 0xffffffffffffffffn) throw new RangeError("leaf_count exceeds u64"); + const input = new Uint8Array(41); + input[0] = 1; + let tmp = count; + for (let i = 8; i >= 1; i--) { + input[i] = Number(tmp & 0xffn); + tmp >>= 8n; + } + input.set(rawRoot, 9); + return blake2b256(input, ROOT_PERSONAL); +} + +/** + * Walk a Merkle proof from leaf to root. + * @param {string} leafHashHex + * @param {Array<{hash: string, position: string}>} proof - sibling steps + * @param {number} [leafCount] + * @returns {{ computedRoot: string, legacyRoot: string, rootScheme: string, steps: Array<{left: string, right: string, result: string}> }} + */ +export function walkProof(leafHashHex, proof, leafCount) { + if (typeof leafHashHex !== "string" || !/^[0-9a-fA-F]{64}$/.test(leafHashHex)) { + throw new TypeError("leaf hash must be exactly 32 bytes of hex"); + } + if (!Array.isArray(proof)) throw new TypeError("proof must be an array"); + if (proof.length > 64) throw new RangeError("proof exceeds 64 steps"); + let current = hexToBytes(leafHashHex); + const steps = []; + + for (let index = 0; index < proof.length; index++) { + if (!(index in proof)) throw new TypeError("proof must not contain holes"); + const step = proof[index]; + if (!step || typeof step !== "object" || Array.isArray(step)) { + throw new TypeError("proof step must be an object"); + } + if (typeof step.hash !== "string" || !/^[0-9a-fA-F]{64}$/.test(step.hash)) { + throw new TypeError("proof sibling must be exactly 32 bytes of hex"); + } + if (step.position !== "left" && step.position !== "right") { + throw new TypeError("proof position must be left or right"); + } + const sibling = hexToBytes(step.hash); + let left, right; + if (step.position === "right") { + left = current; right = sibling; + } else { + left = sibling; right = current; + } + current = nodeHash(left, right); + steps.push({ + left: bytesToHex(left), + right: bytesToHex(right), + result: bytesToHex(current), + }); + } + + const legacyRoot = bytesToHex(current); + if (leafCount === undefined || leafCount === null) { + return { + computedRoot: legacyRoot, + legacyRoot, + rootScheme: LEGACY_SCHEME, + steps, + }; + } + + return { + computedRoot: bytesToHex(commitRoot(leafCount, current)), + legacyRoot, + rootScheme: COUNT_BOUND_SCHEME, + steps, + }; +} + +export function isHistoricalLegacyBundle(bundle) { + const scheme = bundle?.root?.scheme; + const height = bundle?.anchor?.height; + return ( + scheme === LEGACY_SCHEME && + typeof height === "number" && + Number.isSafeInteger(height) && + height > 0 && + height <= LEGACY_ROOT_MAX_ANCHOR_HEIGHT + ); +} diff --git a/dist/index.d.ts b/dist/index.d.ts index 89e1b29..4a51945 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,91 +1,114 @@ -/** - * @frontier-compute/zap1 - ZAP1 Merkle proof verification (WASM-powered) - */ - -/** Initialize the WASM module. Called automatically on first use. */ -export function init(): Promise; +export type Zap1EventType = + | "PROGRAM_ENTRY" + | "OWNERSHIP_ATTEST" + | "CONTRACT_ANCHOR" + | "DEPLOYMENT" + | "HOSTING_PAYMENT" + | "SHIELD_RENEWAL" + | "TRANSFER" + | "EXIT" + | "MERKLE_ROOT" + | "STAKING_DEPOSIT" + | "STAKING_WITHDRAW" + | "STAKING_REWARD" + | "GOVERNANCE_PROPOSAL" + | "GOVERNANCE_VOTE" + | "GOVERNANCE_RESULT" + | "AGENT_REGISTER" + | "AGENT_POLICY" + | "AGENT_ACTION"; -/** All 9 deployed ZAP1 event types. */ -export const EVENT_TYPES: readonly string[]; +export const EVENT_TYPES: readonly Zap1EventType[]; +export const LEAF_HASH_TYPES: readonly [ + "PROGRAM_ENTRY", + "OWNERSHIP_ATTEST", +]; +export const COUNT_BOUND_SCHEME: "ZAP1_COUNT_BOUND_V2"; +export const LEGACY_SCHEME: "ZAP1_LEGACY_DUPLICATE_ODD"; +export const LEGACY_ROOT_MAX_ANCHOR_HEIGHT: 3317133; -/** Event-specific payload for computeLeafHash. */ export interface LeafPayload { walletHash?: string; serialNumber?: string; - facilityId?: string; - contractSha256?: string; - oldWalletHash?: string; - newWalletHash?: string; - timestamp?: number; - month?: number; - year?: number; - rootHash?: string; + [extra: string]: unknown; } -/** One step in a Merkle inclusion proof. */ export interface ProofStep { hash: string; position: "left" | "right"; } -/** Anchor transaction reference. */ -export interface Anchor { - txid: string; - height: number; +/** Untrusted service metadata; not proof of memo binding. */ +export interface AnchorReference { + txid?: string | null; + height?: number | null; + [extra: string]: unknown; } -/** Leaf metadata from the API. */ export interface LeafInfo { hash: string; - event_type: string; + event_type?: string; wallet_hash?: string; serial_number?: string; created_at?: string; + [extra: string]: unknown; } -/** Normalized proof bundle. */ export interface ProofBundle { leaf_hash: string; proof: ProofStep[]; root: string; - anchor: Anchor | null; + leaf_count: number | bigint | null; + root_scheme: string | null; + anchor: AnchorReference | null; leaf: LeafInfo | null; - protocol: string; - version: string; + protocol: string | null; + version: string | null; } -/** Raw proof bundle as returned by the API. */ export interface RawBundle { leaf_hash?: string; - leaf?: LeafInfo & { hash: string }; + leaf?: LeafInfo; proof: ProofStep[]; - root: string | { hash: string }; - anchor?: Anchor; + root: + | string + | { + hash: string; + leaf_count?: number | bigint | null; + scheme?: string | null; + [extra: string]: unknown; + }; + root_hash?: string; + leaf_count?: number | bigint | null; + root_scheme?: string | null; + anchor?: AnchorReference | null; protocol?: string; - version?: string; + version?: string | number; + [extra: string]: unknown; } +export interface VerifyOptions { + allowHistoricalLegacy?: boolean; +} + +/** Compatibility no-op retained for 0.1.x callers. */ +export function init(): Promise; + /** - * Compute the leaf hash for an ZAP1 event. - * Returns null for event types not yet supported client-side. + * Compute a typed leaf hash. Defined-but-unsupported types return null; + * unknown types and malformed supported payloads reject. */ export function computeLeafHash( - eventType: string, - payload: LeafPayload + eventType: Zap1EventType | string, + payload: LeafPayload, ): Promise; -/** - * Verify a Merkle inclusion proof. - * Bundle must have leaf_hash, proof, and root fields. - */ -export function verifyProof(bundle: ProofBundle): Promise; - -/** - * Compute a Merkle node hash: BLAKE2b-256("NordicShield_MRK", left || right). - */ export function nodeHash(leftHex: string, rightHex: string): Promise; +export function commitRoot(rawRootHex: string, leafCount: number | bigint): string; +export function parseBundle(input: string | RawBundle | ProofBundle): ProofBundle; -/** - * Parse a proof bundle from JSON or an object. Normalizes the structure. - */ -export function parseBundle(input: string | RawBundle): ProofBundle; +/** Verify consistency with a supplied root, not on-chain memo binding. */ +export function verifyProof( + bundle: string | RawBundle | ProofBundle, + options?: VerifyOptions, +): Promise; diff --git a/dist/index.js b/dist/index.js index b5703c1..7a8a8b4 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,39 +1,25 @@ /** - * @frontier-compute/zap1 - ZAP1 Merkle proof verification (WASM-powered) + * @frontiercompute/zap1 - strict ZAP1 Merkle-bundle verification. * - * Wraps the zap1-verify Rust crate compiled to WebAssembly. - * BLAKE2b-256 with NordicShield personalizations, Merkle proof walking, - * and leaf hash computation for all 9 deployed ZAP1 event types. + * This module verifies consistency against a supplied root. Anchor metadata is + * not treated as proof of transaction existence or encrypted-memo binding. */ -let wasmModule = null; -let initPromise = null; +import { + bytesToHex, + commitRoot as commitRootBytes, + computeLeafHash as computeLeafHashBytes, + COUNT_BOUND_SCHEME, + hexToBytes, + LEGACY_ROOT_MAX_ANCHOR_HEIGHT, + LEGACY_SCHEME, + nodeHash as nodeHashBytes, + walkProof, +} from "./blake2b.js"; -/** - * Initialize the WASM module. Called automatically on first use. - * Can be called explicitly to preload. - * @returns {Promise} - */ -export async function init() { - if (wasmModule) return; - if (initPromise) return initPromise; - initPromise = (async () => { - const wasm = await import("../wasm/zap1_verify_wasm.js"); - const wasmUrl = new URL("../wasm/zap1_verify_wasm_bg.wasm", import.meta.url); - await wasm.default(wasmUrl); - wasmModule = wasm; - })(); - return initPromise; -} - -async function ensureInit() { - if (!wasmModule) await init(); -} +export { COUNT_BOUND_SCHEME, LEGACY_ROOT_MAX_ANCHOR_HEIGHT, LEGACY_SCHEME }; -// Event types - -/** All 9 deployed ZAP1 event types. */ -export const EVENT_TYPES = [ +export const EVENT_TYPES = Object.freeze([ "PROGRAM_ENTRY", "OWNERSHIP_ATTEST", "CONTRACT_ANCHOR", @@ -43,97 +29,366 @@ export const EVENT_TYPES = [ "TRANSFER", "EXIT", "MERKLE_ROOT", -]; + "STAKING_DEPOSIT", + "STAKING_WITHDRAW", + "STAKING_REWARD", + "GOVERNANCE_PROPOSAL", + "GOVERNANCE_VOTE", + "GOVERNANCE_RESULT", + "AGENT_REGISTER", + "AGENT_POLICY", + "AGENT_ACTION", +]); + +export const LEAF_HASH_TYPES = Object.freeze([ + "PROGRAM_ENTRY", + "OWNERSHIP_ATTEST", +]); + +/** Compatibility no-op retained for callers of 0.1.x. */ +export async function init() {} + +function requireHex32(value, label) { + if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) { + throw new TypeError(`${label} must be exactly 32 bytes of hex`); + } + return value.toLowerCase(); +} + +function requirePlainObject(value, label) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new TypeError(`${label} must be an object`); + } + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) { + throw new TypeError(`${label} must be a plain object`); + } + return value; +} + +function hasOwn(value, key) { + return Object.prototype.hasOwnProperty.call(value, key); +} + +function normalizeLeafCount(value, label = "leaf_count") { + let count; + if (typeof value === "bigint") { + count = value; + } else if (typeof value === "number" && Number.isSafeInteger(value)) { + count = BigInt(value); + } else { + throw new TypeError(`${label} must be a safe integer number or bigint`); + } + if (count <= 0n) throw new RangeError(`${label} must be positive`); + if (count > 0xffffffffffffffffn) { + throw new RangeError(`${label} exceeds u64`); + } + return count; +} -// Leaf hash computation +function normalizeOptionalCount(value, label) { + return value === null ? null : normalizeLeafCount(value, label); +} + +function normalizeOptionalScheme(value, label) { + if (value === null) return null; + if (typeof value !== "string") { + throw new TypeError(`${label} must be a string or null`); + } + return value; +} /** - * Compute the leaf hash for an ZAP1 event. - * - * @param {string} eventType - one of EVENT_TYPES - * @param {object} payload - event-specific fields - * @returns {Promise} 64-char hex leaf hash - * - * Supported payloads: - * PROGRAM_ENTRY: { walletHash: string } - * OWNERSHIP_ATTEST: { walletHash: string, serialNumber: string } - * (other types return null - use verifyProof for path-only verification) + * Validate that a sibling path can exist in the deployed carry-up tree shape. + * Odd final nodes advance unchanged and therefore consume no proof step. */ -export async function computeLeafHash(eventType, payload) { - await ensureInit(); - switch (eventType) { - case "PROGRAM_ENTRY": - return wasmModule.computeProgramEntry(payload.walletHash); - case "OWNERSHIP_ATTEST": - return wasmModule.computeOwnershipAttest( - payload.walletHash, - payload.serialNumber - ); - default: - return null; +function hasPossibleCountBoundPath(leafCount, proof) { + let width = normalizeLeafCount(leafCount); + const lowerWidths = []; + while (width > 1n) { + lowerWidths.push(width); + width = (width + 1n) / 2n; } -} -// Proof verification + let parentIndex = 0n; + let proofIndex = proof.length - 1; + for (let layer = lowerWidths.length - 1; layer >= 0; layer--) { + const lowerWidth = lowerWidths[layer]; + const lastParentIndex = (lowerWidth - 1n) / 2n; + if (lowerWidth % 2n === 1n && parentIndex === lastParentIndex) { + parentIndex = lowerWidth - 1n; + continue; + } + + if (proofIndex < 0) return false; + const position = proof[proofIndex--].position; + const childIndex = + position === "right" ? parentIndex * 2n : parentIndex * 2n + 1n; + if (childIndex >= lowerWidth) return false; + parentIndex = childIndex; + } + + return proofIndex === -1; +} /** - * Verify a Merkle inclusion proof. - * - * @param {object} bundle - proof bundle from the API - * @param {string} bundle.leaf_hash - 64-char hex leaf hash - * @param {Array<{hash: string, position: "left"|"right"}>} bundle.proof - * @param {string} bundle.root - 64-char hex expected root - * @returns {Promise} + * Validate a legacy duplicate-odd path and return the proof layers where the + * sibling must be an exact duplicate of the current node. */ -export async function verifyProof(bundle) { - await ensureInit(); - return wasmModule.verifyProof({ - leaf_hash: bundle.leaf_hash, - proof: bundle.proof, - root: bundle.root, - }); +function legacyDuplicateLayers(leafCount, proof) { + let width = normalizeLeafCount(leafCount); + const lowerWidths = []; + while (width > 1n) { + lowerWidths.push(width); + width = (width + 1n) / 2n; + } + if (proof.length !== lowerWidths.length) return null; + + let parentIndex = 0n; + const duplicateLayers = []; + for (let layer = lowerWidths.length - 1; layer >= 0; layer--) { + const lowerWidth = lowerWidths[layer]; + const step = proof[layer]; + const lastParentIndex = (lowerWidth - 1n) / 2n; + if (lowerWidth % 2n === 1n && parentIndex === lastParentIndex) { + if (step.position !== "right") return null; + parentIndex = lowerWidth - 1n; + duplicateLayers.push(layer); + continue; + } + + const childIndex = + step.position === "right" + ? parentIndex * 2n + : parentIndex * 2n + 1n; + if (childIndex >= lowerWidth) return null; + parentIndex = childIndex; + } + return duplicateLayers; } /** - * Compute a Merkle node hash: BLAKE2b-256("NordicShield_MRK", left || right). - * - * @param {string} leftHex - 64-char hex - * @param {string} rightHex - 64-char hex - * @returns {Promise} 64-char hex result + * Compute a typed leaf hash where a client-side formula is defined. + * Returns null for a defined event type without a local formula. + * Throws for an unknown event type or malformed supported payload. */ +export async function computeLeafHash(eventType, payload) { + if (typeof eventType !== "string" || !EVENT_TYPES.includes(eventType)) { + throw new RangeError("unknown ZAP1 event type"); + } + if (!LEAF_HASH_TYPES.includes(eventType)) return null; + requirePlainObject(payload, "payload"); + const result = computeLeafHashBytes( + eventType, + payload.walletHash, + payload.serialNumber, + ); + return bytesToHex(result); +} + +/** Compute a domain-separated Merkle node hash. */ export async function nodeHash(leftHex, rightHex) { - await ensureInit(); - return wasmModule.nodeHash(leftHex, rightHex); + const left = hexToBytes(requireHex32(leftHex, "left hash")); + const right = hexToBytes(requireHex32(rightHex, "right hash")); + return bytesToHex(nodeHashBytes(left, right)); } -// Bundle parsing +/** Bind a raw tree root to a positive u64 leaf count. */ +export function commitRoot(rawRootHex, leafCount) { + const rawRoot = hexToBytes(requireHex32(rawRootHex, "raw root")); + return bytesToHex(commitRootBytes(leafCount, rawRoot)); +} /** - * Parse a proof bundle from JSON (API response or downloaded file). - * Normalizes the structure for use with verifyProof(). - * - * @param {string|object} input - JSON string or parsed object - * @returns {object} Normalized bundle with leaf_hash, proof, root, anchor, leaf + * Strictly normalize a flat or nested API proof bundle. + * Malformed fields throw; no scheme or count is inferred. */ export function parseBundle(input) { const data = typeof input === "string" ? JSON.parse(input) : input; + requirePlainObject(data, "bundle"); + + const leafRecord = + data.leaf === undefined || data.leaf === null + ? null + : requirePlainObject(data.leaf, "leaf"); + const nestedLeafHash = + leafRecord && hasOwn(leafRecord, "hash") + ? requireHex32(leafRecord.hash, "leaf.hash") + : null; + const flatLeafHash = hasOwn(data, "leaf_hash") + ? requireHex32(data.leaf_hash, "leaf_hash") + : null; + if ( + nestedLeafHash !== null && + flatLeafHash !== null && + nestedLeafHash !== flatLeafHash + ) { + throw new TypeError("conflicting leaf hash representations"); + } + const leafHash = nestedLeafHash ?? flatLeafHash; + if (leafHash === null) throw new TypeError("leaf hash is required"); + + if (!Array.isArray(data.proof)) throw new TypeError("proof must be an array"); + if (data.proof.length > 64) throw new RangeError("proof exceeds 64 steps"); + const proof = []; + for (let index = 0; index < data.proof.length; index++) { + if (!(index in data.proof)) throw new TypeError("proof must not contain holes"); + const step = requirePlainObject(data.proof[index], "proof step"); + if (step.position !== "left" && step.position !== "right") { + throw new TypeError("proof position must be left or right"); + } + proof.push({ + hash: requireHex32(step.hash, "proof sibling"), + position: step.position, + }); + } + + let rootHash; + let leafCount = null; + let rootScheme = null; + if (typeof data.root === "string") { + rootHash = requireHex32(data.root, "root"); + if (hasOwn(data, "leaf_count")) { + normalizeOptionalCount(data.leaf_count, "leaf_count"); + leafCount = data.leaf_count; + } + if (hasOwn(data, "root_scheme")) { + rootScheme = normalizeOptionalScheme(data.root_scheme, "root_scheme"); + } + } else { + const rootRecord = requirePlainObject(data.root, "root"); + rootHash = requireHex32(rootRecord.hash, "root.hash"); + + const nestedCountPresent = hasOwn(rootRecord, "leaf_count"); + const flatCountPresent = hasOwn(data, "leaf_count"); + const nestedCount = nestedCountPresent + ? normalizeOptionalCount(rootRecord.leaf_count, "root.leaf_count") + : null; + const flatCount = flatCountPresent + ? normalizeOptionalCount(data.leaf_count, "leaf_count") + : null; + if ( + nestedCountPresent && + flatCountPresent && + nestedCount !== flatCount + ) { + throw new TypeError("conflicting leaf_count representations"); + } + leafCount = nestedCountPresent + ? rootRecord.leaf_count + : flatCountPresent + ? data.leaf_count + : null; + + const nestedSchemePresent = hasOwn(rootRecord, "scheme"); + const flatSchemePresent = hasOwn(data, "root_scheme"); + const nestedScheme = nestedSchemePresent + ? normalizeOptionalScheme(rootRecord.scheme, "root.scheme") + : null; + const flatScheme = flatSchemePresent + ? normalizeOptionalScheme(data.root_scheme, "root_scheme") + : null; + if ( + nestedSchemePresent && + flatSchemePresent && + nestedScheme !== flatScheme + ) { + throw new TypeError("conflicting root scheme representations"); + } + rootScheme = nestedSchemePresent ? nestedScheme : flatScheme; + } + if (hasOwn(data, "root_hash")) { + const flatRootHash = requireHex32(data.root_hash, "root_hash"); + if (flatRootHash !== rootHash) { + throw new TypeError("conflicting root hash representations"); + } + } - // Normalize: API returns nested objects - const leafHash = data.leaf?.hash || data.leaf_hash; - const proof = data.proof || []; - const rootHash = data.root?.hash || data.root; - const anchor = data.anchor || null; + let anchor = null; + if (data.anchor !== undefined && data.anchor !== null) { + anchor = { ...requirePlainObject(data.anchor, "anchor") }; + } + + let protocol = null; + if (hasOwn(data, "protocol")) { + if (typeof data.protocol !== "string") { + throw new TypeError("protocol must be a string"); + } + protocol = data.protocol; + } + + let version = null; + if (hasOwn(data, "version")) { + if ( + typeof data.version !== "string" && + !(typeof data.version === "number" && Number.isSafeInteger(data.version)) + ) { + throw new TypeError("version must be a string or safe integer number"); + } + version = String(data.version); + } return { leaf_hash: leafHash, - proof: proof.map((s) => ({ - hash: s.hash, - position: s.position, - })), + proof, root: rootHash, + leaf_count: leafCount, + root_scheme: rootScheme, anchor, - leaf: data.leaf || null, - protocol: data.protocol || "ZAP1", - version: data.version || "1", + leaf: leafRecord ? { ...leafRecord, hash: leafHash } : null, + protocol, + version, }; } + +/** + * Verify Merkle-bundle consistency. + * + * Current bundles require the exact COUNT_BOUND_V2 scheme and a positive leaf + * count. Historical legacy verification is opt-in and height-gated. + */ +export async function verifyProof(input, options = {}) { + requirePlainObject(options, "options"); + const bundle = parseBundle(input); + + if (bundle.protocol !== null && bundle.protocol !== "ZAP1") return false; + + if (bundle.root_scheme === COUNT_BOUND_SCHEME) { + if (bundle.version !== null && bundle.version !== "2") return false; + if (bundle.leaf_count === null || bundle.leaf_count === undefined) return false; + if (!hasPossibleCountBoundPath(bundle.leaf_count, bundle.proof)) return false; + const walked = walkProof( + bundle.leaf_hash, + bundle.proof, + bundle.leaf_count, + ); + return walked.computedRoot === bundle.root; + } + + if (bundle.root_scheme === LEGACY_SCHEME) { + if (bundle.version !== null && bundle.version !== "2") return false; + if (options.allowHistoricalLegacy !== true) return false; + if (bundle.leaf_count === null || bundle.leaf_count === undefined) return false; + const height = bundle.anchor?.height; + if ( + typeof height !== "number" || + !Number.isSafeInteger(height) || + height <= 0 || + height > LEGACY_ROOT_MAX_ANCHOR_HEIGHT + ) { + return false; + } + const duplicateLayers = legacyDuplicateLayers( + bundle.leaf_count, + bundle.proof, + ); + if (duplicateLayers === null) return false; + const walked = walkProof(bundle.leaf_hash, bundle.proof); + for (const layer of duplicateLayers) { + if (walked.steps[layer].left !== walked.steps[layer].right) return false; + } + return walked.legacyRoot === bundle.root; + } + + return false; +} diff --git a/frontier-compute-zap1-0.1.0.tgz b/frontier-compute-zap1-0.1.0.tgz deleted file mode 100644 index 65fb4b7483ec5ef12f5e8565e2b78f4a7027fafe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43959 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ξ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 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c75cfa3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,66 @@ +{ + "name": "@frontiercompute/zap1", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@frontiercompute/zap1", + "version": "0.2.0", + "license": "MIT", + "devDependencies": { + "playwright": "1.61.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json index a28f768..a19d2af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@frontiercompute/zap1", - "version": "0.1.3", - "description": "ZAP1 Merkle proof verification for JavaScript/TypeScript - WASM-powered BLAKE2b-256", + "version": "0.2.0", + "description": "Zero-runtime-dependency ZAP1 Merkle proof verification with COUNT_BOUND_V2 and gated legacy support", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -14,11 +14,18 @@ }, "files": [ "dist/", - "wasm/" + "CHANGELOG.md", + "LICENSE", + "PROVENANCE.md", + "README.md" ], "scripts": { - "build": "mkdir -p dist && cp src/index.js dist/index.js && cp src/index.d.ts dist/index.d.ts", - "test": "node test/test.js" + "build": "node scripts/build.mjs", + "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"", + "prepack": "npm run clean && npm run build && npm test", + "test": "node test/test.js", + "test:browser": "node test/browser-test.mjs", + "test:packed": "node scripts/test-packed.mjs" }, "keywords": [ "zcash", @@ -26,12 +33,22 @@ "merkle", "proof", "zap1", - "wasm" + "javascript" ], - "author": "Zk-nd3r ", + "author": "Zk-nd3r ", "license": "MIT", + "sideEffects": false, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "playwright": "1.61.1" + }, "repository": { "type": "git", - "url": "https://github.com/Frontier-Compute/zap1-js" + "url": "https://github.com/Frontier-Compute/zap1-js.git" } } diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..644fd03 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,9 @@ +import { copyFile, mkdir, rm } from "node:fs/promises"; + +await rm("dist", { recursive: true, force: true }); +await mkdir("dist", { recursive: true }); +await Promise.all([ + copyFile("src/index.js", "dist/index.js"), + copyFile("src/index.d.ts", "dist/index.d.ts"), + copyFile("src/blake2b.js", "dist/blake2b.js"), +]); diff --git a/scripts/test-packed.mjs b/scripts/test-packed.mjs new file mode 100644 index 0000000..b90c8e2 --- /dev/null +++ b/scripts/test-packed.mjs @@ -0,0 +1,155 @@ +import assert from "node:assert/strict"; +import { execFileSync, spawnSync } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +const npmCli = process.env.npm_execpath; +if (!npmCli) { + throw new Error("npm_execpath is required; run this gate through npm"); +} +const projectRoot = process.cwd(); +const packOutput = execFileSync(process.execPath, [npmCli, "pack", "--json"], { + cwd: projectRoot, + encoding: "utf8", + stdio: ["ignore", "pipe", "inherit"], +}); +const packMarkers = [ + ...packOutput.matchAll(/\[\s*\{\s*"id"\s*:/g), +]; +if (packMarkers.length === 0) { + throw new Error("npm pack did not emit a JSON result"); +} +const packs = JSON.parse(packOutput.slice(packMarkers.at(-1).index)); +assert.equal(packs.length, 1); +const pack = packs[0]; +assert.equal(pack.name, "@frontiercompute/zap1"); +assert.equal(pack.version, "0.2.0"); +assert.deepEqual( + pack.files.map(({ path }) => path).sort(), + [ + "CHANGELOG.md", + "LICENSE", + "PROVENANCE.md", + "README.md", + "dist/blake2b.js", + "dist/index.d.ts", + "dist/index.js", + "package.json", + ], +); + +const tarball = resolve(projectRoot, pack.filename); +const trialRoot = await mkdtemp(join(tmpdir(), "zap1-packed-")); +try { + await writeFile( + join(trialRoot, "package.json"), + '{"private":true,"type":"module"}\n', + "utf8", + ); + execFileSync( + process.execPath, + [ + npmCli, + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + tarball, + ], + { cwd: trialRoot, stdio: "inherit" }, + ); + + const installedDist = join( + trialRoot, + "node_modules", + "@frontiercompute", + "zap1", + "dist", + ); + const zap1 = await import( + `${pathToFileURL(join(installedDist, "index.js")).href}?packed=1` + ); + const leaf1 = + "075b00df286038a7b3f6bb70054df61343e3481fba579591354a00214e9e019b"; + const leaf2 = + "de62554ad3867a59895befa7216686c923fc86245231e8fb6bd709a20e1fd133"; + const root = + "94421ae28effbe52f651b33eb62c3b428d2ae62be578e05d471cba9794225bbd"; + const bundle = { + leaf_hash: leaf2, + proof: [{ hash: leaf1, position: "left" }], + root: { + hash: root, + leaf_count: 2, + scheme: zap1.COUNT_BOUND_SCHEME, + }, + protocol: "ZAP1", + version: "2", + }; + assert.equal(await zap1.verifyProof(bundle), true); + + const wrongCount = structuredClone(bundle); + wrongCount.root.leaf_count = 3; + assert.equal(await zap1.verifyProof(wrongCount), false); + + const unknownScheme = structuredClone(bundle); + unknownScheme.root.scheme = "ZAP1_UNKNOWN"; + assert.equal(await zap1.verifyProof(unknownScheme), false); + + const malformed = structuredClone(bundle); + malformed.proof[0].position = "sideways"; + await assert.rejects(() => zap1.verifyProof(malformed), TypeError); + assert.equal( + await zap1.verifyProof( + { + leaf_hash: root, + proof: [], + root: { + hash: root, + leaf_count: 2, + scheme: zap1.LEGACY_SCHEME, + }, + anchor: { height: 3286631 }, + protocol: "ZAP1", + version: "2", + }, + { allowHistoricalLegacy: true }, + ), + false, + ); + assert.equal( + await zap1.verifyProof( + { + leaf_hash: leaf2, + proof: [{ hash: leaf1, position: "left" }], + root: { + hash: + "024e36515ea30efc15a0a7962dd8f677455938079430b9eab174f46a4328a07a", + leaf_count: 2, + scheme: zap1.LEGACY_SCHEME, + }, + anchor: { height: 3286631 }, + protocol: "ZAP1", + version: "2", + }, + { allowHistoricalLegacy: true }, + ), + true, + ); + assert.equal(zap1.EVENT_TYPES.length, 18); + assert.equal(zap1.LEAF_HASH_TYPES.length, 2); + + const browser = spawnSync( + process.execPath, + [resolve(projectRoot, "test", "browser-test.mjs"), installedDist], + { cwd: projectRoot, stdio: "inherit" }, + ); + if (browser.status !== 0) { + throw new Error(`packed browser matrix exited ${browser.status}`); + } + console.log(`packed clean-install matrix passed: ${pack.integrity}`); +} finally { + await rm(trialRoot, { recursive: true, force: true }); +} diff --git a/src/blake2b.js b/src/blake2b.js new file mode 100644 index 0000000..adba8c7 --- /dev/null +++ b/src/blake2b.js @@ -0,0 +1,333 @@ +/** + * blake2b.js - Pure JS BLAKE2b-256 with personalization support + * Compatible with Python hashlib.blake2b and Rust blake2b_simd + * No WASM, no native modules, no dependencies. + */ + +const MASK64 = (1n << 64n) - 1n; + +const IV = [ + 0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, + 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n, + 0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, + 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n, +]; + +const SIGMA = [ + [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15], + [14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3], + [11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4], + [7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8], + [9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13], + [2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9], + [12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11], + [13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10], + [6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5], + [10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0], +]; + +function rotr64(x, n) { + const bn = BigInt(n); + return ((x >> bn) | (x << (64n - bn))) & MASK64; +} + +function readLE64(buf, off) { + let v = 0n; + for (let i = 0; i < 8; i++) v |= BigInt(buf[off + i]) << BigInt(8 * i); + return v; +} + +function writeLE64(buf, off, val) { + for (let i = 0; i < 8; i++) buf[off + i] = Number((val >> BigInt(8 * i)) & 0xffn); +} + +function compress(h, block, t, last) { + const v = new Array(16); + for (let i = 0; i < 8; i++) { v[i] = h[i]; v[i + 8] = IV[i]; } + v[12] ^= t & MASK64; + v[13] ^= (t >> 64n) & MASK64; + if (last) v[14] ^= MASK64; + + const m = new Array(16); + for (let i = 0; i < 16; i++) m[i] = readLE64(block, i * 8); + + function G(a, b, c, d, x, y) { + v[a] = (v[a] + v[b] + x) & MASK64; + v[d] = rotr64(v[d] ^ v[a], 32); + v[c] = (v[c] + v[d]) & MASK64; + v[b] = rotr64(v[b] ^ v[c], 24); + v[a] = (v[a] + v[b] + y) & MASK64; + v[d] = rotr64(v[d] ^ v[a], 16); + v[c] = (v[c] + v[d]) & MASK64; + v[b] = rotr64(v[b] ^ v[c], 63); + } + + for (let r = 0; r < 12; r++) { + const s = SIGMA[r % 10]; + G(0,4, 8,12, m[s[ 0]], m[s[ 1]]); + G(1,5, 9,13, m[s[ 2]], m[s[ 3]]); + G(2,6,10,14, m[s[ 4]], m[s[ 5]]); + G(3,7,11,15, m[s[ 6]], m[s[ 7]]); + G(0,5,10,15, m[s[ 8]], m[s[ 9]]); + G(1,6,11,12, m[s[10]], m[s[11]]); + G(2,7, 8,13, m[s[12]], m[s[13]]); + G(3,4, 9,14, m[s[14]], m[s[15]]); + } + + for (let i = 0; i < 8; i++) h[i] = h[i] ^ v[i] ^ v[i + 8]; +} + +/** + * BLAKE2b-256 hash with optional 16-byte personalization. + * @param {Uint8Array} input + * @param {Uint8Array} [personalization] - 16 bytes + * @returns {Uint8Array} 32-byte digest + */ +export function blake2b256(input, personalization) { + const p = new Uint8Array(64); + p[0] = 32; // digest length + p[2] = 1; // fanout + p[3] = 1; // max depth + if (personalization) { + for (let i = 0; i < 16; i++) p[48 + i] = personalization[i] || 0; + } + + const h = new Array(8); + for (let i = 0; i < 8; i++) h[i] = IV[i] ^ readLE64(p, i * 8); + + let t = 0n; + let off = 0; + + if (input.length === 0) { + compress(h, new Uint8Array(128), 0n, true); + } else { + while (off + 128 < input.length) { + t += 128n; + compress(h, input.subarray(off, off + 128), t, false); + off += 128; + } + const last = new Uint8Array(128); + last.set(input.subarray(off)); + t += BigInt(input.length - off); + compress(h, last, t, true); + } + + const out = new Uint8Array(32); + for (let i = 0; i < 4; i++) writeLE64(out, i * 8, h[i]); + return out; +} + +// Hex utilities + +export function hexToBytes(hex) { + if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) { + throw new TypeError("hex must be a non-empty even-length string"); + } + if (!/^[0-9a-fA-F]+$/.test(hex)) { + throw new TypeError("hex contains non-hexadecimal characters"); + } + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(hex.substr(i * 2, 2), 16); + } + return out; +} + +export function bytesToHex(bytes) { + let hex = ""; + for (let i = 0; i < bytes.length; i++) { + hex += bytes[i].toString(16).padStart(2, "0"); + } + return hex; +} + +// ZAP1 BLAKE2b personalizations + +// "NordicShield_\x00\x00\x00" (13 chars + 3 null = 16 bytes) +const LEAF_PERSONAL = new Uint8Array([ + 0x4e,0x6f,0x72,0x64,0x69,0x63,0x53,0x68, + 0x69,0x65,0x6c,0x64,0x5f,0x00,0x00,0x00, +]); + +// "NordicShield_MRK" (16 bytes) +const NODE_PERSONAL = new Uint8Array([ + 0x4e,0x6f,0x72,0x64,0x69,0x63,0x53,0x68, + 0x69,0x65,0x6c,0x64,0x5f,0x4d,0x52,0x4b, +]); + +// "NordicShield_RTK" (16 bytes) +const ROOT_PERSONAL = new Uint8Array([ + 0x4e,0x6f,0x72,0x64,0x69,0x63,0x53,0x68, + 0x69,0x65,0x6c,0x64,0x5f,0x52,0x54,0x4b, +]); + +const ENCODER = new TextEncoder(); +export const COUNT_BOUND_SCHEME = "ZAP1_COUNT_BOUND_V2"; +export const LEGACY_SCHEME = "ZAP1_LEGACY_DUPLICATE_ODD"; +export const LEGACY_ROOT_MAX_ANCHOR_HEIGHT = 3317133; + +// Event-type prefix bytes (known types) +const EVENT_PREFIX = { + PROGRAM_ENTRY: 0x01, + OWNERSHIP_ATTEST: 0x02, +}; + +/** + * Recompute a leaf hash from event data. + * Returns null if the event type's hash formula is unknown. + */ +export function computeLeafHash(eventType, walletHash, serialNumber) { + const prefix = EVENT_PREFIX[eventType]; + if (prefix === undefined) return null; + if (typeof walletHash !== "string") { + throw new TypeError("walletHash must be a string"); + } + + if (eventType === "PROGRAM_ENTRY") { + const walletBytes = ENCODER.encode(walletHash); + if (walletBytes.length > 65535) { + throw new RangeError("walletHash exceeds 65535 UTF-8 bytes"); + } + const input = new Uint8Array(1 + walletBytes.length); + input[0] = prefix; + input.set(walletBytes, 1); + return blake2b256(input, LEAF_PERSONAL); + } + + if (eventType === "OWNERSHIP_ATTEST") { + if (typeof serialNumber !== "string") { + throw new TypeError("serialNumber must be a string"); + } + const walletBytes = ENCODER.encode(walletHash); + const serialBytes = ENCODER.encode(serialNumber); + if (walletBytes.length > 65535 || serialBytes.length > 65535) { + throw new RangeError("ownership fields exceed 65535 UTF-8 bytes"); + } + const input = new Uint8Array(1 + 2 + walletBytes.length + 2 + serialBytes.length); + let off = 0; + input[off++] = prefix; + input[off++] = (walletBytes.length >> 8) & 0xff; + input[off++] = walletBytes.length & 0xff; + input.set(walletBytes, off); off += walletBytes.length; + input[off++] = (serialBytes.length >> 8) & 0xff; + input[off++] = serialBytes.length & 0xff; + input.set(serialBytes, off); + return blake2b256(input, LEAF_PERSONAL); + } + + return null; +} + +/** + * Hash two 32-byte children into a Merkle node. + */ +export function nodeHash(left, right) { + if (!(left instanceof Uint8Array) || left.length !== 32) { + throw new TypeError("left child must be 32 bytes"); + } + if (!(right instanceof Uint8Array) || right.length !== 32) { + throw new TypeError("right child must be 32 bytes"); + } + const input = new Uint8Array(64); + input.set(left, 0); + input.set(right, 32); + return blake2b256(input, NODE_PERSONAL); +} + +export function commitRoot(leafCount, rawRoot) { + if (!(rawRoot instanceof Uint8Array) || rawRoot.length !== 32) { + throw new TypeError("raw root must be 32 bytes"); + } + let count; + if (typeof leafCount === "bigint") { + count = leafCount; + } else if (typeof leafCount === "number" && Number.isSafeInteger(leafCount)) { + count = BigInt(leafCount); + } else { + throw new TypeError("leaf_count must be a safe integer number or bigint"); + } + if (count <= 0n) throw new RangeError("leaf_count must be positive"); + if (count > 0xffffffffffffffffn) throw new RangeError("leaf_count exceeds u64"); + const input = new Uint8Array(41); + input[0] = 1; + let tmp = count; + for (let i = 8; i >= 1; i--) { + input[i] = Number(tmp & 0xffn); + tmp >>= 8n; + } + input.set(rawRoot, 9); + return blake2b256(input, ROOT_PERSONAL); +} + +/** + * Walk a Merkle proof from leaf to root. + * @param {string} leafHashHex + * @param {Array<{hash: string, position: string}>} proof - sibling steps + * @param {number} [leafCount] + * @returns {{ computedRoot: string, legacyRoot: string, rootScheme: string, steps: Array<{left: string, right: string, result: string}> }} + */ +export function walkProof(leafHashHex, proof, leafCount) { + if (typeof leafHashHex !== "string" || !/^[0-9a-fA-F]{64}$/.test(leafHashHex)) { + throw new TypeError("leaf hash must be exactly 32 bytes of hex"); + } + if (!Array.isArray(proof)) throw new TypeError("proof must be an array"); + if (proof.length > 64) throw new RangeError("proof exceeds 64 steps"); + let current = hexToBytes(leafHashHex); + const steps = []; + + for (let index = 0; index < proof.length; index++) { + if (!(index in proof)) throw new TypeError("proof must not contain holes"); + const step = proof[index]; + if (!step || typeof step !== "object" || Array.isArray(step)) { + throw new TypeError("proof step must be an object"); + } + if (typeof step.hash !== "string" || !/^[0-9a-fA-F]{64}$/.test(step.hash)) { + throw new TypeError("proof sibling must be exactly 32 bytes of hex"); + } + if (step.position !== "left" && step.position !== "right") { + throw new TypeError("proof position must be left or right"); + } + const sibling = hexToBytes(step.hash); + let left, right; + if (step.position === "right") { + left = current; right = sibling; + } else { + left = sibling; right = current; + } + current = nodeHash(left, right); + steps.push({ + left: bytesToHex(left), + right: bytesToHex(right), + result: bytesToHex(current), + }); + } + + const legacyRoot = bytesToHex(current); + if (leafCount === undefined || leafCount === null) { + return { + computedRoot: legacyRoot, + legacyRoot, + rootScheme: LEGACY_SCHEME, + steps, + }; + } + + return { + computedRoot: bytesToHex(commitRoot(leafCount, current)), + legacyRoot, + rootScheme: COUNT_BOUND_SCHEME, + steps, + }; +} + +export function isHistoricalLegacyBundle(bundle) { + const scheme = bundle?.root?.scheme; + const height = bundle?.anchor?.height; + return ( + scheme === LEGACY_SCHEME && + typeof height === "number" && + Number.isSafeInteger(height) && + height > 0 && + height <= LEGACY_ROOT_MAX_ANCHOR_HEIGHT + ); +} diff --git a/src/index.d.ts b/src/index.d.ts index 89e1b29..4a51945 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,91 +1,114 @@ -/** - * @frontier-compute/zap1 - ZAP1 Merkle proof verification (WASM-powered) - */ - -/** Initialize the WASM module. Called automatically on first use. */ -export function init(): Promise; +export type Zap1EventType = + | "PROGRAM_ENTRY" + | "OWNERSHIP_ATTEST" + | "CONTRACT_ANCHOR" + | "DEPLOYMENT" + | "HOSTING_PAYMENT" + | "SHIELD_RENEWAL" + | "TRANSFER" + | "EXIT" + | "MERKLE_ROOT" + | "STAKING_DEPOSIT" + | "STAKING_WITHDRAW" + | "STAKING_REWARD" + | "GOVERNANCE_PROPOSAL" + | "GOVERNANCE_VOTE" + | "GOVERNANCE_RESULT" + | "AGENT_REGISTER" + | "AGENT_POLICY" + | "AGENT_ACTION"; -/** All 9 deployed ZAP1 event types. */ -export const EVENT_TYPES: readonly string[]; +export const EVENT_TYPES: readonly Zap1EventType[]; +export const LEAF_HASH_TYPES: readonly [ + "PROGRAM_ENTRY", + "OWNERSHIP_ATTEST", +]; +export const COUNT_BOUND_SCHEME: "ZAP1_COUNT_BOUND_V2"; +export const LEGACY_SCHEME: "ZAP1_LEGACY_DUPLICATE_ODD"; +export const LEGACY_ROOT_MAX_ANCHOR_HEIGHT: 3317133; -/** Event-specific payload for computeLeafHash. */ export interface LeafPayload { walletHash?: string; serialNumber?: string; - facilityId?: string; - contractSha256?: string; - oldWalletHash?: string; - newWalletHash?: string; - timestamp?: number; - month?: number; - year?: number; - rootHash?: string; + [extra: string]: unknown; } -/** One step in a Merkle inclusion proof. */ export interface ProofStep { hash: string; position: "left" | "right"; } -/** Anchor transaction reference. */ -export interface Anchor { - txid: string; - height: number; +/** Untrusted service metadata; not proof of memo binding. */ +export interface AnchorReference { + txid?: string | null; + height?: number | null; + [extra: string]: unknown; } -/** Leaf metadata from the API. */ export interface LeafInfo { hash: string; - event_type: string; + event_type?: string; wallet_hash?: string; serial_number?: string; created_at?: string; + [extra: string]: unknown; } -/** Normalized proof bundle. */ export interface ProofBundle { leaf_hash: string; proof: ProofStep[]; root: string; - anchor: Anchor | null; + leaf_count: number | bigint | null; + root_scheme: string | null; + anchor: AnchorReference | null; leaf: LeafInfo | null; - protocol: string; - version: string; + protocol: string | null; + version: string | null; } -/** Raw proof bundle as returned by the API. */ export interface RawBundle { leaf_hash?: string; - leaf?: LeafInfo & { hash: string }; + leaf?: LeafInfo; proof: ProofStep[]; - root: string | { hash: string }; - anchor?: Anchor; + root: + | string + | { + hash: string; + leaf_count?: number | bigint | null; + scheme?: string | null; + [extra: string]: unknown; + }; + root_hash?: string; + leaf_count?: number | bigint | null; + root_scheme?: string | null; + anchor?: AnchorReference | null; protocol?: string; - version?: string; + version?: string | number; + [extra: string]: unknown; } +export interface VerifyOptions { + allowHistoricalLegacy?: boolean; +} + +/** Compatibility no-op retained for 0.1.x callers. */ +export function init(): Promise; + /** - * Compute the leaf hash for an ZAP1 event. - * Returns null for event types not yet supported client-side. + * Compute a typed leaf hash. Defined-but-unsupported types return null; + * unknown types and malformed supported payloads reject. */ export function computeLeafHash( - eventType: string, - payload: LeafPayload + eventType: Zap1EventType | string, + payload: LeafPayload, ): Promise; -/** - * Verify a Merkle inclusion proof. - * Bundle must have leaf_hash, proof, and root fields. - */ -export function verifyProof(bundle: ProofBundle): Promise; - -/** - * Compute a Merkle node hash: BLAKE2b-256("NordicShield_MRK", left || right). - */ export function nodeHash(leftHex: string, rightHex: string): Promise; +export function commitRoot(rawRootHex: string, leafCount: number | bigint): string; +export function parseBundle(input: string | RawBundle | ProofBundle): ProofBundle; -/** - * Parse a proof bundle from JSON or an object. Normalizes the structure. - */ -export function parseBundle(input: string | RawBundle): ProofBundle; +/** Verify consistency with a supplied root, not on-chain memo binding. */ +export function verifyProof( + bundle: string | RawBundle | ProofBundle, + options?: VerifyOptions, +): Promise; diff --git a/src/index.js b/src/index.js index b5703c1..7a8a8b4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,39 +1,25 @@ /** - * @frontier-compute/zap1 - ZAP1 Merkle proof verification (WASM-powered) + * @frontiercompute/zap1 - strict ZAP1 Merkle-bundle verification. * - * Wraps the zap1-verify Rust crate compiled to WebAssembly. - * BLAKE2b-256 with NordicShield personalizations, Merkle proof walking, - * and leaf hash computation for all 9 deployed ZAP1 event types. + * This module verifies consistency against a supplied root. Anchor metadata is + * not treated as proof of transaction existence or encrypted-memo binding. */ -let wasmModule = null; -let initPromise = null; +import { + bytesToHex, + commitRoot as commitRootBytes, + computeLeafHash as computeLeafHashBytes, + COUNT_BOUND_SCHEME, + hexToBytes, + LEGACY_ROOT_MAX_ANCHOR_HEIGHT, + LEGACY_SCHEME, + nodeHash as nodeHashBytes, + walkProof, +} from "./blake2b.js"; -/** - * Initialize the WASM module. Called automatically on first use. - * Can be called explicitly to preload. - * @returns {Promise} - */ -export async function init() { - if (wasmModule) return; - if (initPromise) return initPromise; - initPromise = (async () => { - const wasm = await import("../wasm/zap1_verify_wasm.js"); - const wasmUrl = new URL("../wasm/zap1_verify_wasm_bg.wasm", import.meta.url); - await wasm.default(wasmUrl); - wasmModule = wasm; - })(); - return initPromise; -} - -async function ensureInit() { - if (!wasmModule) await init(); -} +export { COUNT_BOUND_SCHEME, LEGACY_ROOT_MAX_ANCHOR_HEIGHT, LEGACY_SCHEME }; -// Event types - -/** All 9 deployed ZAP1 event types. */ -export const EVENT_TYPES = [ +export const EVENT_TYPES = Object.freeze([ "PROGRAM_ENTRY", "OWNERSHIP_ATTEST", "CONTRACT_ANCHOR", @@ -43,97 +29,366 @@ export const EVENT_TYPES = [ "TRANSFER", "EXIT", "MERKLE_ROOT", -]; + "STAKING_DEPOSIT", + "STAKING_WITHDRAW", + "STAKING_REWARD", + "GOVERNANCE_PROPOSAL", + "GOVERNANCE_VOTE", + "GOVERNANCE_RESULT", + "AGENT_REGISTER", + "AGENT_POLICY", + "AGENT_ACTION", +]); + +export const LEAF_HASH_TYPES = Object.freeze([ + "PROGRAM_ENTRY", + "OWNERSHIP_ATTEST", +]); + +/** Compatibility no-op retained for callers of 0.1.x. */ +export async function init() {} + +function requireHex32(value, label) { + if (typeof value !== "string" || !/^[0-9a-fA-F]{64}$/.test(value)) { + throw new TypeError(`${label} must be exactly 32 bytes of hex`); + } + return value.toLowerCase(); +} + +function requirePlainObject(value, label) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new TypeError(`${label} must be an object`); + } + const prototype = Object.getPrototypeOf(value); + if (prototype !== Object.prototype && prototype !== null) { + throw new TypeError(`${label} must be a plain object`); + } + return value; +} + +function hasOwn(value, key) { + return Object.prototype.hasOwnProperty.call(value, key); +} + +function normalizeLeafCount(value, label = "leaf_count") { + let count; + if (typeof value === "bigint") { + count = value; + } else if (typeof value === "number" && Number.isSafeInteger(value)) { + count = BigInt(value); + } else { + throw new TypeError(`${label} must be a safe integer number or bigint`); + } + if (count <= 0n) throw new RangeError(`${label} must be positive`); + if (count > 0xffffffffffffffffn) { + throw new RangeError(`${label} exceeds u64`); + } + return count; +} -// Leaf hash computation +function normalizeOptionalCount(value, label) { + return value === null ? null : normalizeLeafCount(value, label); +} + +function normalizeOptionalScheme(value, label) { + if (value === null) return null; + if (typeof value !== "string") { + throw new TypeError(`${label} must be a string or null`); + } + return value; +} /** - * Compute the leaf hash for an ZAP1 event. - * - * @param {string} eventType - one of EVENT_TYPES - * @param {object} payload - event-specific fields - * @returns {Promise} 64-char hex leaf hash - * - * Supported payloads: - * PROGRAM_ENTRY: { walletHash: string } - * OWNERSHIP_ATTEST: { walletHash: string, serialNumber: string } - * (other types return null - use verifyProof for path-only verification) + * Validate that a sibling path can exist in the deployed carry-up tree shape. + * Odd final nodes advance unchanged and therefore consume no proof step. */ -export async function computeLeafHash(eventType, payload) { - await ensureInit(); - switch (eventType) { - case "PROGRAM_ENTRY": - return wasmModule.computeProgramEntry(payload.walletHash); - case "OWNERSHIP_ATTEST": - return wasmModule.computeOwnershipAttest( - payload.walletHash, - payload.serialNumber - ); - default: - return null; +function hasPossibleCountBoundPath(leafCount, proof) { + let width = normalizeLeafCount(leafCount); + const lowerWidths = []; + while (width > 1n) { + lowerWidths.push(width); + width = (width + 1n) / 2n; } -} -// Proof verification + let parentIndex = 0n; + let proofIndex = proof.length - 1; + for (let layer = lowerWidths.length - 1; layer >= 0; layer--) { + const lowerWidth = lowerWidths[layer]; + const lastParentIndex = (lowerWidth - 1n) / 2n; + if (lowerWidth % 2n === 1n && parentIndex === lastParentIndex) { + parentIndex = lowerWidth - 1n; + continue; + } + + if (proofIndex < 0) return false; + const position = proof[proofIndex--].position; + const childIndex = + position === "right" ? parentIndex * 2n : parentIndex * 2n + 1n; + if (childIndex >= lowerWidth) return false; + parentIndex = childIndex; + } + + return proofIndex === -1; +} /** - * Verify a Merkle inclusion proof. - * - * @param {object} bundle - proof bundle from the API - * @param {string} bundle.leaf_hash - 64-char hex leaf hash - * @param {Array<{hash: string, position: "left"|"right"}>} bundle.proof - * @param {string} bundle.root - 64-char hex expected root - * @returns {Promise} + * Validate a legacy duplicate-odd path and return the proof layers where the + * sibling must be an exact duplicate of the current node. */ -export async function verifyProof(bundle) { - await ensureInit(); - return wasmModule.verifyProof({ - leaf_hash: bundle.leaf_hash, - proof: bundle.proof, - root: bundle.root, - }); +function legacyDuplicateLayers(leafCount, proof) { + let width = normalizeLeafCount(leafCount); + const lowerWidths = []; + while (width > 1n) { + lowerWidths.push(width); + width = (width + 1n) / 2n; + } + if (proof.length !== lowerWidths.length) return null; + + let parentIndex = 0n; + const duplicateLayers = []; + for (let layer = lowerWidths.length - 1; layer >= 0; layer--) { + const lowerWidth = lowerWidths[layer]; + const step = proof[layer]; + const lastParentIndex = (lowerWidth - 1n) / 2n; + if (lowerWidth % 2n === 1n && parentIndex === lastParentIndex) { + if (step.position !== "right") return null; + parentIndex = lowerWidth - 1n; + duplicateLayers.push(layer); + continue; + } + + const childIndex = + step.position === "right" + ? parentIndex * 2n + : parentIndex * 2n + 1n; + if (childIndex >= lowerWidth) return null; + parentIndex = childIndex; + } + return duplicateLayers; } /** - * Compute a Merkle node hash: BLAKE2b-256("NordicShield_MRK", left || right). - * - * @param {string} leftHex - 64-char hex - * @param {string} rightHex - 64-char hex - * @returns {Promise} 64-char hex result + * Compute a typed leaf hash where a client-side formula is defined. + * Returns null for a defined event type without a local formula. + * Throws for an unknown event type or malformed supported payload. */ +export async function computeLeafHash(eventType, payload) { + if (typeof eventType !== "string" || !EVENT_TYPES.includes(eventType)) { + throw new RangeError("unknown ZAP1 event type"); + } + if (!LEAF_HASH_TYPES.includes(eventType)) return null; + requirePlainObject(payload, "payload"); + const result = computeLeafHashBytes( + eventType, + payload.walletHash, + payload.serialNumber, + ); + return bytesToHex(result); +} + +/** Compute a domain-separated Merkle node hash. */ export async function nodeHash(leftHex, rightHex) { - await ensureInit(); - return wasmModule.nodeHash(leftHex, rightHex); + const left = hexToBytes(requireHex32(leftHex, "left hash")); + const right = hexToBytes(requireHex32(rightHex, "right hash")); + return bytesToHex(nodeHashBytes(left, right)); } -// Bundle parsing +/** Bind a raw tree root to a positive u64 leaf count. */ +export function commitRoot(rawRootHex, leafCount) { + const rawRoot = hexToBytes(requireHex32(rawRootHex, "raw root")); + return bytesToHex(commitRootBytes(leafCount, rawRoot)); +} /** - * Parse a proof bundle from JSON (API response or downloaded file). - * Normalizes the structure for use with verifyProof(). - * - * @param {string|object} input - JSON string or parsed object - * @returns {object} Normalized bundle with leaf_hash, proof, root, anchor, leaf + * Strictly normalize a flat or nested API proof bundle. + * Malformed fields throw; no scheme or count is inferred. */ export function parseBundle(input) { const data = typeof input === "string" ? JSON.parse(input) : input; + requirePlainObject(data, "bundle"); + + const leafRecord = + data.leaf === undefined || data.leaf === null + ? null + : requirePlainObject(data.leaf, "leaf"); + const nestedLeafHash = + leafRecord && hasOwn(leafRecord, "hash") + ? requireHex32(leafRecord.hash, "leaf.hash") + : null; + const flatLeafHash = hasOwn(data, "leaf_hash") + ? requireHex32(data.leaf_hash, "leaf_hash") + : null; + if ( + nestedLeafHash !== null && + flatLeafHash !== null && + nestedLeafHash !== flatLeafHash + ) { + throw new TypeError("conflicting leaf hash representations"); + } + const leafHash = nestedLeafHash ?? flatLeafHash; + if (leafHash === null) throw new TypeError("leaf hash is required"); + + if (!Array.isArray(data.proof)) throw new TypeError("proof must be an array"); + if (data.proof.length > 64) throw new RangeError("proof exceeds 64 steps"); + const proof = []; + for (let index = 0; index < data.proof.length; index++) { + if (!(index in data.proof)) throw new TypeError("proof must not contain holes"); + const step = requirePlainObject(data.proof[index], "proof step"); + if (step.position !== "left" && step.position !== "right") { + throw new TypeError("proof position must be left or right"); + } + proof.push({ + hash: requireHex32(step.hash, "proof sibling"), + position: step.position, + }); + } + + let rootHash; + let leafCount = null; + let rootScheme = null; + if (typeof data.root === "string") { + rootHash = requireHex32(data.root, "root"); + if (hasOwn(data, "leaf_count")) { + normalizeOptionalCount(data.leaf_count, "leaf_count"); + leafCount = data.leaf_count; + } + if (hasOwn(data, "root_scheme")) { + rootScheme = normalizeOptionalScheme(data.root_scheme, "root_scheme"); + } + } else { + const rootRecord = requirePlainObject(data.root, "root"); + rootHash = requireHex32(rootRecord.hash, "root.hash"); + + const nestedCountPresent = hasOwn(rootRecord, "leaf_count"); + const flatCountPresent = hasOwn(data, "leaf_count"); + const nestedCount = nestedCountPresent + ? normalizeOptionalCount(rootRecord.leaf_count, "root.leaf_count") + : null; + const flatCount = flatCountPresent + ? normalizeOptionalCount(data.leaf_count, "leaf_count") + : null; + if ( + nestedCountPresent && + flatCountPresent && + nestedCount !== flatCount + ) { + throw new TypeError("conflicting leaf_count representations"); + } + leafCount = nestedCountPresent + ? rootRecord.leaf_count + : flatCountPresent + ? data.leaf_count + : null; + + const nestedSchemePresent = hasOwn(rootRecord, "scheme"); + const flatSchemePresent = hasOwn(data, "root_scheme"); + const nestedScheme = nestedSchemePresent + ? normalizeOptionalScheme(rootRecord.scheme, "root.scheme") + : null; + const flatScheme = flatSchemePresent + ? normalizeOptionalScheme(data.root_scheme, "root_scheme") + : null; + if ( + nestedSchemePresent && + flatSchemePresent && + nestedScheme !== flatScheme + ) { + throw new TypeError("conflicting root scheme representations"); + } + rootScheme = nestedSchemePresent ? nestedScheme : flatScheme; + } + if (hasOwn(data, "root_hash")) { + const flatRootHash = requireHex32(data.root_hash, "root_hash"); + if (flatRootHash !== rootHash) { + throw new TypeError("conflicting root hash representations"); + } + } - // Normalize: API returns nested objects - const leafHash = data.leaf?.hash || data.leaf_hash; - const proof = data.proof || []; - const rootHash = data.root?.hash || data.root; - const anchor = data.anchor || null; + let anchor = null; + if (data.anchor !== undefined && data.anchor !== null) { + anchor = { ...requirePlainObject(data.anchor, "anchor") }; + } + + let protocol = null; + if (hasOwn(data, "protocol")) { + if (typeof data.protocol !== "string") { + throw new TypeError("protocol must be a string"); + } + protocol = data.protocol; + } + + let version = null; + if (hasOwn(data, "version")) { + if ( + typeof data.version !== "string" && + !(typeof data.version === "number" && Number.isSafeInteger(data.version)) + ) { + throw new TypeError("version must be a string or safe integer number"); + } + version = String(data.version); + } return { leaf_hash: leafHash, - proof: proof.map((s) => ({ - hash: s.hash, - position: s.position, - })), + proof, root: rootHash, + leaf_count: leafCount, + root_scheme: rootScheme, anchor, - leaf: data.leaf || null, - protocol: data.protocol || "ZAP1", - version: data.version || "1", + leaf: leafRecord ? { ...leafRecord, hash: leafHash } : null, + protocol, + version, }; } + +/** + * Verify Merkle-bundle consistency. + * + * Current bundles require the exact COUNT_BOUND_V2 scheme and a positive leaf + * count. Historical legacy verification is opt-in and height-gated. + */ +export async function verifyProof(input, options = {}) { + requirePlainObject(options, "options"); + const bundle = parseBundle(input); + + if (bundle.protocol !== null && bundle.protocol !== "ZAP1") return false; + + if (bundle.root_scheme === COUNT_BOUND_SCHEME) { + if (bundle.version !== null && bundle.version !== "2") return false; + if (bundle.leaf_count === null || bundle.leaf_count === undefined) return false; + if (!hasPossibleCountBoundPath(bundle.leaf_count, bundle.proof)) return false; + const walked = walkProof( + bundle.leaf_hash, + bundle.proof, + bundle.leaf_count, + ); + return walked.computedRoot === bundle.root; + } + + if (bundle.root_scheme === LEGACY_SCHEME) { + if (bundle.version !== null && bundle.version !== "2") return false; + if (options.allowHistoricalLegacy !== true) return false; + if (bundle.leaf_count === null || bundle.leaf_count === undefined) return false; + const height = bundle.anchor?.height; + if ( + typeof height !== "number" || + !Number.isSafeInteger(height) || + height <= 0 || + height > LEGACY_ROOT_MAX_ANCHOR_HEIGHT + ) { + return false; + } + const duplicateLayers = legacyDuplicateLayers( + bundle.leaf_count, + bundle.proof, + ); + if (duplicateLayers === null) return false; + const walked = walkProof(bundle.leaf_hash, bundle.proof); + for (const layer of duplicateLayers) { + if (walked.steps[layer].left !== walked.steps[layer].right) return false; + } + return walked.legacyRoot === bundle.root; + } + + return false; +} diff --git a/test/browser-test.mjs b/test/browser-test.mjs new file mode 100644 index 0000000..920135e --- /dev/null +++ b/test/browser-test.mjs @@ -0,0 +1,120 @@ +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, resolve } from "node:path"; +import { chromium } from "playwright"; + +const packageRoot = resolve(process.argv[2] ?? "dist"); +const mime = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript; charset=utf-8", +}; + +const server = createServer(async (request, response) => { + try { + if (request.url === "/") { + response.writeHead(200, { "content-type": mime[".html"] }); + response.end("zap1 browser test"); + return; + } + if (request.url !== "/index.js" && request.url !== "/blake2b.js") { + response.writeHead(404); + response.end("not found"); + return; + } + const path = resolve(packageRoot, request.url.slice(1)); + const bytes = await readFile(path); + response.writeHead(200, { + "content-type": mime[extname(path)] ?? "application/octet-stream", + }); + response.end(bytes); + } catch (error) { + response.writeHead(500); + response.end(String(error)); + } +}); + +await new Promise((resolveListen) => + server.listen(0, "127.0.0.1", resolveListen), +); +const address = server.address(); +const origin = `http://127.0.0.1:${address.port}`; + +const browser = await chromium.launch({ headless: true }); +try { + const page = await browser.newPage(); + await page.goto(origin); + const result = await page.evaluate(async () => { + const zap1 = await import("/index.js"); + const leaf1 = + "075b00df286038a7b3f6bb70054df61343e3481fba579591354a00214e9e019b"; + const leaf2 = + "de62554ad3867a59895befa7216686c923fc86245231e8fb6bd709a20e1fd133"; + const root = + "94421ae28effbe52f651b33eb62c3b428d2ae62be578e05d471cba9794225bbd"; + const bundle = { + leaf_hash: leaf2, + proof: [{ hash: leaf1, position: "left" }], + root: { + hash: root, + leaf_count: 2, + scheme: zap1.COUNT_BOUND_SCHEME, + }, + protocol: "ZAP1", + version: "2", + }; + const valid = await zap1.verifyProof(bundle); + const wrongCountBundle = structuredClone(bundle); + wrongCountBundle.root.leaf_count = 3; + const wrongCount = await zap1.verifyProof(wrongCountBundle); + const unknownSchemeBundle = structuredClone(bundle); + unknownSchemeBundle.root.scheme = "ZAP1_UNKNOWN"; + const unknownScheme = await zap1.verifyProof(unknownSchemeBundle); + const malformedBundle = structuredClone(bundle); + malformedBundle.proof[0].position = "sideways"; + let malformedRejected = false; + try { + await zap1.verifyProof(malformedBundle); + } catch (error) { + malformedRejected = error instanceof TypeError; + } + const legacyRootAsLeaf = await zap1.verifyProof( + { + leaf_hash: root, + proof: [], + root: { + hash: root, + leaf_count: 2, + scheme: zap1.LEGACY_SCHEME, + }, + anchor: { height: 3286631 }, + protocol: "ZAP1", + version: "2", + }, + { allowHistoricalLegacy: true }, + ); + return { + valid, + wrongCount, + unknownScheme, + malformedRejected, + legacyRootAsLeaf, + eventTypes: zap1.EVENT_TYPES.length, + typedTypes: zap1.LEAF_HASH_TYPES.length, + }; + }); + if ( + result.valid !== true || + result.wrongCount !== false || + result.unknownScheme !== false || + result.malformedRejected !== true || + result.legacyRootAsLeaf !== false || + result.eventTypes !== 18 || + result.typedTypes !== 2 + ) { + throw new Error(`browser matrix failed: ${JSON.stringify(result)}`); + } + console.log("browser matrix passed"); +} finally { + await browser.close(); + await new Promise((resolveClose) => server.close(resolveClose)); +} diff --git a/test/test.js b/test/test.js index edd0251..2b3dd53 100644 --- a/test/test.js +++ b/test/test.js @@ -1,22 +1,56 @@ -import assert from "node:assert"; -import { readFileSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname, join } from "node:path"; - -// Pre-init WASM with initSync so we skip the fetch() path that fails in Node -const __dirname = dirname(fileURLToPath(import.meta.url)); -const wasmJs = await import(join(__dirname, "..", "wasm", "zap1_verify_wasm.js")); -const wasmBytes = readFileSync(join(__dirname, "..", "wasm", "zap1_verify_wasm_bg.wasm")); -wasmJs.initSync({ module: wasmBytes }); - +import assert from "node:assert/strict"; import { - init, - EVENT_TYPES, + commitRoot, computeLeafHash, + COUNT_BOUND_SCHEME, + EVENT_TYPES, + init, + LEAF_HASH_TYPES, + LEGACY_ROOT_MAX_ANCHOR_HEIGHT, + LEGACY_SCHEME, nodeHash, - verifyProof, parseBundle, + verifyProof, } from "../src/index.js"; +import { blake2b256, bytesToHex } from "../src/blake2b.js"; + +const LEAF1 = "075b00df286038a7b3f6bb70054df61343e3481fba579591354a00214e9e019b"; +const LEAF2 = "de62554ad3867a59895befa7216686c923fc86245231e8fb6bd709a20e1fd133"; +const RAW_ROOT = "024e36515ea30efc15a0a7962dd8f677455938079430b9eab174f46a4328a07a"; +const V2_ROOT = "94421ae28effbe52f651b33eb62c3b428d2ae62be578e05d471cba9794225bbd"; + +const currentBundle = { + leaf: { + hash: LEAF2, + event_type: "OWNERSHIP_ATTEST", + }, + proof: [{ hash: LEAF1, position: "left" }], + root: { + hash: V2_ROOT, + leaf_count: 2, + scheme: COUNT_BOUND_SCHEME, + }, + anchor: { txid: null, height: null }, + protocol: "ZAP1", + version: "2", +}; + +const legacyBundle = { + leaf: { + hash: LEAF2, + event_type: "OWNERSHIP_ATTEST", + }, + proof: [{ hash: LEAF1, position: "left" }], + root: { + hash: RAW_ROOT, + leaf_count: 2, + scheme: LEGACY_SCHEME, + }, + anchor: { + txid: "98e1d6a01614c464c237f982d9dc2138c5f8aa08342f67b867a18a4ce998af9a", + height: 3286631, + }, +}; let passed = 0; let failed = 0; @@ -25,188 +59,461 @@ async function test(name, fn) { try { await fn(); passed++; - console.log(` ok ${name}`); - } catch (e) { + console.log(`ok ${name}`); + } catch (error) { failed++; - console.error(` FAIL ${name}`); - console.error(` ${e.message}`); + console.error(`FAIL ${name}`); + console.error(error); } } -// Test vectors from zap1-verify/src/lib.rs (E2E_PROOF_20260327) -const LEAF1 = "075b00df286038a7b3f6bb70054df61343e3481fba579591354a00214e9e019b"; -const LEAF2 = "de62554ad3867a59895befa7216686c923fc86245231e8fb6bd709a20e1fd133"; -const ROOT = "024e36515ea30efc15a0a7962dd8f677455938079430b9eab174f46a4328a07a"; - -console.log("zap1-js tests\n"); - -// Init -await test("init() completes without error", async () => { - await init(); +await test("BLAKE2b-256 matches independent boundary vectors", () => { + const vectors = [ + [0, "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8", "543dd29451e4e736621ae1b70d1db8d9acddaf5def0f360f0ebd459ba7cf7eaa"], + [1, "44e9e1dfd31e4c8c8e05d6db76912790ae9b2f989463f59f709cdd3df7393675", "e590e659e08dc8f8de9cf98c43ac32d9def7ebdac7c4659a8f96ce378934781e"], + [3, "9e23a08fdcfe61165716b5c09290f1127e1fefd8ced7f44bf6bc972f6e808c1d", "e9a667696d80bcd010b2dc273f97e41938673d8c12b32fe5ac50c9cf8f52f3ca"], + [127, "2bafc551fac2b7471a310590f926931e76efa61773f721c11a58795c861fbe01", "79983c90a167518e5d42bb801ab15038bbf38898ae5f49685e05094587986192"], + [128, "d8964bd9acf62d8064e4569331ca8474ac23c1cc1a4ace160880bcea85b837f9", "d2c1b15171cefa73813351594bc7efa5303103e62e0d1441cd761a7f001788cc"], + [129, "52d53b85c007796f8cc02865bd710c6bea2e1e19ddb7aef99fbbd48b87393f22", "e9629c8e65ba4722944615fd59fd8a92597fd043cb1537a8d92149ec41f24ab8"], + [255, "748a4aa0dc8cb47c53c4f5c91aed59ee63ff1b5b4864f88beed92f3fdd593aa3", "c64523ae59477b84a9f63a39822e51ce43581079f985a9d600d48ea5c5213d80"], + [256, "781150a9570dd6a6086e11b0f60bb629707d99523fb8da9985403a28c6864a9d", "620d008c782b43bc0b6a98fb0c51c3a2038334d647b045a5d4a710b44efea1d3"], + [257, "7efdee97778ed5d97ea9ccd7835289e0d92a27a6ce1d3da32cbaed8369cc7099", "6c5e694b44daa11fd24e3b2459b49d2feebb3c97f12072a9008b664032c8e590"], + [1024, "5760f7d34eb98e6dd16591f6412cf5ce43878e9ad7c28953595feb6746827baa", "59d58e94cb5dbbcda979b859983d5a0ac9b7bb9aeb857127ce526d9f6a7a77b9"], + ]; + const personal = new TextEncoder().encode("NordicShield_MRK"); + for (const [length, plain, personalized] of vectors) { + const input = Uint8Array.from( + { length }, + (_, index) => (index * 37 + 11) % 256, + ); + assert.equal(bytesToHex(blake2b256(input)), plain); + assert.equal(bytesToHex(blake2b256(input, personal)), personalized); + } }); -await test("init() is idempotent", async () => { +await test("init is an idempotent compatibility no-op", async () => { await init(); await init(); }); -// EVENT_TYPES -await test("EVENT_TYPES has 9 entries", () => { - assert.strictEqual(EVENT_TYPES.length, 9); +await test("canonical event registry is immutable and complete", () => { + assert.equal(EVENT_TYPES.length, 18); + assert.equal(EVENT_TYPES[0], "PROGRAM_ENTRY"); + assert.equal(EVENT_TYPES[17], "AGENT_ACTION"); + assert.throws(() => EVENT_TYPES.push("UNKNOWN"), TypeError); + assert.deepEqual(LEAF_HASH_TYPES, ["PROGRAM_ENTRY", "OWNERSHIP_ATTEST"]); }); -await test("EVENT_TYPES first and last", () => { - assert.strictEqual(EVENT_TYPES[0], "PROGRAM_ENTRY"); - assert.strictEqual(EVENT_TYPES[8], "MERKLE_ROOT"); -}); - -// computeLeafHash - PROGRAM_ENTRY -await test("computeLeafHash PROGRAM_ENTRY - e2e vector", async () => { - const hash = await computeLeafHash("PROGRAM_ENTRY", { - walletHash: "e2e_wallet_20260327", - }); - assert.strictEqual(hash, LEAF1); +await test("PROGRAM_ENTRY typed vector", async () => { + assert.equal( + await computeLeafHash("PROGRAM_ENTRY", { + walletHash: "e2e_wallet_20260327", + }), + LEAF1, + ); }); -await test("computeLeafHash PROGRAM_ENTRY - wallet_abc vector", async () => { - const hash = await computeLeafHash("PROGRAM_ENTRY", { - walletHash: "wallet_abc", - }); - assert.strictEqual( - hash, - "344a05bf81faf6e2d54a0e52ea0267aff0244998eb1ee27adf5627413e92f089" +await test("OWNERSHIP_ATTEST typed vector", async () => { + assert.equal( + await computeLeafHash("OWNERSHIP_ATTEST", { + walletHash: "e2e_wallet_20260327", + serialNumber: "Z15P-E2E-001", + }), + LEAF2, ); }); -// computeLeafHash - OWNERSHIP_ATTEST -await test("computeLeafHash OWNERSHIP_ATTEST - e2e vector", async () => { - const hash = await computeLeafHash("OWNERSHIP_ATTEST", { - walletHash: "e2e_wallet_20260327", - serialNumber: "Z15P-E2E-001", - }); - assert.strictEqual(hash, LEAF2); +await test("defined unsupported and unknown types are distinct", async () => { + assert.equal(await computeLeafHash("DEPLOYMENT", {}), null); + await assert.rejects(() => computeLeafHash("NOT_A_TYPE", {}), RangeError); }); -await test("computeLeafHash OWNERSHIP_ATTEST - wallet_abc vector", async () => { - const hash = await computeLeafHash("OWNERSHIP_ATTEST", { - walletHash: "wallet_abc", - serialNumber: "Z15P-2026-001", - }); - assert.strictEqual( - hash, - "5d77b9a3435948a98099267e510a14663cc0fa80afd2a3ee5fb4363f6ecdfa13" +await test("typed hashing rejects missing and oversized fields", async () => { + await assert.rejects( + () => computeLeafHash("PROGRAM_ENTRY", {}), + TypeError, + ); + await assert.rejects( + () => + computeLeafHash("OWNERSHIP_ATTEST", { + walletHash: "wallet", + }), + TypeError, + ); + await assert.rejects( + () => + computeLeafHash("OWNERSHIP_ATTEST", { + walletHash: "x".repeat(65536), + serialNumber: "serial", + }), + RangeError, ); }); -// computeLeafHash - unsupported type returns null -await test("computeLeafHash unsupported type returns null", async () => { - const hash = await computeLeafHash("DEPLOYMENT", { serialNumber: "x" }); - assert.strictEqual(hash, null); +await test("node and count-bound root vectors", async () => { + assert.equal(await nodeHash(LEAF1, LEAF2), RAW_ROOT); + assert.equal(commitRoot(RAW_ROOT, 2), V2_ROOT); }); -// nodeHash -await test("nodeHash - e2e root vector", async () => { - const root = await nodeHash(LEAF1, LEAF2); - assert.strictEqual(root, ROOT); +await test("current nested bundle verifies", async () => { + assert.equal(await verifyProof(currentBundle), true); + assert.equal(parseBundle(currentBundle).leaf_count, 2); }); -await test("nodeHash is not commutative", async () => { - const ab = await nodeHash(LEAF1, LEAF2); - const ba = await nodeHash(LEAF2, LEAF1); - assert.notStrictEqual(ab, ba); +await test("wrong leaf count fails", async () => { + const wrong = structuredClone(currentBundle); + wrong.root.leaf_count = 3; + assert.equal(await verifyProof(wrong), false); }); -// verifyProof -await test("verifyProof - leaf1 with leaf2 sibling right", async () => { - const valid = await verifyProof({ - leaf_hash: LEAF1, - proof: [{ hash: LEAF2, position: "right" }], - root: ROOT, - }); - assert.strictEqual(valid, true); +await test("unknown or missing scheme fails closed", async () => { + const unknown = structuredClone(currentBundle); + unknown.root.scheme = "ZAP1_UNKNOWN"; + assert.equal(await verifyProof(unknown), false); + const missing = structuredClone(currentBundle); + delete missing.root.scheme; + assert.equal(await verifyProof(missing), false); }); -await test("verifyProof - leaf2 with leaf1 sibling left", async () => { - const valid = await verifyProof({ - leaf_hash: LEAF2, - proof: [{ hash: LEAF1, position: "left" }], - root: ROOT, - }); - assert.strictEqual(valid, true); +await test("missing count never falls back to legacy", async () => { + const missing = structuredClone(currentBundle); + delete missing.root.leaf_count; + assert.equal(await verifyProof(missing), false); }); -await test("verifyProof - wrong root returns false", async () => { - const valid = await verifyProof({ - leaf_hash: LEAF1, - proof: [{ hash: LEAF2, position: "right" }], - root: "ff".repeat(32), - }); - assert.strictEqual(valid, false); +await test("legacy is opt-in and height gated", async () => { + assert.equal(await verifyProof(legacyBundle), false); + assert.equal( + await verifyProof(legacyBundle, { allowHistoricalLegacy: true }), + true, + ); + for (const height of [ + LEGACY_ROOT_MAX_ANCHOR_HEIGHT + 1, + 0, + -1, + "", + "3286631", + false, + 1.5, + ]) { + const candidate = structuredClone(legacyBundle); + candidate.anchor.height = height; + assert.equal( + await verifyProof(candidate, { allowHistoricalLegacy: true }), + false, + ); + } }); -await test("verifyProof - empty proof, leaf is root", async () => { - const valid = await verifyProof({ - leaf_hash: LEAF1, - proof: [], - root: LEAF1, - }); - assert.strictEqual(valid, true); -}); - -// parseBundle - flat input -await test("parseBundle normalizes flat input", () => { - const bundle = parseBundle({ - leaf_hash: LEAF1, - proof: [{ hash: LEAF2, position: "right" }], - root: ROOT, - }); - assert.strictEqual(bundle.leaf_hash, LEAF1); - assert.strictEqual(bundle.root, ROOT); - assert.strictEqual(bundle.proof.length, 1); - assert.strictEqual(bundle.proof[0].hash, LEAF2); - assert.strictEqual(bundle.proof[0].position, "right"); - assert.strictEqual(bundle.protocol, "ZAP1"); - assert.strictEqual(bundle.version, "1"); -}); - -// parseBundle - nested API response -await test("parseBundle normalizes nested API response", () => { - const bundle = parseBundle({ - leaf: { hash: LEAF1, event_type: "PROGRAM_ENTRY" }, - proof: [{ hash: LEAF2, position: "right" }], - root: { hash: ROOT }, - anchor: { txid: "abc123", height: 100 }, +await test("legacy rejects root-as-leaf and requires a count", async () => { + assert.equal( + await verifyProof( + { + leaf_hash: RAW_ROOT, + proof: [], + root: { + hash: RAW_ROOT, + leaf_count: 2, + scheme: LEGACY_SCHEME, + }, + anchor: { height: 3286631 }, + protocol: "ZAP1", + version: "2", + }, + { allowHistoricalLegacy: true }, + ), + false, + ); + + const missingCount = structuredClone(legacyBundle); + delete missingCount.root.leaf_count; + assert.equal( + await verifyProof(missingCount, { allowHistoricalLegacy: true }), + false, + ); +}); + +await test("legacy accepts exact duplicate-odd three- and five-leaf paths", async () => { + const leaf3 = "03".repeat(32); + const pair12 = await nodeHash(LEAF1, LEAF2); + const duplicate3 = await nodeHash(leaf3, leaf3); + const raw3 = await nodeHash(pair12, duplicate3); + const legacyBase = { + anchor: { height: 3286631 }, protocol: "ZAP1", version: "2", - }); - assert.strictEqual(bundle.leaf_hash, LEAF1); - assert.strictEqual(bundle.root, ROOT); - assert.strictEqual(bundle.anchor.txid, "abc123"); - assert.strictEqual(bundle.leaf.hash, LEAF1); - assert.strictEqual(bundle.version, "2"); -}); - -// parseBundle - JSON string -await test("parseBundle handles JSON string input", () => { - const json = JSON.stringify({ - leaf_hash: LEAF1, - proof: [], - root: ROOT, - }); - const bundle = parseBundle(json); - assert.strictEqual(bundle.leaf_hash, LEAF1); - assert.strictEqual(bundle.root, ROOT); -}); - -// parseBundle - defaults -await test("parseBundle defaults for missing fields", () => { - const bundle = parseBundle({ leaf_hash: LEAF1, proof: [], root: ROOT }); - assert.strictEqual(bundle.anchor, null); - assert.strictEqual(bundle.leaf, null); - assert.strictEqual(bundle.protocol, "ZAP1"); - assert.strictEqual(bundle.version, "1"); -}); - -// Summary + }; + + assert.equal( + await verifyProof( + { + ...legacyBase, + leaf_hash: leaf3, + proof: [ + { hash: leaf3, position: "right" }, + { hash: pair12, position: "left" }, + ], + root: { + hash: raw3, + leaf_count: 3, + scheme: LEGACY_SCHEME, + }, + }, + { allowHistoricalLegacy: true }, + ), + true, + ); + + const wrongDuplicate = await nodeHash(leaf3, LEAF1); + const craftedWrongRoot = await nodeHash(pair12, wrongDuplicate); + assert.equal( + await verifyProof( + { + ...legacyBase, + leaf_hash: leaf3, + proof: [ + { hash: LEAF1, position: "right" }, + { hash: pair12, position: "left" }, + ], + root: { + hash: craftedWrongRoot, + leaf_count: 3, + scheme: LEGACY_SCHEME, + }, + }, + { allowHistoricalLegacy: true }, + ), + false, + ); + + const leaf4 = "04".repeat(32); + const leaf5 = "05".repeat(32); + const pair34 = await nodeHash(leaf3, leaf4); + const duplicate5 = await nodeHash(leaf5, leaf5); + const firstFour = await nodeHash(pair12, pair34); + const duplicate5Again = await nodeHash(duplicate5, duplicate5); + const raw5 = await nodeHash(firstFour, duplicate5Again); + assert.equal( + await verifyProof( + { + ...legacyBase, + leaf_hash: leaf5, + proof: [ + { hash: leaf5, position: "right" }, + { hash: duplicate5, position: "right" }, + { hash: firstFour, position: "left" }, + ], + root: { + hash: raw5, + leaf_count: 5, + scheme: LEGACY_SCHEME, + }, + }, + { allowHistoricalLegacy: true }, + ), + true, + ); +}); + +await test("empty proof requires exactly one leaf", async () => { + const oneRoot = commitRoot(LEAF1, 1); + assert.equal( + await verifyProof({ + leaf_hash: LEAF1, + proof: [], + root: { + hash: oneRoot, + leaf_count: 1, + scheme: COUNT_BOUND_SCHEME, + }, + }), + true, + ); + assert.equal( + await verifyProof({ + leaf_hash: LEAF1, + proof: [], + root: { + hash: commitRoot(LEAF1, 2), + leaf_count: 2, + scheme: COUNT_BOUND_SCHEME, + }, + }), + false, + ); +}); + +await test("count-bound paths reject impossible shapes", async () => { + const impossibleRawRoot = await nodeHash(LEAF1, LEAF2); + assert.equal( + await verifyProof({ + leaf_hash: LEAF1, + proof: [{ hash: LEAF2, position: "right" }], + root: { + hash: commitRoot(impossibleRawRoot, 1), + leaf_count: 1, + scheme: COUNT_BOUND_SCHEME, + }, + protocol: "ZAP1", + version: "2", + }), + false, + ); +}); + +await test("carry-up paths accept valid three- and five-leaf shapes", async () => { + const leaf3 = "03".repeat(32); + const pair12 = await nodeHash(LEAF1, LEAF2); + const raw3 = await nodeHash(pair12, leaf3); + const root3 = commitRoot(raw3, 3); + + assert.equal( + await verifyProof({ + leaf_hash: leaf3, + proof: [{ hash: pair12, position: "left" }], + root: { + hash: root3, + leaf_count: 3, + scheme: COUNT_BOUND_SCHEME, + }, + }), + true, + ); + assert.equal( + await verifyProof({ + leaf_hash: LEAF1, + proof: [ + { hash: LEAF2, position: "right" }, + { hash: leaf3, position: "right" }, + ], + root: { + hash: root3, + leaf_count: 3, + scheme: COUNT_BOUND_SCHEME, + }, + }), + true, + ); + + const leaf4 = "04".repeat(32); + const leaf5 = "05".repeat(32); + const pair34 = await nodeHash(leaf3, leaf4); + const firstFour = await nodeHash(pair12, pair34); + const raw5 = await nodeHash(firstFour, leaf5); + const root5 = commitRoot(raw5, 5); + + assert.equal( + await verifyProof({ + leaf_hash: leaf5, + proof: [{ hash: firstFour, position: "left" }], + root: { + hash: root5, + leaf_count: 5, + scheme: COUNT_BOUND_SCHEME, + }, + }), + true, + ); + assert.equal( + await verifyProof({ + leaf_hash: leaf3, + proof: [ + { hash: leaf4, position: "right" }, + { hash: pair12, position: "left" }, + { hash: leaf5, position: "right" }, + ], + root: { + hash: root5, + leaf_count: 5, + scheme: COUNT_BOUND_SCHEME, + }, + }), + true, + ); +}); + +await test("explicit protocol and version must match the root scheme", async () => { + const badProtocol = structuredClone(currentBundle); + badProtocol.protocol = "NOT_ZAP1"; + assert.equal(await verifyProof(badProtocol), false); + + const badCurrentVersion = structuredClone(currentBundle); + badCurrentVersion.version = "999"; + assert.equal(await verifyProof(badCurrentVersion), false); + + const explicitLegacy = structuredClone(legacyBundle); + explicitLegacy.protocol = "ZAP1"; + explicitLegacy.version = "2"; + assert.equal( + await verifyProof(explicitLegacy, { allowHistoricalLegacy: true }), + true, + ); + explicitLegacy.version = "1"; + assert.equal( + await verifyProof(explicitLegacy, { allowHistoricalLegacy: true }), + false, + ); +}); + +await test("duplicate bundle representations must agree", async () => { + const agreeing = structuredClone(currentBundle); + agreeing.leaf_hash = LEAF2; + agreeing.root_hash = V2_ROOT; + agreeing.leaf_count = 2; + agreeing.root_scheme = COUNT_BOUND_SCHEME; + assert.equal(await verifyProof(agreeing), true); + + const leafConflict = structuredClone(agreeing); + leafConflict.leaf_hash = LEAF1; + await assert.rejects(() => verifyProof(leafConflict), TypeError); + + const rootConflict = structuredClone(agreeing); + rootConflict.root_hash = LEAF1; + await assert.rejects(() => verifyProof(rootConflict), TypeError); + + const countConflict = structuredClone(agreeing); + countConflict.leaf_count = 3; + await assert.rejects(() => verifyProof(countConflict), TypeError); + + const schemeConflict = structuredClone(agreeing); + schemeConflict.root_scheme = LEGACY_SCHEME; + await assert.rejects(() => verifyProof(schemeConflict), TypeError); +}); + +await test("malformed hashes and positions reject", async () => { + const badHex = structuredClone(currentBundle); + badHex.leaf.hash = "gg".repeat(32); + await assert.rejects(() => verifyProof(badHex), TypeError); + + const badPosition = structuredClone(currentBundle); + badPosition.proof[0].position = "RIGHT"; + await assert.rejects(() => verifyProof(badPosition), TypeError); +}); + +await test("proof arrays are bounded and hole-free", async () => { + const long = structuredClone(currentBundle); + long.proof = Array.from({ length: 65 }, () => ({ + hash: LEAF1, + position: "left", + })); + await assert.rejects(() => verifyProof(long), RangeError); + + const hole = structuredClone(currentBundle); + hole.proof = new Array(1); + await assert.rejects(() => verifyProof(hole), TypeError); +}); + +await test("leaf count rejects coercion and u64 wraparound", () => { + assert.throws(() => commitRoot(RAW_ROOT, "2"), TypeError); + assert.throws(() => commitRoot(RAW_ROOT, false), TypeError); + assert.throws(() => commitRoot(RAW_ROOT, Number.NaN), TypeError); + assert.throws(() => commitRoot(RAW_ROOT, 1.5), TypeError); + assert.throws(() => commitRoot(RAW_ROOT, 0), RangeError); + assert.throws( + () => commitRoot(RAW_ROOT, 0x10000000000000000n), + RangeError, + ); +}); + console.log(`\n${passed} passed, ${failed} failed`); if (failed > 0) process.exit(1); diff --git a/wasm/zap1_verify_wasm.js b/wasm/zap1_verify_wasm.js deleted file mode 100644 index a129afc..0000000 --- a/wasm/zap1_verify_wasm.js +++ /dev/null @@ -1,526 +0,0 @@ -/* @ts-self-types="./zap1_verify_wasm.d.ts" */ - -/** - * Compute the leaf hash for an OWNERSHIP_ATTEST event. - * @param {string} wallet_hash - * @param {string} serial_number - * @returns {string} - */ -export function computeOwnershipAttest(wallet_hash, serial_number) { - let deferred3_0; - let deferred3_1; - try { - const ptr0 = passStringToWasm0(wallet_hash, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(serial_number, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ret = wasm.computeOwnershipAttest(ptr0, len0, ptr1, len1); - deferred3_0 = ret[0]; - deferred3_1 = ret[1]; - return getStringFromWasm0(ret[0], ret[1]); - } finally { - wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); - } -} - -/** - * Compute the leaf hash for a PROGRAM_ENTRY event. - * @param {string} wallet_hash - * @returns {string} - */ -export function computeProgramEntry(wallet_hash) { - let deferred2_0; - let deferred2_1; - try { - const ptr0 = passStringToWasm0(wallet_hash, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ret = wasm.computeProgramEntry(ptr0, len0); - deferred2_0 = ret[0]; - deferred2_1 = ret[1]; - return getStringFromWasm0(ret[0], ret[1]); - } finally { - wasm.__wbindgen_free(deferred2_0, deferred2_1, 1); - } -} - -/** - * Compute a Merkle node hash: BLAKE2b-256("NordicShield_MRK", left || right). - * @param {string} left_hex - * @param {string} right_hex - * @returns {string} - */ -export function nodeHash(left_hex, right_hex) { - let deferred4_0; - let deferred4_1; - try { - const ptr0 = passStringToWasm0(left_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len0 = WASM_VECTOR_LEN; - const ptr1 = passStringToWasm0(right_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - const ret = wasm.nodeHash(ptr0, len0, ptr1, len1); - var ptr3 = ret[0]; - var len3 = ret[1]; - if (ret[3]) { - ptr3 = 0; len3 = 0; - throw takeFromExternrefTable0(ret[2]); - } - deferred4_0 = ptr3; - deferred4_1 = len3; - return getStringFromWasm0(ptr3, len3); - } finally { - wasm.__wbindgen_free(deferred4_0, deferred4_1, 1); - } -} - -/** - * Verify a Merkle proof from JS. - * - * Accepts a proof bundle as a JS object: - * ```js - * { leaf_hash: "abcd...", proof: [{hash: "...", position: "left|right"}, ...], root: "ef01..." } - * ``` - * @param {any} bundle - * @returns {boolean} - */ -export function verifyProof(bundle) { - const ret = wasm.verifyProof(bundle); - if (ret[2]) { - throw takeFromExternrefTable0(ret[1]); - } - return ret[0] !== 0; -} - -function __wbg_get_imports() { - const import0 = { - __proto__: null, - __wbg_Error_55538483de6e3abe: function(arg0, arg1) { - const ret = Error(getStringFromWasm0(arg0, arg1)); - return ret; - }, - __wbg_String_8564e559799eccda: function(arg0, arg1) { - const ret = String(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg___wbindgen_boolean_get_fe2a24fdfdb4064f: function(arg0) { - const v = arg0; - const ret = typeof(v) === 'boolean' ? v : undefined; - return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0; - }, - __wbg___wbindgen_debug_string_d89627202d0155b7: function(arg0, arg1) { - const ret = debugString(arg1); - const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - const len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg___wbindgen_in_fe3eb6a509f75744: function(arg0, arg1) { - const ret = arg0 in arg1; - return ret; - }, - __wbg___wbindgen_is_function_2a95406423ea8626: function(arg0) { - const ret = typeof(arg0) === 'function'; - return ret; - }, - __wbg___wbindgen_is_object_59a002e76b059312: function(arg0) { - const val = arg0; - const ret = typeof(val) === 'object' && val !== null; - return ret; - }, - __wbg___wbindgen_is_undefined_87a3a837f331fef5: function(arg0) { - const ret = arg0 === undefined; - return ret; - }, - __wbg___wbindgen_jsval_loose_eq_cf851f110c48f9ba: function(arg0, arg1) { - const ret = arg0 == arg1; - return ret; - }, - __wbg___wbindgen_number_get_769f3676dc20c1d7: function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'number' ? obj : undefined; - getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true); - }, - __wbg___wbindgen_string_get_f1161390414f9b59: function(arg0, arg1) { - const obj = arg1; - const ret = typeof(obj) === 'string' ? obj : undefined; - var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); - var len1 = WASM_VECTOR_LEN; - getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true); - getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true); - }, - __wbg___wbindgen_throw_5549492daedad139: function(arg0, arg1) { - throw new Error(getStringFromWasm0(arg0, arg1)); - }, - __wbg_call_6ae20895a60069a2: function() { return handleError(function (arg0, arg1) { - const ret = arg0.call(arg1); - return ret; - }, arguments); }, - __wbg_done_19f92cb1f8738aba: function(arg0) { - const ret = arg0.done; - return ret; - }, - __wbg_get_a50328e7325d7f9b: function() { return handleError(function (arg0, arg1) { - const ret = Reflect.get(arg0, arg1); - return ret; - }, arguments); }, - __wbg_get_unchecked_7c6bbabf5b0b1fbf: function(arg0, arg1) { - const ret = arg0[arg1 >>> 0]; - return ret; - }, - __wbg_get_with_ref_key_6412cf3094599694: function(arg0, arg1) { - const ret = arg0[arg1]; - return ret; - }, - __wbg_instanceof_ArrayBuffer_8d855993947fc3a2: function(arg0) { - let result; - try { - result = arg0 instanceof ArrayBuffer; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }, - __wbg_instanceof_Uint8Array_ce24d58a5f4bdcc3: function(arg0) { - let result; - try { - result = arg0 instanceof Uint8Array; - } catch (_) { - result = false; - } - const ret = result; - return ret; - }, - __wbg_isArray_867202cf8f195ed8: function(arg0) { - const ret = Array.isArray(arg0); - return ret; - }, - __wbg_iterator_54661826e186eb6a: function() { - const ret = Symbol.iterator; - return ret; - }, - __wbg_length_e6e1633fbea6cfa9: function(arg0) { - const ret = arg0.length; - return ret; - }, - __wbg_length_fae3e439140f48a4: function(arg0) { - const ret = arg0.length; - return ret; - }, - __wbg_new_1d96678aaacca32e: function(arg0) { - const ret = new Uint8Array(arg0); - return ret; - }, - __wbg_next_55d835fe0ab5b3e7: function(arg0) { - const ret = arg0.next; - return ret; - }, - __wbg_next_e34cfb9df1518d7c: function() { return handleError(function (arg0) { - const ret = arg0.next(); - return ret; - }, arguments); }, - __wbg_prototypesetcall_3875d54d12ef2eec: function(arg0, arg1, arg2) { - Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2); - }, - __wbg_value_d5b248ce8419bd1b: function(arg0) { - const ret = arg0.value; - return ret; - }, - __wbindgen_cast_0000000000000001: function(arg0, arg1) { - // Cast intrinsic for `Ref(String) -> Externref`. - const ret = getStringFromWasm0(arg0, arg1); - return ret; - }, - __wbindgen_init_externref_table: function() { - const table = wasm.__wbindgen_externrefs; - const offset = table.grow(4); - table.set(0, undefined); - table.set(offset + 0, undefined); - table.set(offset + 1, null); - table.set(offset + 2, true); - table.set(offset + 3, false); - }, - }; - return { - __proto__: null, - "./zap1_verify_wasm_bg.js": import0, - }; -} - -function addToExternrefTable0(obj) { - const idx = wasm.__externref_table_alloc(); - wasm.__wbindgen_externrefs.set(idx, obj); - return idx; -} - -function debugString(val) { - // primitive types - const type = typeof val; - if (type == 'number' || type == 'boolean' || val == null) { - return `${val}`; - } - if (type == 'string') { - return `"${val}"`; - } - if (type == 'symbol') { - const description = val.description; - if (description == null) { - return 'Symbol'; - } else { - return `Symbol(${description})`; - } - } - if (type == 'function') { - const name = val.name; - if (typeof name == 'string' && name.length > 0) { - return `Function(${name})`; - } else { - return 'Function'; - } - } - // objects - if (Array.isArray(val)) { - const length = val.length; - let debug = '['; - if (length > 0) { - debug += debugString(val[0]); - } - for(let i = 1; i < length; i++) { - debug += ', ' + debugString(val[i]); - } - debug += ']'; - return debug; - } - // Test for built-in - const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val)); - let className; - if (builtInMatches && builtInMatches.length > 1) { - className = builtInMatches[1]; - } else { - // Failed to match the standard '[object ClassName]' - return toString.call(val); - } - if (className == 'Object') { - // we're a user defined class or Object - // JSON.stringify avoids problems with cycles, and is generally much - // easier than looping through ownProperties of `val`. - try { - return 'Object(' + JSON.stringify(val) + ')'; - } catch (_) { - return 'Object'; - } - } - // errors - if (val instanceof Error) { - return `${val.name}: ${val.message}\n${val.stack}`; - } - // TODO we could test for more things here, like `Set`s and `Map`s. - return className; -} - -function getArrayU8FromWasm0(ptr, len) { - ptr = ptr >>> 0; - return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); -} - -let cachedDataViewMemory0 = null; -function getDataViewMemory0() { - if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) { - cachedDataViewMemory0 = new DataView(wasm.memory.buffer); - } - return cachedDataViewMemory0; -} - -function getStringFromWasm0(ptr, len) { - ptr = ptr >>> 0; - return decodeText(ptr, len); -} - -let cachedUint8ArrayMemory0 = null; -function getUint8ArrayMemory0() { - if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { - cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); - } - return cachedUint8ArrayMemory0; -} - -function handleError(f, args) { - try { - return f.apply(this, args); - } catch (e) { - const idx = addToExternrefTable0(e); - wasm.__wbindgen_exn_store(idx); - } -} - -function isLikeNone(x) { - return x === undefined || x === null; -} - -function passStringToWasm0(arg, malloc, realloc) { - if (realloc === undefined) { - const buf = cachedTextEncoder.encode(arg); - const ptr = malloc(buf.length, 1) >>> 0; - getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); - WASM_VECTOR_LEN = buf.length; - return ptr; - } - - let len = arg.length; - let ptr = malloc(len, 1) >>> 0; - - const mem = getUint8ArrayMemory0(); - - let offset = 0; - - for (; offset < len; offset++) { - const code = arg.charCodeAt(offset); - if (code > 0x7F) break; - mem[ptr + offset] = code; - } - if (offset !== len) { - if (offset !== 0) { - arg = arg.slice(offset); - } - ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; - const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); - const ret = cachedTextEncoder.encodeInto(arg, view); - - offset += ret.written; - ptr = realloc(ptr, len, offset, 1) >>> 0; - } - - WASM_VECTOR_LEN = offset; - return ptr; -} - -function takeFromExternrefTable0(idx) { - const value = wasm.__wbindgen_externrefs.get(idx); - wasm.__externref_table_dealloc(idx); - return value; -} - -let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); -cachedTextDecoder.decode(); -const MAX_SAFARI_DECODE_BYTES = 2146435072; -let numBytesDecoded = 0; -function decodeText(ptr, len) { - numBytesDecoded += len; - if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) { - cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - cachedTextDecoder.decode(); - numBytesDecoded = len; - } - return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); -} - -const cachedTextEncoder = new TextEncoder(); - -if (!('encodeInto' in cachedTextEncoder)) { - cachedTextEncoder.encodeInto = function (arg, view) { - const buf = cachedTextEncoder.encode(arg); - view.set(buf); - return { - read: arg.length, - written: buf.length - }; - }; -} - -let WASM_VECTOR_LEN = 0; - -let wasmModule, wasm; -function __wbg_finalize_init(instance, module) { - wasm = instance.exports; - wasmModule = module; - cachedDataViewMemory0 = null; - cachedUint8ArrayMemory0 = null; - wasm.__wbindgen_start(); - return wasm; -} - -async function __wbg_load(module, imports) { - if (typeof Response === 'function' && module instanceof Response) { - if (typeof WebAssembly.instantiateStreaming === 'function') { - try { - return await WebAssembly.instantiateStreaming(module, imports); - } catch (e) { - const validResponse = module.ok && expectedResponseType(module.type); - - if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') { - console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); - - } else { throw e; } - } - } - - const bytes = await module.arrayBuffer(); - return await WebAssembly.instantiate(bytes, imports); - } else { - const instance = await WebAssembly.instantiate(module, imports); - - if (instance instanceof WebAssembly.Instance) { - return { instance, module }; - } else { - return instance; - } - } - - function expectedResponseType(type) { - switch (type) { - case 'basic': case 'cors': case 'default': return true; - } - return false; - } -} - -function initSync(module) { - if (wasm !== undefined) return wasm; - - - if (module !== undefined) { - if (Object.getPrototypeOf(module) === Object.prototype) { - ({module} = module) - } else { - console.warn('using deprecated parameters for `initSync()`; pass a single object instead') - } - } - - const imports = __wbg_get_imports(); - if (!(module instanceof WebAssembly.Module)) { - module = new WebAssembly.Module(module); - } - const instance = new WebAssembly.Instance(module, imports); - return __wbg_finalize_init(instance, module); -} - -async function __wbg_init(module_or_path) { - if (wasm !== undefined) return wasm; - - - if (module_or_path !== undefined) { - if (Object.getPrototypeOf(module_or_path) === Object.prototype) { - ({module_or_path} = module_or_path) - } else { - console.warn('using deprecated parameters for the initialization function; pass a single object instead') - } - } - - if (module_or_path === undefined) { - module_or_path = new URL('zap1_verify_wasm_bg.wasm', import.meta.url); - } - const imports = __wbg_get_imports(); - - if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { - module_or_path = fetch(module_or_path); - } - - const { instance, module } = await __wbg_load(await module_or_path, imports); - - return __wbg_finalize_init(instance, module); -} - -export { initSync, __wbg_init as default }; diff --git a/wasm/zap1_verify_wasm_bg.wasm b/wasm/zap1_verify_wasm_bg.wasm deleted file mode 100644 index 32f10d6808bcfb7b0cca2362d5295a4420e1e90c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 83444 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