Signature-Quorum Oracles for AI and Off-Chain Data

Part 4 of “Oracles in Practice” — a hands-on series for smart-contract engineers.

Some facts have no market to average and no public feed to read: a model’s risk score, a proof-of-reserves attestation, the output of an off-chain computation. For these you fall back to the oldest trick in distributed systems — make several independent parties attest, and require a threshold to agree. This is a signature quorum, and it’s increasingly how AI outputs are anchored on-chain.

Crucially, be honest about what it proves. A quorum proves these authorized signers attested to this value, produced by this model, over these inputs. It does not prove the model was correct. It’s the cheapest verification layer — identity and agreement — and you stack TEE attestation or ZK-inference proofs on top when you need to prove the computation itself.

The report and its EIP-712 type

Sign structured data, not a bare hash — EIP-712 gives signers (and hardware wallets) a legible payload and binds the signature to your contract and chain. The report carries everything a consumer needs to reason about provenance:

struct AIReport {
    bytes32 modelId;    // which model produced the value
    bytes32 inputHash;  // hash of the exact inputs fed to it
    int256  value;      // the output (risk score, price, classification...)
    uint64  confidence; // scaled 0..1e6
    uint64  timestamp;  // when inference ran, off-chain
    uint64  roundId;    // strictly increasing; replay protection
}

bytes32 private constant REPORT_TYPEHASH = keccak256(
    "AIReport(bytes32 modelId,bytes32 inputHash,int256 value,uint64 confidence,uint64 timestamp,uint64 roundId)"
);

The typehash string must match your struct byte-for-byte, or off-chain and on-chain digests diverge and every signature silently fails to recover the expected signer. This is the single most common integration bug here — worth a dedicated test asserting the contract’s _hashTypedDataV4 matches ethers’ TypedDataEncoder.hash.

The verification loop, and a free lunch

Recover each signature against the digest and count valid, authorized signers. The elegant part is how you prevent a single signer from being counted twice or a caller from padding duplicates:

bytes32 digest = _hashTypedDataV4(_hashReport(report));

address last = address(0);
uint256 valid = 0;
for (uint256 i = 0; i < signatures.length; i++) {
    address signer = ECDSA.recover(digest, signatures[i]);
    if (signer <= last) revert SignaturesUnsortedOrDuplicate();
    if (!isSigner[signer]) revert UnauthorizedSigner(signer);
    last = signer;
    unchecked { valid++; }
}
if (valid < threshold) revert QuorumNotMet();

Requiring strictly ascending recovered addresses does double duty in one cheap pass: it rejects duplicates (a repeated signer can’t be greater than the previous) and removes any need for a seen-address set in storage or memory. The cost is pushed off-chain: the relayer must sort signatures by recovered signer before submitting. That’s exactly the right place for the cost to live.

The off-chain side, in ethers:

const signed = await Promise.all(publishers.map((p) => signOne(p, report)));
signed.sort((a, b) => (a.signer.toLowerCase() < b.signer.toLowerCase() ? -1 : 1));
return signed.map((s) => s.signature);

The guards around the quorum

A valid quorum on a stale or replayed report is still an exploit. The full submitReport wraps the loop in the same fail-closed discipline as the other oracles:

  • Replay: roundId must strictly exceed the last accepted round. An old, perfectly-signed report can’t be resubmitted.
  • Freshness: reject a future timestamp, and reject one older than maxStaleness.
  • Circuit breaker: once there’s a baseline, bound the per-update deviation (maxDeviationBps) so a single bad quorum can’t move the value arbitrarily.
  • Signer governance: owner-gated addSigner/removeSigner, with setThreshold guarded so the threshold can never exceed the signer count.

I verified the full contract on a local EVM: a valid 2-of-3 quorum commits; below-threshold, unauthorized, and duplicated-signer submissions revert; non-increasing rounds, future and stale timestamps revert; and a +400% jump trips the breaker while a +15% move passes.

Why this is the natural home for AI outputs

Return to the honest framing. When a contract reads an AI oracle value, it’s asking a security question with several parts: who produced this, what inputs, what model, was the computation actually run, and how much damage if it’s wrong? A signature quorum answers the first three cleanly — identity, inputs, model — and, via the deviation breaker, bounds the last. It does not answer “was the computation actually run correctly,” and no amount of signing will. That’s the boundary where you reach for the next layer:

  • TEE attestation proves authorized, unmodified code ran in a sealed environment.
  • ZK-inference proofs prove a specific model produced this output for these inputs — expensive today, but the strongest guarantee and the direction the field is heading.

Layer them deliberately. Signatures give you a cheap, legible identity-and-agreement floor that stops single-server compromise; TEE/ZK sit on top when the stakes justify proving the computation itself. Most protocols should start at the signature layer, tune the quorum and thresholds conservatively, and add heavier proofs only where the value at risk demands it.

That closes the series. Three sourcing patterns — aggregated push feeds, on-chain TWAPs, and signature quorums — none of which removes trust, all of which shatter it across enough independent points that betraying the system stops being profitable. The engineering is in choosing the right pattern for your data, and validating every value as if it were written by your adversary. Because sometimes it is.


Tags: Solidity, AI, Cryptography, SmartContracts, Web3

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *