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:
-
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.
-
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
falseto protect a friendly whale from liquidation, ortrueto force-liquidate a competitor. -
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.
-
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.
-
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
- Each FHE gate produces a gate-level statement: "gate G with encrypted inputs (a, b) produced encrypted output c"
- The IVC accumulator folds each gate proof into a running proof
- The final proof is a single constant-size SNARK attesting to the entire execution trace
- Proof size MUST NOT exceed
maxProofSize()(RECOMMENDED: 1024 bytes) - 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 interfacecontracts/interfaces/IERC8229_Chaining.sol— verified composition of encrypted computationscontracts/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
Copyright and related rights waived via CC0.
