EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8229: FHE Computation Verification

An interface for on-chain verification of FHE computation via recursive zero-knowledge proofs.

Authors
Created2026-04-17
Discussion Linkhttps://ethereum-magicians.org/t/erc-8229-fhe-computation-verification/28217
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1682PR open

EIP-GPT summary

Contents
AbstractMotivationWhy not just trust the FHE node?Why recursive proofs?SpecificationDefinitionsVerification FlowInterfaceERC-165 Interface DetectionOptional Extension: Prover RegistryOptional Extension: Computation ChainingProof Public InputsIVC Folding RequirementsCommitment SchemeRationaleWhy store only verificationKeyHash on-chain?Why compute executionId on-chain?Why disputeResult?Why proverAddress in public inputs?Why registerCircuitUpgrade?Why gracePeriod in deactivateCircuit?Why proofSystemId as bytes4?Why a challenger reward?Formal Verification of Underlying Proof SystemsBackwards CompatibilityReference ImplementationSecurity ConsiderationsVerification Key SubstitutionexecutionId CollisionProof MalleabilityCo-processor Denial of ServiceFreshness and Replay AttacksGas Cost AttacksRecursive Proof SoundnessChain ReorganizationCopyright

Abstract

This EIP defines an interface for on-chain verification of Fully Homomorphic Encryption (FHE) computation. When a smart contract executes logic on encrypted data, external observers cannot inspect the computation. This EIP provides a mechanism for anyone to verify that the encrypted computation was performed correctly — without learning anything about the encrypted inputs, outputs, or intermediate values.

The EIP specifies how FHE circuits are registered, how recursive zero-knowledge proofs of correct execution are submitted and verified on-chain, and how verified results are consumed by dependent contracts. A single compact proof (≤ 1 KB) attests to arbitrarily complex encrypted computations via Incrementally Verifiable Computation (IVC) proof folding.

This is the trust layer that makes blind computation trustless.

Motivation

FHE enables computation on encrypted data. Computation alone is not enough — you need verification. Without verification, an FHE co-processor (the node performing the encrypted computation) could return arbitrary results. The user cannot check the output because it is encrypted. The contract cannot check because it never sees plaintext.

This creates a fundamental trust problem:

  1. Co-processor honesty — FHE computation is delegated to specialized hardware (GPUs, FPGAs, ASICs). These co-processors are external to the EVM. Without verification, you must trust the co-processor operator — defeating the purpose of decentralization.

  2. Result integrity — When an encrypted DeFi vault computes "does this position exceed the liquidation threshold?", the answer must be provably correct. A malicious co-processor could return false to protect a friendly whale from liquidation, or true to force-liquidate a competitor.

  3. Composability — When Contract A consumes the output of Contract B's encrypted computation, Contract A needs assurance that B's computation was correct. Without a verification interface, each contract pair must implement custom trust assumptions.

  4. Auditability — Regulators and auditors need to verify that encrypted financial computations (tax calculations, compliance checks, solvency proofs) were performed correctly, without seeing the underlying data.

  5. Multi-party computation chains — In complex DeFi protocols, encrypted outputs of one computation become inputs to the next. Errors compound. Verification at each step prevents cascading failures.

Why not just trust the FHE node?

For the same reason we do not trust Ethereum validators to execute transactions correctly — we verify. The entire value proposition of blockchain is trustless execution. FHE without verification is encrypted cloud computing with extra steps. This EIP brings FHE computation into the trustless paradigm.

Why recursive proofs?

