Attestations are live over REST — contracts are not deployed yetRead an attestation
Cleaton

Verify a signature

Recover the attester, check the registry state, and check freshness — off chain in TypeScript or on chain for about 30k gas.

Argued in full in the whitepaper at §7.2.

Verification is a pure function of the payload, the signature and registry state. No oracle call, no external dependency.

On chain#

Registry.sol
function verify(Attestation calldata a, bytes calldata sig)
    external view returns (bool valid, address attester)
{
    bytes32 digest = _hashTypedDataV4(_structHash(a));
    attester = ECDSA.recover(digest, sig);
    valid = registeredAttester[attester]
         && block.timestamp <= a.validUntil
         && bondOf[attester] >= minBond
         && !revoked[digest];
}

About 30k gas — a deliberate target. A deposit guard that costs more than the deposit it protects will not be adopted, so verification had to be cheap enough to sit in the hot path rather than in a periodic job.

Note what the check covers beyond the signature itself:

  • registeredAttester — the key is one the registry knows
  • validUntil — the attestation is not stale
  • bondOf >= minBond — the attester is still economically backed
  • !revoked — the attestation was not withdrawn early

Off chain#

verify.ts
import { verifyTypedData } from "viem";

const domain = {
  name: "Cleaton",
  version: "1",
  chainId: a.chainId,
  verifyingContract: REGISTRY,
} as const;

const types = {
  Attestation: [
    { name: "pool",          type: "address" },
    { name: "chainId",       type: "uint256" },
    { name: "accountingId",  type: "bytes32" },
    { name: "refLiquidity",  type: "uint256" },
    { name: "horizonDays",   type: "uint32"  },
    { name: "confidenceBps", type: "uint16"  },
    { name: "thresholdBps",  type: "uint16"  },
    { name: "observedAt",    type: "uint64"  },
    { name: "validUntil",    type: "uint64"  },
    { name: "modelHash",     type: "bytes32" },
    { name: "featureRoot",   type: "bytes32" },
    { name: "flags",         type: "uint8"   },
    { name: "nonce",         type: "uint64"  },
  ],
} as const;

const recovered = await verifyTypedData({
  address: a.attester, domain, types,
  primaryType: "Attestation", message: a, signature: a.signature,
});

// Freshness is a separate check. A valid signature over a stale attestation
// is still a valid signature.
const fresh = a.validUntil > Math.floor(Date.now() / 1000);

if (!recovered || !fresh) throw new Error("reject");

What verification does not tell you#