// 🤖 The ledger-stream hash chain (issue #1224): canonical bytes, key // derivation, link verification, and the tamper cases — every rule the // append/verify tools rely on, pinned without any IO. // node test/ledgerchain.test.mjs import assert from "node:assert/strict"; import { canonicalBytes, sha256hex, buildEntry, verifyLink } from "../tools/ledgerchain.mjs"; let failures = 0; function test(name, fn) { try { fn(); console.log("PASS " + name); } catch (e) { console.log("FAIL " + name + " — " + (e && e.message)); failures++; } } test("canonical bytes are key-order independent, deeply", () => { const a = canonicalBytes({ b: 1, a: { d: [{ z: 1, y: 2 }], c: 3 } }); const b = canonicalBytes({ a: { c: 3, d: [{ y: 2, z: 1 }] }, b: 1 }); assert.equal(a.toString(), b.toString()); assert.notEqual(sha256hex(canonicalBytes({ a: 1 })), sha256hex(canonicalBytes({ a: 2 }))); }); test("genesis entry: null prev, key carries compact ts + own sha12", () => { const { entry, bytes, sha256, key } = buildEntry({ ledger: "music", writer: "Paul Campbell", ts: "2026-07-21T09:34:13.000Z", payload: { track: "Isn't She Lovely" }, prev: null, }); assert.equal(entry.prev_key, null); assert.equal(entry.prev_sha256, null); assert.equal(sha256, sha256hex(bytes)); assert.equal(key, "ledgers/music/entries/20260721T093413Z-" + sha256.slice(0, 12) + ".json"); }); test("a two-entry chain round-trips through verifyLink", () => { const g = buildEntry({ ledger: "music", writer: "w", ts: "2026-07-21T09:00:00Z", payload: { n: 1 }, prev: null }); const e2 = buildEntry({ ledger: "music", writer: "w", ts: "2026-07-21T09:05:00Z", payload: { n: 2 }, prev: { key: g.key, sha256: g.sha256 } }); const v2 = verifyLink({ bytes: e2.bytes, key: e2.key, claimedSha: e2.sha256 }); assert.ok(v2.ok, v2.reason); assert.equal(v2.entry.prev_key, g.key); const v1 = verifyLink({ bytes: g.bytes, key: v2.entry.prev_key, claimedSha: v2.entry.prev_sha256 }); assert.ok(v1.ok, v1.reason); assert.equal(v1.entry.prev_key, null, "walk terminates at genesis"); }); test("tampered bytes break the link", () => { const g = buildEntry({ ledger: "music", writer: "w", ts: "2026-07-21T09:00:00Z", payload: { n: 1 }, prev: null }); const tampered = Buffer.from(g.bytes.toString().replace('"n":1', '"n":9')); const v = verifyLink({ bytes: tampered, key: g.key, claimedSha: g.sha256 }); assert.equal(v.ok, false); assert.match(v.reason, /claims/); }); test("an entry moved to a different key is caught", () => { const g = buildEntry({ ledger: "music", writer: "w", ts: "2026-07-21T09:00:00Z", payload: { n: 1 }, prev: null }); const v = verifyLink({ bytes: g.bytes, key: "ledgers/music/entries/20990101T000000Z-" + g.sha256.slice(0, 12) + ".json", claimedSha: g.sha256 }); assert.equal(v.ok, false); assert.match(v.reason, /derives/); }); test("payloads are order-canonical too — same payload, same key", () => { const p1 = buildEntry({ ledger: "l", writer: "w", ts: "2026-07-21T09:00:00Z", payload: { a: 1, b: 2 }, prev: null }); const p2 = buildEntry({ ledger: "l", writer: "w", ts: "2026-07-21T09:00:00Z", payload: { b: 2, a: 1 }, prev: null }); assert.equal(p1.key, p2.key); }); if (failures > 0) { console.error(failures + " failure(s)"); process.exit(1); }