From 0c9de1729d884e0427e80d1e8d1461f21c911d5a Mon Sep 17 00:00:00 2001 From: Aziel Eliab Date: Sun, 11 Oct 2026 12:42:35 -0400 Subject: [PATCH] Question seal via SUBMIT text_sha256; restore ledger-form rescore tamper tests --- workers/godlock-uk/factor-detail.test.mjs | 61 ++++++++++++++++++++++- workers/godlock-uk/src/factorDetail.js | 18 ++++++- workers/godlock-uk/src/index.js | 4 +- workers/godlock-uk/src/ui.js | 3 ++ workers/godlock-uk/thread-expand.test.mjs | 2 +- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/workers/godlock-uk/factor-detail.test.mjs b/workers/godlock-uk/factor-detail.test.mjs index 699b977..45e62eb 100644 --- a/workers/godlock-uk/factor-detail.test.mjs +++ b/workers/godlock-uk/factor-detail.test.mjs @@ -1,7 +1,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { scoreDebateText } from "./src/debateScore.js"; -import { factorDetail, storedFactorView, factorDetailSealed, ledgerRescoreLines, reasoningSealed, NOT_STORED, NOT_SEALED_LABEL, FACTOR_KEYS } from "./src/factorDetail.js"; +import { factorDetail, storedFactorView, factorDetailSealed, ledgerRescoreLines, reasoningSealed, challengeSealed, NOT_STORED, NOT_SEALED_LABEL, FACTOR_KEYS } from "./src/factorDetail.js"; import { hashPayload, canonicalJson, ZERO } from "./src/ledger.js"; import { receiptLedgerAndProof, _resetProofCache, HEAD_SQL, TAIL_SQL, BY_RECEIPT_SQL } from "./src/receiptProof.js"; import { threadView, threadExpandScript } from "./src/ui.js"; @@ -227,3 +227,62 @@ describe("reasoning seal", () => { assert.match(v.reasoning.find((x) => x[0] === "Reasoning seal")[1], /not covered/); }); }); + +describe("ledger-form tamper tests", () => { + const d0 = mkDetail({ spre: 0.1 }); + const d1 = mkDetail({ spre: 0.2 }); + const H = (d1stored) => [ + { id: "i", reason: "initial", triad_json: JSON.stringify({ factor_detail: d0 }) }, + { id: "a", reason: "x", triad_json: JSON.stringify({ factor_detail: d1stored }) }, + ]; + async function run(submitSha, hist) { + _resetProofCache(); + const ledger = ledgerWith([ + ["SUBMIT", { receipt_id: "r1", factor_detail_sha256: submitSha }], + ["RESCORE", { receipt_id: "r1", rescore_id: "a", factor_detail_sha256: hashPayload(d1) }], + ]); + const { rows, proof } = await receiptLedgerAndProof({ DB: fakeDb(ledger) }, "r1"); + assert.equal(proof.status, "verified"); + return { lines: ledgerRescoreLines(hist, rows, proof, "r1"), proof }; + } + it("edited rescore factor_detail, ledger row kept: not covered", async () => { + const { lines, proof } = await run(hashPayload(d0), H(mkDetail({ spre: 0.99 }))); + assert.equal(lines[0].sealed, false); + const v = threadView({ ok: true, proof, receipt: { rescore_factor_changes: lines } }); + assert.match(v.factors.find((x) => x[0].startsWith("Rescore"))[1], /\(stored data, not covered\)$/); + }); + it("previous (SUBMIT) detail unsealed, own detail sealed: not covered", async () => { + const { lines } = await run("0".repeat(64), H(d1)); + assert.equal(lines[0].sealed, false); + }); + it("control: both sealed is covered", async () => { + const { lines } = await run(hashPayload(d0), H(d1)); + assert.equal(lines[0].sealed, true); + }); +}); + +describe("question seal", () => { + const q = "Is the steel class a proof?"; + async function proofFor(textSha) { + _resetProofCache(); + return receiptLedgerAndProof({ DB: fakeDb(ledgerWith([["SUBMIT", { receipt_id: "r1", text_sha256: textSha }]])) }, "r1"); + } + it("covered when sha256(challenge_text) matches the sealed text_sha256", async () => { + const { sha256hex } = await import("./src/ledger.js"); + const { rows, proof } = await proofFor(sha256hex(q)); + assert.equal(challengeSealed(q, rows, proof), true); + const v = threadView({ ok: true, proof, receipt: { summary: "S", challenge_sealed: true } }); + assert.match(v.reasoning.find((x) => x[0] === "Question seal")[1], /covered by the seal/); + }); + it("edited challenge_text: stored data, not covered", async () => { + const { sha256hex } = await import("./src/ledger.js"); + const { rows, proof } = await proofFor(sha256hex(q)); + assert.equal(challengeSealed(q + " edited", rows, proof), false); + assert.equal(challengeSealed(null, rows, proof), false); + const v = threadView({ ok: true, proof, receipt: { summary: "S", challenge_sealed: false } }); + assert.equal(v.reasoning.find((x) => x[0] === "Question seal")[1], "Question text is stored data, not covered by the seal."); + }); + it("unverified proof: not covered", () => { + assert.equal(challengeSealed(q, [], { status: "unverified", entries: [] }), false); + }); +}); diff --git a/workers/godlock-uk/src/factorDetail.js b/workers/godlock-uk/src/factorDetail.js index 26a09aa..c690598 100644 --- a/workers/godlock-uk/src/factorDetail.js +++ b/workers/godlock-uk/src/factorDetail.js @@ -7,7 +7,7 @@ * or ledger hash, so existing hashes are untouched. * Author: Aziel Eliab. */ -import { hashPayload } from "./ledger.js"; +import { hashPayload, sha256hex } from "./ledger.js"; export const FACTOR_DETAIL_SCHEMA = "godlock.factor_detail.v1"; export const FACTOR_KEYS = ["physics", "linguistics", "bayesian", "truth_formula", "clce", "spre"]; @@ -238,3 +238,19 @@ export function reasoningSealed(receiptRow, ledgerRows, proof, hashFn) { try { h = hashFn(receiptRow); } catch { return false; } return h === p.content_sha256; } + +/** + * The question text is covered only when the SUBMIT row is in the verified + * chain and sha256(stored challenge_text) equals its text_sha256. Else false. + */ +export function challengeSealed(challengeText, ledgerRows, proof) { + if (typeof challengeText !== "string" || !proof || proof.status !== "verified") return false; + const sub = (Array.isArray(ledgerRows) ? ledgerRows : []).find((r) => String(r.action) === "SUBMIT"); + if (!sub) return false; + const p = payloadOf(sub); + if (!p || typeof p.text_sha256 !== "string" || p.text_sha256.length !== 64) return false; + const e = (Array.isArray(proof.entries) ? proof.entries : []) + .find((x) => Number(x.sequence) === Number(sub.sequence) && x.entry_hash === sub.entry_hash); + if (!e || !e.hash_ok || !e.in_verified_range || !e.matches_checkpoint) return false; + return sha256hex(challengeText) === p.text_sha256; +} diff --git a/workers/godlock-uk/src/index.js b/workers/godlock-uk/src/index.js index 8f0a6c0..1d23206 100644 --- a/workers/godlock-uk/src/index.js +++ b/workers/godlock-uk/src/index.js @@ -24,7 +24,7 @@ import { donateDoc, DONATE_RAILS, donateQrIdFromPath } from "./donate.js"; import { handleRuntimeRoot, isRuntimeRequest, runtimeCors } from "./runtimeRoot.js"; import { appendLedger, verifyLedger, ledgerEntriesForId, sha256hex, publicEntry, hashPayload } from "./ledger.js"; import { receiptLedgerAndProof } from "./receiptProof.js"; -import { factorDetail, storedFactorView, factorDetailSealed, ledgerRescoreLines, reasoningSealed } from "./factorDetail.js"; +import { factorDetail, storedFactorView, factorDetailSealed, ledgerRescoreLines, reasoningSealed, challengeSealed } from "./factorDetail.js"; import { robotsTxt, sitemapXml, citeDoc, llmsDoc, aiDoc, siteOpenApi, mcpDiscoveryDoc, BANNER, DOWNLOAD, DOWNLOAD_STATS, DOWNLOAD_COUNT, GITHUB, AUTHOR, indexNowKeyBody, INDEXNOW_KEY_PATH, INDEXNOW_WELL_KNOWN_PATH, @@ -1289,7 +1289,7 @@ export default { receipt: (() => { const fv = storedFactorView(row.rescores); fv.rescore_factor_changes = ledgerRescoreLines(row.rescores, ledgerRows, proof, id); - return { ...publicPayload(row), ...fv, factor_detail_sealed: factorDetailSealed(fv.factor_detail, ledgerRows, proof), reasoning_sealed: reasoningSealed(row, ledgerRows, proof, hashReceipt) }; + return { ...publicPayload(row), ...fv, factor_detail_sealed: factorDetailSealed(fv.factor_detail, ledgerRows, proof), reasoning_sealed: reasoningSealed(row, ledgerRows, proof, hashReceipt), challenge_sealed: challengeSealed(row.challenge_text, ledgerRows, proof) }; })(), ledger: entries, proof, diff --git a/workers/godlock-uk/src/ui.js b/workers/godlock-uk/src/ui.js index 44cbb51..a7c76fb 100644 --- a/workers/godlock-uk/src/ui.js +++ b/workers/godlock-uk/src/ui.js @@ -533,6 +533,9 @@ export function threadView(j) { if (typeof r.summary === "string" && r.summary) out.reasoning.push(["Summary", r.summary]); if (typeof r.explanation === "string" && r.explanation) out.reasoning.push(["Reasoning", r.explanation]); if (!out.reasoning.length) out.reasoning.push(["Reasoning", "No reasoning was recorded for this receipt."]); + out.reasoning.push(["Question seal", r.challenge_sealed === true + ? "Question text is covered by the seal: its sha256 matches text_sha256 in the sealed SUBMIT entry." + : "Question text is stored data, not covered by the seal."]); out.reasoning.push(["Reasoning seal", r.reasoning_sealed === true ? "Summary and reasoning are covered by the seal: the receipt row hashes to content_sha256 in the sealed SUBMIT entry." : "Summary and reasoning are stored data. They are not covered by the seal."]); diff --git a/workers/godlock-uk/thread-expand.test.mjs b/workers/godlock-uk/thread-expand.test.mjs index 6b93c8a..1872561 100644 --- a/workers/godlock-uk/thread-expand.test.mjs +++ b/workers/godlock-uk/thread-expand.test.mjs @@ -128,7 +128,7 @@ describe("thread expand view", () => { const v = threadView({ ok: true, receipt: { summary: "S", explanation: "E" }, proof: { status: "verified", receipt_hash: "h1", prev_hash: "h0", seal: "h9", timestamp_utc: "t", verify_url: "/verify" } }); assert.equal(v.status, "verified"); assert.deepEqual(v.reasoning.slice(0, 2), [["Summary", "S"], ["Reasoning", "E"]]); - assert.deepEqual(v.reasoning[2], ["Reasoning seal", "Summary and reasoning are stored data. They are not covered by the seal."]); + assert.deepEqual(v.reasoning.find((x) => x[0] === "Reasoning seal"), ["Reasoning seal", "Summary and reasoning are stored data. They are not covered by the seal."]); const m = Object.fromEntries(v.receipt); assert.equal(m["Seal (verified ledger head)"], "h9"); assert.equal(m.Verify, "/verify"); });