--- eip: 8039 title: ZK Proof Verification for Smart Accounts description: Proof-system-agnostic interface for on-chain verification of succinct zero-knowledge proof by smart accounts. author: Walid Khemiri (@khemiriwalid), Ismail Amara (@ismailamara13) discussions-to: https://ethereum-magicians.org/t/erc-8039-zk-proof-verification-for-smart-accounts-programming-smart-account-logic-with-zk-circuits/27919 status: Draft type: Standards Track category: ERC created: 2025-10-04 --- ## Abstract This ERC defines a standard interface that allows smart accounts to verify succinct zero-knowledge proofs on-chain. It standardizes how proofs and public inputs are supplied and how verification success or failure is reported, following a pattern similar to [ERC-1271](./erc-1271.md) signature validation. ## Motivation Smart accounts increasingly need to verify zero-knowledge proofs for: - Authentication - Authorization - Private state transitions: Prove valid state changes without revealing data - Compliance proofs: KYC/AML verification without data exposure - Account Recovery However, each proof system (Groth16, PLONK, etc) has different interfaces, making integration difficult. This ERC provides a **unified interface** that: - Works with any proof system (Groth16, PLONK, HONK, etc.) - Follows the [ERC-1271](./erc-1271.md) pattern (familiar to developers) - Enables proof-system-agnostic smart account logic ## 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. This standard defines a minimal interface for on-chain verification of zero-knowledge proofs by smart accounts. The interface follows the [ERC-1271](./erc-1271.md) pattern, returning a magic value on successful verification rather than reverting on failure. Each verifier contract is specialized for a single proof system implementation and a single statement/relation, ensuring simplicity and gas efficiency. The instance (public inputs) is passed as a parameter during the verification call. This standard does not mandate where or how the instance is obtained or constructed—it may be provided externally by the caller, retrieved from on-chain storage, computed on-chain, or any combination thereof. Implementers have full flexibility to design instance sourcing according to their application requirements. Verifier contracts expose three functions: proof verification, proof type identification, and human-readable metadata. This separation allows both automated tooling (via proof types) and human operators (via metadata) to understand what proofs are being verified and how. ### Core Interface ```solidity /// @title IERC8039 /// @notice Standard interface for verifying proofs from a single proof system (ERC-8039) /// @dev Similar to ERC-1271 but for ZK proofs instead of signatures /// Magic value for successful verification: bytes4(keccak256("verifyProof(bytes,bytes)")) = 0x534f5876 interface IERC8039 { /// @notice Verifies a zero-knowledge proof /// @dev MUST NOT revert on invalid proofs; MUST return 0x00000000 instead /// @param publicInputs The public inputs for the proof (the instance) /// @param proof The serialized proof data (the evidence) /// @return magicValue Returns 0x534f5876 if valid, 0x00000000 otherwise function verifyProof( bytes calldata publicInputs, bytes calldata proof ) external view returns (bytes4 magicValue); /// @notice Returns the proof type this verifier supports /// @dev Used for introspection and verification /// Format: keccak256("system-implementation") /// Example: keccak256("groth16-circom") /// @return proofType The proof type identifier function getProofType() external view returns (bytes32 proofType); /// @notice Returns human-readable metadata describing the statement/relation being proven /// @dev Describes what this verifier proves (not how it proves it). /// Should include application name, purpose, and version. /// Example: "Age verification v2.1.0" /// @return metadata Human-readable description of the statement function metadata() external view returns (string memory metadata); } ``` ### Proof Type Identifiers Proof types use implementation-specific identifiers to distinguish between different implementations of proof systems. **Format:** `keccak256("system-implementation")` **Examples:** ```solidity keccak256("groth16-circom") keccak256("groth16-gnark") keccak256("plonk-circom") keccak256("plonk-gnark") keccak256("plonk-halo2_pse") keccak256("ultraplonk-barretenberg") keccak256("honk-barretenberg") keccak256("sp1") keccak256("risc0") ``` ### Provable Statement Metadata The `metadata()` function returns a human-readable description of what statement/relation the verifier proves. **Key Properties:** - Descriptive: Clearly states the application and purpose - Versioned: Includes version number for upgrades/changes - Human-readable: For debugging, auditing, and UI display **Examples:** ```solidity // ZK MultiSig function metadata() external pure returns (string memory) { return "Private signers v1.3.0 - Private multisig wallet"; } // Age verification function metadata() external pure returns (string memory) { return "Age verification v2.1.0 - Prove age > 18"; } // Voting eligibility function metadata() external pure returns (string memory) { return "DAO voting v1.0.0 - Token holder eligibility"; } // Privacy payment function metadata() external pure returns (string memory) { return "Private transfer v2.5.0 - Shielded USDC transactions"; } ``` ## Rationale ### Why Magic Value Pattern (Like [ERC-1271](./erc-1271.md))? This ERC follows the [ERC-1271](./erc-1271.md) pattern for consistency: - Familiar to developers: Smart account developers already understand this pattern from signature validation - Non-reverting behavior: Allows batch verification without try/catch overhead - Composability: Callers can check the return value inline without exception handling - Gas efficiency: Returning a value is cheaper than reverting with error messages The magic value `0x534f5876` is `bytes4(keccak256("verifyProof(bytes,bytes)"))`, following the same convention as [ERC-1271](./erc-1271.md)'s `0x1626ba7e`. ### Why Separate `getProofType()` and `metadata()`? `getProofType()` identifies the **technical implementation**: - Enables verification that proof format matches expectations - Allows routing logic based on proof system - Differentiates between implementations of the same protocol (e.g., Circom vs Gnark Groth16) - Machine-readable identifier for automated tooling `metadata()` describes the **semantic meaning**: - Human-readable description of what statement is being proven - Includes version information for circuit/program upgrades - Aids debugging, auditing, and user interface display - Documents the application-level purpose Together, they provide complete context: **how** the proof is verified (technical) and **what** is being proven (semantic). ### Why Not Revert on Invalid Proofs? Following [ERC-1271](./erc-1271.md)'s design philosophy: 1. Batch verification: Multiple proofs can be verified in a single call without one failure blocking others 2. Gas efficiency: No revert overhead or error message encoding 3. Predictable behavior: Callers always receive a response, simplifying integration 4. Consistent pattern: Matches existing smart account verification patterns Invalid proofs return `0x00000000` instead of the magic value, allowing callers to handle failures gracefully. ### Why `bytes calldata` for Public Inputs? The generic `bytes` type allows maximum flexibility: - Proof-system agnostic: Different systems use different formats (bytes32[], uint256[], structured data) - Forward compatible: New proof systems can define their own formats - Efficient encoding: Systems can optimize their encoding without interface changes - Verifier-specific decoding: Each adapter decodes according to its needs Adapters are responsible for decoding public inputs into their proof system's expected format. ## Reference Implementation ### Constants Library ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; /** * @title ERC8039Constants * @dev Constants for ERC-8039 proof verification standard */ library ERC8039Constants { /** * @notice Magic value to be returned upon successful proof verification * @dev bytes4(keccak256("verifyProof(bytes,bytes)")) = 0x534f5876 * Similar to ERC-1271's 0x1626ba7e magic value */ bytes4 internal constant PROOF_MAGIC_VALUE = 0x534f5876; } /** * @title ProofTypes * @dev Identifiers for different ZK proof system implementations */ library ProofTypes { bytes32 public constant HONK_BARRETENBERG = keccak256("honk-barretenberg"); bytes32 public constant GROTH16_CIRCOM = keccak256("groth16-circom"); bytes32 public constant GROTH16_GNARK = keccak256("groth16-gnark"); bytes32 public constant SP1 = keccak256("sp1"); bytes32 public constant RISC0 = keccak256("risc0"); } ``` ### 1. Honk Proof Verifier (Noir/Barretenberg) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC8039, ProofTypes, ERC8039Constants} from "./interfaces/IERC8039.sol"; import {HonkVerifier} from "./HonkVerifier.sol"; /** * @title Honk Proof Verifier Adapter * @notice Adapts the HonkVerifier (Noir/Aztec) to IERC8039 */ contract HonkProofVerifier is IERC8039 { /** * @notice The underlying Honk verifier contract */ HonkVerifier public immutable honkVerifier; /** * @notice Creates a new Honk proof verifier adapter * @param _honkVerifier The address of the HonkVerifier contract */ constructor(address _honkVerifier) { require(_honkVerifier != address(0), "Invalid verifier address"); honkVerifier = HonkVerifier(_honkVerifier); } /** * @inheritdoc IERC8039 */ function verifyProof( bytes calldata publicInputs, bytes calldata proof ) external view returns (bytes4 magicValue) { // Decode public inputs to bytes32[] array (expected by HonkVerifier) bytes32[] memory decodedInputs = abi.decode(publicInputs, (bytes32[])); // Verify the proof using HonkVerifier bool isValid = honkVerifier.verify(proof, decodedInputs); // Return magic value if valid if (isValid) { magicValue = ERC8039Constants.PROOF_MAGIC_VALUE; } } /** * @inheritdoc IERC8039 */ function getProofType() external pure returns (bytes32) { return ProofTypes.HONK_BARRETENBERG; } /** * @inheritdoc IERC8039 */ function metadata() external pure returns (string memory) { return "ZK MultiSig v1.0.0 - Threshold signature"; } } ``` ### 2. Groth16 Proof Verifier (Circom/SnarkJS) ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC8039, ProofTypes, ERC8039Constants} from "./interfaces/IERC8039.sol"; /** * @title Groth16 Verifier Interface (Circom-generated) */ interface IGroth16Verifier { function verifyProof( uint256[2] calldata a, uint256[2][2] calldata b, uint256[2] calldata c, uint256[] calldata input ) external view returns (bool); } /** * @title Groth16 Proof Verifier Adapter * @notice Adapts Circom-generated Groth16 verifier to IERC8039 */ contract Groth16ProofVerifier is IERC8039 { /** * @notice Underlying Groth16 verifier (generated by Circom) */ IGroth16Verifier public immutable groth16Verifier; /** * @notice Creates a new Groth16 proof verifier adapter */ constructor(address _verifier) { require(_verifier != address(0), "Invalid verifier"); groth16Verifier = IGroth16Verifier(_verifier); } /** * @inheritdoc IERC8039 */ function verifyProof( bytes calldata publicInputs, bytes calldata proof ) external view returns (bytes4 magicValue) { // Decode Groth16 proof components (a, b, c) (uint256[2] memory a, uint256[2][2] memory b, uint256[2] memory c) = abi.decode(proof, (uint256[2], uint256[2][2], uint256[2])); // Decode public inputs uint256[] memory inputs = abi.decode(publicInputs, (uint256[])); // Verify proof bool isValid = groth16Verifier.verifyProof(a, b, c, inputs); // Return magic value if valid if (isValid) { magicValue = ERC8039Constants.PROOF_MAGIC_VALUE; } } /** * @inheritdoc IERC8039 */ function getProofType() external pure returns (bytes32) { return ProofTypes.GROTH16_CIRCOM; } /** * @inheritdoc IERC8039 */ function metadata() external pure returns (string memory) { return "ZK MultiSig v1.0.0 - Threshold signature"; } } ``` ### 3. SP1 zkVM Proof Verifier ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {IERC8039, ProofTypes, ERC8039Constants} from "./interfaces/IERC8039.sol"; /** * @title SP1 Verifier Interface * @author Succinct Labs * @notice This contract is the interface for the SP1 Verifier. */ interface ISP1Verifier { /** * @notice Verifies a proof with given public values and vkey. * @dev It is expected that the first 4 bytes of proofBytes must match the first 4 bytes of * target verifier's VERIFIER_HASH. * @param programVKey The verification key for the RISC-V program. * @param publicValues The public values encoded as bytes. * @param proofBytes The proof of the program execution the SP1 zkVM encoded as bytes. */ function verifyProof( bytes32 programVKey, bytes calldata publicValues, bytes calldata proofBytes ) external view; } /** * @title SP1 Proof Verifier Adapter * @notice Adapts SP1 zkVM verifier to IERC8039 * @dev SP1 is a zero-knowledge virtual machine for proving Rust program execution */ contract SP1ProofVerifier is IERC8039 { /** * @notice Underlying SP1 verifier */ ISP1Verifier public immutable sp1Verifier; /** * @notice Program verification key (specific to the Rust program being verified) */ bytes32 public immutable programVKey; /** * @notice Creates a new SP1 proof verifier adapter */ constructor(address _verifier, bytes32 _programVKey) { require(_verifier != address(0), "Invalid verifier"); require(_programVKey != bytes32(0), "Invalid program vkey"); sp1Verifier = ISP1Verifier(_verifier); programVKey = _programVKey; } /** * @inheritdoc IERC8039 */ function verifyProof( bytes calldata publicInputs, bytes calldata proof ) external view returns (bytes4 magicValue) { // SP1 verifier reverts on failure, so we use try/catch try sp1Verifier.verifyProof(programVKey, publicInputs, proof) { // If no revert, proof is valid magicValue = ERC8039Constants.PROOF_MAGIC_VALUE; } catch { // Proof verification failed, return 0x00000000 (default) } } /** * @inheritdoc IERC8039 */ function getProofType() external pure returns (bytes32) { return ProofTypes.SP1; } /** * @inheritdoc IERC8039 */ function metadata() external pure returns (string memory) { return "SP1 zkVM v1.0.0 - Rust program execution verification"; } } ``` ## Security Considerations ### Proof Replay Attacks **CRITICAL:** Zero-knowledge proofs verified by smart accounts are vulnerable to replay attacks if not properly protected. A valid proof verified once can be replayed to: - Execute unauthorized operations on the same account - Replay the same proof across different accounts - Reuse proofs across different chains Implementers MUST include replay prevention mechanisms in their provable statement design, specifically within the public inputs of the circuit. The specific mechanism should be chosen based on the application's security requirements and threat model. Common approaches include but are not limited to binding proofs to specific contexts (account addresses, chain IDs, nonces, timestamps, or other contextual data). The choice of replay prevention mechanism is outside the scope of this standard and should be carefully designed for each use case. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).