FHE operations are expensive. A single encrypted multiplication can involve thousands of polynomial operations. Proving each operation individually would produce proofs larger than the computation itself. Recursive proof composition (IVC — Incrementally Verifiable Computation) folds N operation proofs into a single constant-size proof. One 256-byte SNARK attests to an entire encrypted program execution, verifiable in O(1) on-chain.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Definitions

  • Circuit: A deterministic program compiled from encrypted contract logic into a sequence of FHE gates (ADD, MUL, CMP, MUX). Each circuit has a unique hash identifying its structure.
  • Gate: A single FHE operation — the atomic unit of encrypted computation.
  • Execution Trace: The ordered sequence of gates executed during a specific invocation of a circuit.
  • Execution Proof: A zero-knowledge proof attesting that a specific execution trace is a valid execution of a registered circuit on specific inputs, producing specific outputs.
  • IVC Folding: The process of combining gate-level proofs into a single constant-size proof. Each gate's proof is folded into the accumulator; the running proof grows by zero bytes per gate.
  • Verification Key: A public key specific to a circuit, used to verify execution proofs. Generated during circuit compilation.
  • Co-processor: The off-chain entity performing the FHE computation and generating the execution proof. Untrusted by design — the proof is the only source of truth.
  • Result Commitment: A Poseidon hash of the encrypted output of a computation.
  • executionId: A unique identifier computed on-chain as keccak256(abi.encodePacked(circuitHash, inputCommitment, resultCommitment, msg.sender, block.number)).

Verification Flow

    Developer writes encrypted contract
                  │
                  ▼
       ┌──────────────────────┐
       │   Circuit Compiler   │  (off-chain)
       └──────────────────────┘
                  │  Produces: circuitHash, verificationKey, gateCount
                  ▼
       ┌──────────────────────┐
       │  registerCircuit()   │  stores keccak256(verificationKey) on-chain
       └──────────────────────┘
                  │  User submits encrypted transaction
                  ▼
       ┌──────────────────────┐
       │   FHE Co-processor   │  untrusted off-chain, executes circuit,
       │                      │  generates IVC proof
       └──────────────────────┘
                  │  Produces: proof, resultCommitment, verificationKey
                  ▼
       ┌──────────────────────┐
       │   submitProof()      │  verifies key hash, computes executionId on-chain
       └──────────────────────┘
                  │
            ┌─────┴─────┐
         VALID        INVALID
            │             │
            ▼             ▼
       executionId      revert
       stored on-chain
            │
            ▼
       Dependent contracts consume via isResultVerified()

Interface

Every compliant contract MUST implement the following interface:

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0;

interface IERCZZZZ {

    struct CircuitInfo {
        bytes32 circuitHash;
        bytes32 verificationKeyHash; // keccak256(verificationKey) — not the key itself
        address registrant;
        uint256 gateCount;
        uint256 registeredAt;
        uint256 currentVersion;
        bool active;
        bytes4 schemeId;             // FHE scheme as bytes4 (not string)
    }

    struct ExecutionResult {
        bytes32 executionId;   // Computed on-chain — NOT provided by caller
        bytes32 circuitHash;
        bytes32 inputCommitment;
        bytes32 resultCommitment;
        bytes32 proofHash;
        address prover;
        uint256 verifiedAt;
        bool verified;
        bool disputed;
    }

    // ─── Errors ──────────────────────────────────────

    error CircuitAlreadyRegistered(bytes32 circuitHash);
    error CircuitNotRegistered(bytes32 circuitHash);
    error CircuitNotActive(bytes32 circuitHash);
    error ProofVerificationFailed(bytes32 executionId, bytes32 circuitHash);
    error ExecutionAlreadySubmitted(bytes32 executionId);
    error VerificationKeyHashMismatch(bytes32 circuitHash, bytes32 provided, bytes32 stored);
    error InvalidInputCommitment(bytes32 executionId);
    error GateCountExceedsLimit(uint256 gateCount, uint256 maxGates);
    error UnauthorizedCircuitRegistrant(address caller);
    error CircuitInGracePeriod(bytes32 circuitHash, uint256 deactivatesAt);
    error ExecutionNotVerified(bytes32 executionId);
    error ExecutionAlreadyDisputed(bytes32 executionId);

    // ─── Events ──────────────────────────────────────

    /// @notice Emitted when a new circuit is registered.
    event CircuitRegistered(
        bytes32 indexed circuitHash,
        address indexed registrant,
        uint256 gateCount,
        bytes32 verificationKeyHash
    );

