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
- EIP-712 signature — the verdict fields are signed by Sigil's sealer key.
- Merkle leaf — a deterministic hash of the verdict's core fields.
- On-chain anchor — leaves are batched into Merkle roots anchored in
SigilRegistryon 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: ✓ ANCHOREDTamper evidence
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.