Verifying decisions

Check that a published decision record is exactly what the chain acted on, step by step.

Every decision exists in two places: a summary on chain in DecisionLog, and the full record at the location the summary points to. Checking that they match takes five steps.

#The record

{
  "kind": "grade",
  "subject": "214",
  "question": "How well does the result meet the brief? ...",
  "request": { "state": "...", "questions": { "decision": { "type": "score" } } },
  "model": "jev-1.13",
  "answer": "accept",
  "confidence": 0.81,
  "probabilities": { "0": 0.05, "1": 0.14, "2": 0.81 }
}

#Steps

  1. Read the decision. Call DecisionLog.get(id) and read its Recorded event for payloadHash and payloadURI.
  2. Fetch the record from payloadURI.
  3. Check the record. Serialize it canonically, with object keys sorted and no whitespace, and compute keccak256 of the UTF-8 bytes. It must equal payloadHash.
  4. Check the input. Serialize the record's request field the same way. Its keccak256 must equal the on-chain inputHash.
  5. Check the confidence. confidence × 10,000, rounded, must equal the on-chain confidenceBps.
import { keccak256, toBytes } from "viem";

function canonical(value: unknown): string {
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
  if (value && typeof value === "object") {
    return `{${Object.keys(value)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)
      .join(",")}}`;
  }
  return JSON.stringify(value);
}

const ok =
  keccak256(toBytes(canonical(record))) === payloadHash &&
  keccak256(toBytes(canonical(record.request))) === decision.inputHash &&
  Math.round(record.confidence * 10_000) === decision.confidenceBps;

#What this proves

That the published question, request, answer and probabilities are the ones recorded on chain before the decision was applied, and that the confidence the contract compared against its threshold is the one in the record.