    /// @notice Emitted when a circuit's verification key is upgraded.
    event CircuitUpgraded(
        bytes32 indexed circuitHash,
        uint256 previousVersion,
        uint256 newVersion,
        bytes32 newVerificationKeyHash
    );

    /// @notice Emitted when a circuit enters grace period before deactivation.
    event CircuitDeactivating(
        bytes32 indexed circuitHash,
        address indexed deactivatedBy,
        uint256 deactivatesAt
    );

    /// @notice Emitted when an execution proof is verified.
    event ExecutionVerified(
        bytes32 indexed executionId,
        bytes32 indexed circuitHash,
        address indexed prover,
        bytes32 resultCommitment
    );

    /// @notice Emitted when a proof fails verification.
    event ExecutionRejected(
        bytes32 indexed executionId,
        bytes32 indexed circuitHash,
        address indexed prover
    );

    /// @notice Emitted when a verified result is disputed.
    event ResultDisputed(
        bytes32 indexed executionId,
        address indexed challenger,
        bytes32 counterProofHash
    );

    // ─── Circuit Registry ─────────────────────────────

    /// @notice Register a circuit. Stores keccak256(verificationKey) — not the full key.
    /// @dev    circuitHash MUST be Keccak256 of the circuit's canonical gate-level representation.
    ///         Only authorized registrants may call this.
    /// @param  circuitHash          Unique circuit identifier.
    /// @param  verificationKeyHash  keccak256(verificationKey) — stored on-chain.
    /// @param  gateCount            Number of FHE gates.
    /// @param  schemeId             FHE scheme identifier as bytes4.
    function registerCircuit(
        bytes32 circuitHash,
        bytes32 verificationKeyHash,
        uint256 gateCount,
        bytes4 schemeId
    ) external;

    /// @notice Upgrade a circuit's verification key.
    /// @dev    Increments currentVersion. Historical results remain valid.
    ///         Only the original registrant or governance may call.
    ///         Emits CircuitUpgraded.
    function registerCircuitUpgrade(
        bytes32 circuitHash,
        bytes32 newVerificationKeyHash
    ) external;

    /// @notice Returns the latest version number for a circuit.
    function latestCircuitVersion(bytes32 circuitHash) external view returns (uint256);

    /// @notice Schedule circuit deactivation with a grace period.
    /// @dev    During gracePeriod seconds, existing submissions are still accepted.
    ///         After expiry, submitProof reverts with CircuitNotActive.
    ///         Emits CircuitDeactivating.
    /// @param  circuitHash  Circuit to deactivate.
    /// @param  gracePeriod  Seconds until deactivation takes effect (RECOMMENDED: 86400).
    function deactivateCircuit(bytes32 circuitHash, uint256 gracePeriod) external;

    function circuitInfo(bytes32 circuitHash) external view returns (CircuitInfo memory);
    function isCircuitActive(bytes32 circuitHash) external view returns (bool);

    // ─── Proof Submission ─────────────────────────────

    /// @notice Submit an execution proof for on-chain verification.
    /// @dev    Verification key is supplied and verified:
    ///           require(keccak256(verificationKey) == circuitInfo.verificationKeyHash)
    ///
    ///         executionId is computed on-chain (caller MUST NOT provide it):
    ///           executionId = keccak256(abi.encodePacked(
    ///             circuitHash, inputCommitment, resultCommitment, msg.sender, block.number
    ///           ))
    ///
    ///         Freshness check — proof's encoded timestamp MUST satisfy:
    ///           |proof.timestamp - block.timestamp| <= maxTimestampDrift()
    ///         Stale or future-dated proofs MUST be rejected.
    ///
    /// @param  circuitHash       Which circuit was executed.
    /// @param  inputCommitment   Poseidon commitment to the encrypted inputs.
    /// @param  resultCommitment  Poseidon commitment to the encrypted outputs.
    /// @param  verificationKey   Full verification key — verified against stored hash.
    /// @param  proof             The IVC execution proof.
    /// @return executionId       The on-chain computed unique execution identifier.
    function submitProof(
        bytes32 circuitHash,
        bytes32 inputCommitment,
        bytes32 resultCommitment,
        bytes calldata verificationKey,
        bytes calldata proof
    ) external returns (bytes32 executionId);

