181 lines
5.8 KiB
JavaScript
181 lines
5.8 KiB
JavaScript
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);
|
|
}
|
|
}
|