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 65fb4b7..0000000 Binary files a/frontier-compute-zap1-0.1.0.tgz and /dev/null differ 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 32f10d6..0000000 Binary files a/wasm/zap1_verify_wasm_bg.wasm and /dev/null differ