SIGIL

Verifiable receipts

Verify any Sigil verdict yourself — signature, leaf, and on-chain Merkle proof.

Trust comes from verifiable receipts, not from the AI. Every verdict carries a seal you can check without trusting Sigil's servers.

The three layers

  1. EIP-712 signature — the verdict fields are signed by Sigil's sealer key.
  2. Merkle leaf — a deterministic hash of the verdict's core fields.
  3. On-chain anchor — leaves are batched into Merkle roots anchored in SigilRegistry on X Layer; verify(leaf, proof, batchId) is a public view call.

EIP-712 domain and types

domain = { name: "Sigil", version: "1", chainId, verifyingContract }

types.Verdict = [
  { name: "receiptId",     type: "string"  },
  { name: "subjectDigest", type: "bytes32" },
  { name: "verdict",       type: "uint8"   },
  { name: "riskScore",     type: "uint8"   },
  { name: "rubricVersion", type: "string"  },
  { name: "issuedAt",      type: "uint64"  },
  { name: "expiresAt",     type: "uint64"  },
]

Verdicts expire 900 seconds after issue — they are point-in-time judgments.

Leaf encoding (identical in TypeScript and Solidity)

leaf = keccak256(abi.encode(
  keccak256(bytes(receiptId)),  // bytes32
  subjectDigest,                // bytes32
  uint8(verdict),               // 0 PASS / 1 WARN / 2 BLOCK
  uint8(riskScore),             // 0..100
  uint64(issuedAt)              // unix seconds
));

For transactions, subjectDigest = keccak256(abi.encode(uint256 chainId, address from, address to, uint256 value, bytes data)) exactly. For off-chain signatures, Sigil hashes a canonical, key-sorted representation of { primaryType, domain, types, message }, so the receipt binds the permit that was screened rather than an empty synthetic transaction.

The Merkle tree is OpenZeppelin-compatible: commutative sorted-pair keccak256 (lower hex hashed first), odd nodes promoted — so a stock MerkleProof.verify accepts Sigil proofs.

Standalone verification snippet

Only viem required. This exact script lives at scripts/verify-receipt.mjs and is exercised against live receipts:

import { encodeAbiParameters, keccak256, stringToBytes, verifyTypedData,
         createPublicClient, http } from "viem";

const v = await (await fetch(`https://<sigil-host>/verdicts/${receiptId}`)).json();

// 1) signature
const sigOk = await verifyTypedData({
  address: v.seal.signer,
  domain: { name: "Sigil", version: "1", chainId: v.chainId,
            verifyingContract: SIGIL_REGISTRY },
  types: { Verdict: [
    { name: "receiptId", type: "string" }, { name: "subjectDigest", type: "bytes32" },
    { name: "verdict", type: "uint8" },    { name: "riskScore", type: "uint8" },
    { name: "rubricVersion", type: "string" },
    { name: "issuedAt", type: "uint64" },  { name: "expiresAt", type: "uint64" },
  ]},
  primaryType: "Verdict",
  message: { receiptId: v.receiptId, subjectDigest: v.subject.digest,
             verdict: v.verdict, riskScore: v.riskScore, rubricVersion: v.rubricVersion,
             issuedAt: BigInt(v.issuedAt), expiresAt: BigInt(v.expiresAt) },
  signature: v.seal.signature,
});

// 2) leaf
const leaf = keccak256(encodeAbiParameters(
  [{ type: "bytes32" }, { type: "bytes32" }, { type: "uint8" }, { type: "uint8" }, { type: "uint64" }],
  [keccak256(stringToBytes(v.receiptId)), v.subject.digest, v.verdict, v.riskScore, BigInt(v.issuedAt)],
));
const leafOk = leaf === v.seal.leaf;

// 3) on-chain anchor (once the batch containing this leaf is anchored)
const client = createPublicClient({ transport: http("https://rpc.xlayer.tech") });
const anchored = await client.readContract({
  address: SIGIL_REGISTRY,
  abi: [{ name: "verify", type: "function", stateMutability: "view",
          inputs: [{ type: "bytes32" }, { type: "bytes32[]" }, { type: "uint256" }],
          outputs: [{ type: "bool" }] }],
  functionName: "verify",
  args: [leaf, proof, batchId],
});

Run it directly:

node scripts/verify-receipt.mjs https://<sigil-host>/verdicts/sgl_… \
  --registry <SigilRegistry> --rpc https://rpc.xlayer.tech --batch 0 --proof 0x…,0x…

Output on a genuine receipt:

signature recovers to the published sealer: ✓ VALID
recomputed leaf matches seal.leaf:   ✓ VALID
on-chain SigilRegistry.verify:       ✓ ANCHORED

Tamper evidence

The seal covers the fields in types.Verdict: receipt ID, subject digest, verdict, score, rubric version, and timestamps. Changing any of those breaks verification. Findings, simulation detail, and coverage are supporting evidence rather than EIP-712 fields; re-run the deterministic rubric when independently auditing them.