Building dapps

Verifiable Inference

On-chain AI inference is only useful if consumers can trust the results. Our approach to verifiable inference gives developers a choice: Citrate implements three verification tiers that provide different tradeoffs between speed, cost, and cryptographic guarantee strength. Every inference result on the network carries an attestation, and the verification tier determines how that attestation is validated.

Why Verification Matters

In a decentralized network, the model operator executing your inference request is an untrusted third party. Without verification, an operator could return fabricated results, serve a cheaper model than advertised, or tamper with outputs to manipulate downstream contract logic. Verifiable inference closes this trust gap by providing mathematical or economic guarantees that the result is authentic.

The three tiers form a spectrum from fastest/cheapest to slowest/most-secure. Most applications will use Signature verification for routine operations and escalate to Optimistic or ZK-SNARK for high-value decisions.

Tier 1: Signature Verification

Signature verification is the fastest and cheapest tier. The model operator signs the inference output with their registered attestation key, and the consumer verifies the signature on-chain.

How it works:

  1. The operator executes inference and produces output bytes
  2. The operator signs keccak256(requestId || output) with their attestation private key
  3. The signature is included with the fulfilled result
  4. The calling contract (or the InferenceEngine) verifies the signature against the operator's registered public key
// On-chain signature verification
interface IAttestationVerifier {
    function verifySignature(
        bytes32 requestId,
        bytes calldata output,
        bytes calldata signature,
        address attester
    ) external view returns (bool valid);
}
 
// Usage in your contract
contract MyApp {
    IAttestationVerifier constant verifier = IAttestationVerifier(address(0x0104));
 
    function handleResult(
        bytes32 requestId,
        bytes calldata output,
        bytes calldata signature,
        address operator
    ) external {
        require(
            verifier.verifySignature(requestId, output, signature, operator),
            "Invalid attestation"
        );
        // Process the verified output
    }
}

Tradeoffs:

PropertyValue
Latency~0 additional (inline with result)
Gas cost~15,000 gas for verification
Security modelTrust the operator's key; slashing for misbehavior
Best forLow-value queries, high-frequency requests, non-critical outputs

The security relies on economic incentives: operators stake SALT as collateral, and if a signature is proven to attest to an incorrect result, the operator's stake is slashed. This makes cheating economically irrational for operators with meaningful stake.

Tier 2: Optimistic Verification

Optimistic verification adds a challenge window during which any network participant can dispute the result. If no dispute is raised within the window, the result is accepted as valid. If a dispute is raised, the network re-executes the inference and compares outputs.

How it works:

  1. The operator executes inference and submits the result with a signature attestation
  2. The result enters a challenge window (configurable, default 10 blocks / ~10 seconds)
  3. During the window, any staked challenger can submit a dispute by calling dispute(requestId)
  4. If disputed, the network selects three independent operators to re-execute the inference
  5. If the majority of re-executions disagree with the original result, the original operator is slashed
  6. If no dispute is raised, the result finalizes after the window closes
// Request inference with optimistic verification
function requestWithOptimisticVerification(bytes32 modelId, bytes calldata input)
    external payable
{
    IInferenceEngine engine = IInferenceEngine(address(0x0101));
 
    // The verification tier is encoded in the request options
    bytes memory options = abi.encode(
        uint8(2),           // Verification tier: 2 = Optimistic
        uint256(10),        // Challenge window: 10 blocks
        uint256(100_000)    // Callback gas limit
    );
 
    engine.requestInference{value: msg.value}(
        modelId,
        input,
        address(this),
        this.onVerifiedResult.selector,
        100_000
    );
}

Tradeoffs:

PropertyValue
LatencyChallenge window (10-50 blocks, configurable)
Gas cost~15,000 base + ~50,000 if disputed
Security modelEconomic security; disputes are expensive to raise frivolously
Best forMedium-value decisions, DeFi integrations, governance inputs

Challengers must stake SALT to raise a dispute, which they forfeit if the original result is confirmed correct. This prevents spam disputes while ensuring legitimate challenges are economically viable.

Tier 3: ZK-SNARK Verification

ZK-SNARK verification provides the highest security guarantee: a cryptographic proof that the inference was executed correctly on the declared model with the given inputs. No trust assumptions, no challenge windows, no economic games.

How it works:

  1. The operator executes inference inside a zkVM (zero-knowledge virtual machine)
  2. The zkVM produces a SNARK proof alongside the output
  3. The proof attests that a specific computation (the model) was applied to specific inputs to produce the output
  4. The on-chain verifier checks the proof in constant time regardless of computation complexity
// ZK-SNARK verification on-chain
interface IZKVerifier {
    function verifyProof(
        bytes32 requestId,
        bytes calldata output,
        bytes calldata proof,
        bytes32 verificationKey
    ) external view returns (bool valid);
}
 
// Request ZK-verified inference
contract HighValueApp {
    IInferenceEngine constant engine = IInferenceEngine(address(0x0101));
 
    function requestZKInference(bytes32 modelId, bytes calldata input)
        external payable
    {
        // ZK verification is specified through higher payment
        // The network routes to ZK-capable operators
        engine.requestInference{value: msg.value}(
            modelId,
            input,
            address(this),
            this.onZKVerifiedResult.selector,
            200_000
        );
    }
 
    function onZKVerifiedResult(bytes32 requestId, bytes calldata output) external {
        // This callback only fires if the ZK proof verified successfully
        // Process with full cryptographic confidence
    }
}

Tradeoffs:

PropertyValue
Latency30 seconds to 5 minutes (proof generation)
Gas cost~300,000 gas for proof verification
Security modelCryptographic; no trust assumptions
Best forHigh-value financial decisions, governance votes, regulatory compliance

ZK-SNARK proofs are computationally expensive to generate, so inference fees for this tier are significantly higher. Currently, ZK verification is supported for models under 1 billion parameters. Larger models use a hybrid approach where critical layers are proven and others use optimistic verification.

Choosing the Right Tier

Select your verification tier based on the value at risk and your latency tolerance:

ScenarioRecommended TierRationale
Chat responses, content generationSignatureLow stakes, high frequency
DeFi price feeds, risk scoresOptimisticMedium stakes, acceptable delay
Loan approvals, large tradesZK-SNARKHigh value at risk
Governance proposal analysisOptimistic or ZKDepends on proposal value
Real-time gaming, UX interactionsSignatureLatency-critical

You can also mix tiers within a single application. We built the system this way intentionally -- for example, a DeFi protocol might use Signature verification for displaying indicative prices and ZK-SNARK verification for executing actual trades.

Further Reading