    /// @notice Batch-submit multiple execution proofs atomically.
    /// @dev    Each proof verified independently. Entire batch reverts on any failure.
    function submitProofBatch(
        bytes32[] calldata circuitHashes,
        bytes32[] calldata inputCommitments,
        bytes32[] calldata resultCommitments,
        bytes[] calldata verificationKeys,
        bytes[] calldata proofs
    ) external returns (bytes32[] memory executionIds);

    /// @notice Dispute a verified result with a counter-proof.
    /// @dev    The counterProof MUST demonstrate that resultCommitment is inconsistent
    ///         with the stated circuit and inputCommitment.
    ///         Emits ResultDisputed if the dispute is upheld.
    function disputeResult(bytes32 executionId, bytes calldata counterProof) external;

    // ─── Result Queries ───────────────────────────────

    function isResultVerified(bytes32 executionId) external view returns (bool);
    function isDisputed(bytes32 executionId) external view returns (bool);
    function executionResult(bytes32 executionId) external view returns (ExecutionResult memory);

    /// @notice Returns the result commitment for a verified execution.
    /// @dev    MUST revert with ExecutionNotVerified if not verified.
    function verifiedResultCommitment(bytes32 executionId) external view returns (bytes32);

    function verifyResultProvenance(
        bytes32 executionId,
        bytes32 circuitHash,
        bytes32 inputCommitment,
        bytes32 resultCommitment
    ) external view returns (bool);

    // ─── Configuration ────────────────────────────────

    function maxGateCount() external view returns (uint256);
    function maxProofSize() external view returns (uint256);

    /// @notice Maximum allowed drift between proof timestamp and block.timestamp.
    /// @dev    Used for freshness check. RECOMMENDED: 300 seconds.
    function maxTimestampDrift() external view returns (uint256);

    /// @notice Returns the supported proof system as bytes4.
    /// @dev    Gas-efficient alternative to string. Known values:
    ///           0x4e6f7661 — Nova
    ///           0x48616c32 — Halo2
    ///           0x534e6f76 — SuperNova
    function proofSystemId() external view returns (bytes4);
}

ERC-165 Interface Detection

Compliant contracts MUST implement ERC-165 and return true for the interface ID of IERCZZZZ.

Optional Extension: Prover Registry

For networks with multiple competing co-processors:

interface IERCZZZZ_ProverRegistry is IERCZZZZ {

    struct ProverInfo {
        address prover;
        uint256 stake;
        uint256 totalExecutions;
        uint256 failedExecutions;
        uint256 registeredAt;
        bool active;
    }

    event ProverRegistered(address indexed prover, uint256 stake);

    /// @notice Emitted when a prover is slashed.
    event ProverSlashed(
        address indexed prover,
        uint256 amount,
        bytes32 indexed executionId,
        address indexed challenger
    );

    /// @notice Emitted when a challenger earns their slash reward.
    event ChallengerRewarded(
        address indexed challenger,
        uint256 reward,
        bytes32 indexed executionId
    );

    function registerProver() external payable;
    function proverInfo(address prover) external view returns (ProverInfo memory);
    function isProverActive(address prover) external view returns (bool);

    /// @notice Slash a prover. Challenger receives challengerRewardBps() of the slash.
    /// @dev    Emits ProverSlashed and ChallengerRewarded.
    function slashProver(address prover, bytes32 executionId, address challenger) external;

    /// @notice Basis points of slashed amount paid to the challenger. RECOMMENDED: 1000.
    function challengerRewardBps() external view returns (uint256);
    function minProverStake() external view returns (uint256);
}

Optional Extension: Computation Chaining

