import { readFileSync } from "node:fs"; import { strictEqual, ok } from "node:assert"; import { describe, it } from "node:test"; import { parseNodeSection as bundleParseNodeSection, parseBundle, parseManifest } from "../src/bundle.js"; import { verifyNodeHashes, verifyClosure, verifyRootClosure, deserializePayload, computeNodeHash, parseNodeSection, } from "../src/merkle.js"; describe("merkle — deserializePayload", () => { it("Leaf (0x00)", () => { const result = deserializePayload(Buffer.from([0x00])); strictEqual(result.type, "leaf"); }); it("Stem (0x01 + 32 bytes)", () => { const childHash = Buffer.alloc(32, 0xab); const payload = Buffer.concat([Buffer.from([0x01]), childHash]); const result = deserializePayload(payload); strictEqual(result.type, "stem"); strictEqual(result.childHash, "ab".repeat(32)); }); it("Fork (0x02 + 64 bytes)", () => { const left = Buffer.alloc(32, 0x01); const right = Buffer.alloc(32, 0x02); const payload = Buffer.concat([Buffer.from([0x02]), left, right]); const result = deserializePayload(payload); strictEqual(result.type, "fork"); strictEqual(result.leftHash, "01".repeat(32)); strictEqual(result.rightHash, "02".repeat(32)); }); it("Leaf with extra bytes fails", () => { throws(() => deserializePayload(Buffer.from([0x00, 0x00])), /invalid leaf/); }); it("Unknown type fails", () => { throws(() => deserializePayload(Buffer.from([0xff])), /unknown type/); }); }); describe("merkle — computeNodeHash", () => { it("Leaf hash is correct length", () => { const leaf = { type: "leaf" }; const hash = computeNodeHash(leaf); strictEqual(hash.length, 64); }); it("Leaf hash matches expected Arboricx domain", () => { const leaf = { type: "leaf" }; const hash = computeNodeHash(leaf); strictEqual(hash, "92b8a9796dbeafbcd36757535876256392170d137bf36b319d77f11a37112158"); }); }); describe("merkle — node section parsing", () => { const fixtureDir = "../../test/fixtures"; it("parses id.arboricx with correct node count", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/id.arboricx`) ); const { nodeMap } = parseNodeSection(data); strictEqual(nodeMap.size, 4); }); it("parses true.arboricx with correct node count", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/true.arboricx`) ); const { nodeMap } = parseNodeSection(data); strictEqual(nodeMap.size, 2); }); it("parses false.arboricx with correct node count", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/false.arboricx`) ); const { nodeMap } = parseNodeSection(data); strictEqual(nodeMap.size, 1); }); }); describe("merkle — hash verification", () => { const fixtureDir = "../../test/fixtures"; it("id.arboricx nodes all verify", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/id.arboricx`) ); const { nodeMap } = parseNodeSection(data); const { verified, mismatches } = verifyNodeHashes(nodeMap); ok(verified, "id.arboricx node hashes should verify"); strictEqual(mismatches.length, 0); }); it("true.arboricx nodes all verify", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/true.arboricx`) ); const { nodeMap } = parseNodeSection(data); const { verified, mismatches } = verifyNodeHashes(nodeMap); ok(verified, "true.arboricx node hashes should verify"); strictEqual(mismatches.length, 0); }); it("corrupted node payload fails hash verification", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/id.arboricx`) ); const { nodeMap } = parseNodeSection(data); // Find a stem node to corrupt let stemKey = null; for (const [key, node] of nodeMap) { if (node.type === "stem") { stemKey = key; break; } } ok(stemKey, "should find a stem node to corrupt"); const stem = nodeMap.get(stemKey); // Corrupt the child hash so serializeNode produces a different payload const corrupted = { ...stem, childHash: "00".repeat(32), payload: Buffer.concat([Buffer.from([0x01]), Buffer.alloc(32, 0x00)]), }; nodeMap.set(stemKey, corrupted); const { verified, mismatches } = verifyNodeHashes(nodeMap); ok(!verified, "corrupted stem should fail hash verification"); ok(mismatches.length > 0, "should have mismatches"); }); }); describe("merkle — closure verification", () => { const fixtureDir = "../../test/fixtures"; it("id.arboricx has complete closure", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/id.arboricx`) ); const { nodeMap } = parseNodeSection(data); const { complete, missing } = verifyClosure(nodeMap); ok(complete, "id.arboricx should have complete closure"); strictEqual(missing.length, 0); }); it("verifyRootClosure checks transitive reachability", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/id.arboricx`) ); const { nodeMap } = parseNodeSection(data); // Use the actual root hash from the fixture's manifest const manifest = parseManifest(readFileSync(`${fixtureDir}/id.arboricx`)); const rootHash = manifest.exports[0].root; const { complete, missingRoots } = verifyRootClosure(nodeMap, rootHash); ok(complete, "root should be reachable"); strictEqual(missingRoots.length, 0); }); it("parseNodeSection returns correct node count", () => { const data = bundleParseNodeSection( readFileSync(`${fixtureDir}/id.arboricx`) ); const result = parseNodeSection(data); strictEqual(result.count, 4); }); }); // Helper for throws function throws(fn, expected) { try { fn(); return false; } catch (e) { return expected.test(e.message); } }