Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion workers/godlock-uk/factor-detail.test.mjs
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
});
});
18 changes: 17 additions & 1 deletion workers/godlock-uk/src/factorDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -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;
}
4 changes: 2 additions & 2 deletions workers/godlock-uk/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions workers/godlock-uk/src/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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."]);
Expand Down
2 changes: 1 addition & 1 deletion workers/godlock-uk/thread-expand.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand Down
Loading