For multi-step encrypted computation pipelines:

interface IERCZZZZ_Chaining is IERCZZZZ {

    event ChainVerified(bytes32[] executionChain, bytes32 finalResultCommitment);

    /// @notice Returns true if executionB's input is executionA's verified output.
    function isChainedExecution(bytes32 executionA, bytes32 executionB)
        external view returns (bool);

    /// @notice Verify an ordered chain of executions end-to-end.
    function verifyExecutionChain(bytes32[] calldata executionChain)
        external view returns (bool);
}

Proof Public Inputs

The execution proof MUST encode the following public inputs:

executionPublicInputs = {
    chainId:           uint256,   // EIP-155 chain ID
    contractAddress:   address,   // Verifier contract address
    executionId:       bytes32,   // On-chain computed — keccak256(circuit||input||result||sender||block)
    circuitHash:       bytes32,   // Circuit that was executed
    inputCommitment:   bytes32,   // Poseidon hash of encrypted inputs
    resultCommitment:  bytes32,   // Poseidon hash of encrypted outputs
    proverAddress:     address,   // Co-processor address — binds proof to prover identity
    gateCount:         uint256,   // Number of gates executed
    proofSystemId:     bytes4,    // Proof system identifier
    timestamp:         uint256    // Block timestamp at proof generation time
                                  // Freshness: |timestamp - block.timestamp| <= maxTimestampDrift()
}

Prover binding: proverAddress MUST equal msg.sender. A proof generated by prover A cannot be submitted by prover B.

Freshness check: Implementations MUST reject proofs where |proof.timestamp - block.timestamp| > maxTimestampDrift() to prevent replay of outdated computation attestations.

IVC Folding Requirements

  1. Each FHE gate produces a gate-level statement: "gate G with encrypted inputs (a, b) produced encrypted output c"
  2. The IVC accumulator folds each gate proof into a running proof
  3. The final proof is a single constant-size SNARK attesting to the entire execution trace
  4. Proof size MUST NOT exceed maxProofSize() (RECOMMENDED: 1024 bytes)
  5. Verification gas cost MUST be O(1) regardless of circuit complexity

Implementations SHOULD use:

  • Nova (Relaxed R1CS folding) — RECOMMENDED for gate-level IVC
  • Halo2 (IPA-based recursive composition)
  • SuperNova (multi-circuit IVC folding)

Commitment Scheme

inputCommitment  = Poseidon(encryptedInput_1 || ... || encryptedInput_n)
resultCommitment = Poseidon(encryptedOutput_1 || ... || encryptedOutput_m)

Implementations MAY use Keccak256 for non-ZK contexts but SHOULD prefer Poseidon for consistency.

Rationale

Why store only verificationKeyHash on-chain?

Verification keys can be hundreds of KB. Storing only keccak256(verificationKey) (32 bytes) reduces registration cost by 99%+ while preserving integrity. The full key is supplied at proof-submission time and verified against the stored hash.

Why compute executionId on-chain?

Allowing callers to provide their own executionId enables front-running and squatting attacks. Computing executionId = keccak256(circuitHash || inputCommitment || resultCommitment || msg.sender || block.number) on-chain removes this attack surface entirely.

Why disputeResult?

Initial on-chain verification uses a compact proof system optimized for speed. A sophisticated adversary with access to a broken implementation could generate a proof that passes verification but is computationally incorrect. disputeResult allows a challenger to submit a counter-proof demonstrating incorrectness. Provers stake collateral slashable if their result is successfully disputed.

Why proverAddress in public inputs?

Without binding the proof to msg.sender, a prover could generate a valid proof and another party could submit it first (front-running), claiming verification credit. Including proverAddress in public inputs makes proofs non-transferable.

Why registerCircuitUpgrade?

Circuits may need updates (bug fixes, efficiency improvements). The upgrade mechanism allows the verification key to change while preserving the circuit's identity and all historical results.

Why gracePeriod in deactivateCircuit?

Immediate deactivation would break in-flight computations: a co-processor executing a circuit that gets deactivated between execution start and proof submission would have their work invalidated. A grace period (RECOMMENDED: 24 hours) allows in-flight computations to complete.

Why proofSystemId as bytes4?

String comparison is gas-expensive and error-prone. bytes4 allows constant-gas equality checks and fits in a single storage slot.

Why a challenger reward?

Without economic incentive, no one verifies results. The challenger reward (a percentage of slashed stake) creates a profitable market for result verification, ensuring incorrect computations are quickly identified and disputed.

Formal Verification of Underlying Proof Systems

This standard defines an interface for verifying FHE computation via zero-knowledge proofs, but does not mandate a specific proof system. Implementations may use Halo2, Nova (and folding-scheme successors such as HyperNova or SuperNova), Plonky3, or any system meeting the verification semantics defined in this specification.

The cryptographic primitives underlying these proof systems — sum-check arguments, FRI (Fast Reed-Solomon Interactive Oracle Proof of Proximity), WHIR, and related Interactive Oracle Proof machinery — are the subject of active formal verification work in the Verified-zkEVM project, notably through the ArkLib library. ArkLib aims to provide machine-checked completeness and round-by-round knowledge soundness proofs in Lean 4 for the information-theoretic core of these systems.

This standard does not require implementations to use formally verified proof systems today, but is designed to remain compatible with such systems as they reach production maturity. Future revisions MAY define a proofSystemId (bytes4) field allowing implementations to declare conformance to a specific verified proof system specification; the registry of verified-system identifiers is left to a subsequent extension or to off-chain metadata.

Backwards Compatibility

This EIP introduces entirely new functionality with no precedent in existing ERCs. It has no backwards compatibility concerns.

Contracts that consume verified FHE results interact through isResultVerified() and verifiedResultCommitment(). No changes to existing contracts are required.

This EIP REQUIRES ERC-165 for interface detection.

Reference Implementation

The normative interfaces are published at Valisthea/styx-erc-fhe-verification:

  • contracts/interfaces/IERC8229.sol — the core verification interface
  • contracts/interfaces/IERC8229_Chaining.sol — verified composition of encrypted computations
  • contracts/interfaces/IERC8229_ProverRegistry.sol — co-processor reputation, stake and slashing

A working implementation of the on-chain IVC verifier is in development and is not yet available. This section will be updated when it exists and is tested.

Security Considerations

Verification Key Substitution

If an attacker replaces a circuit's stored hash, they can generate valid proofs for arbitrary computations. Implementations MUST restrict registerCircuit() and registerCircuitUpgrade() to authorized parties.

executionId Collision

The on-chain executionId computation includes block.number and msg.sender. Two identical submissions in the same block by the same prover would produce the same ID and the second would revert with ExecutionAlreadySubmitted. This is the correct behavior.

Proof Malleability

Some proof systems allow transforming a valid proof into another valid proof for the same statement. Implementations SHOULD use non-malleable proof systems, or derive proofHash in a malleable-resistant way.

Co-processor Denial of Service

A malicious co-processor could refuse to generate proofs. The Prover Registry extension mitigates this through competing provers. Implementations SHOULD support multiple provers per circuit.

Freshness and Replay Attacks

The timestamp check (|proof.timestamp - block.timestamp| <= maxTimestampDrift()) prevents proofs generated in an outdated context from being submitted. Without this, a prover could cache a proof and replay it when state has changed.

Gas Cost Attacks

Proof verification is gas-intensive. Implementations SHOULD require prover registration with stake, and SHOULD charge a submission fee refunded on success, to deter invalid proof spam.

Recursive Proof Soundness

The security of IVC folding depends on the underlying proof system's soundness. Implementations SHOULD use well-audited implementations (Nova, Halo2) and SHOULD NOT use experimental folding schemes in production.

Chain Reorganization

A verified execution could be reverted by chain reorganization. Dependent contracts SHOULD wait for 64 block confirmations before treating a result as final.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth