ERC-8039: ZK Proof Verification for Smart Accounts(Programming Smart Account Logic with ZK Circuits)

Category: Standards Track, Account Abstraction, Zero-Knowledge Proofs

Authors: Walid Khemiri, Ismail Amara

Status: Discussion

Related: [ERC-8039 Draft]( Add ERC: ZK Proof Verification for Smart Accounts by khemiriwalid · Pull Request #1238 · ethereum/ERCs · GitHub )

First implementation: GitHub - MicrochainLabs/microchain-zk-signers · GitHub

  1. Abstract

Smart accounts enable **programmable account logic** through on-chain code. Zero-knowledge proofs unlock two superpowers for smart accounts:

1. Privacy: Execute logic while hiding sensitive information (private inputs, hidden state)

2. Succinctness: Verify complex off-chain computations with small on-chain proofs

The missing piece? A standardized interface for smart accounts to verify ZK proofs on-chain.

We introduce **ERC-8039: ZK Proof Verification for Smart Accounts** - a proof-system-agnostic interface that enables smart accounts to program authentication, authorization, recovery, and any account-related logic using zero-knowledge circuits. Following the proven ERC-1271 pattern, ERC-8039 provides a unified interface that works with any proof system (Groth16, PLONK, HONK, etc.), enabling:

Privacy Use Cases:

:white_check_mark: Private Authentication: Prove signature validation without revealing signers, or thresholds

:white_check_mark: Privacy-Preserving Policies: Enforce spending limits, gas prices, time-locks keeping constraints hidden

:white_check_mark: Confidential Recovery: Account recovery with hidden guardians and mechanisms

:white_check_mark: Credential Verification: Prove age, identity, credit score without revealing data

Succinctness Use Cases:

:white_check_mark: Complex Policy Validation: Execute sophisticated rules off-chain, verify cheaply on-chain

:white_check_mark: Historical State Verification: Prove properties about past blockchain state (co-processor pattern)

:white_check_mark: Trust-Minimized Computation: Execute arbitrary logic off-chain with on-chain verification

Infrastructure**:**

:white_check_mark: Proof-System Agnostic: Switch or support multiple proof systems without changing account logic

:white_check_mark: Forward Compatible: Works with any ZK framework (Noir, Circom, SP1, RISC0) and future proof systems

Architecture:

Introducing Provable Ownership

A key application of ERC-8039 is **Programmable Ownership** - defining account ownership through zero-knowledge circuit logic rather than on-chain code. Just as smart contracts evolved from “Programmable Money” to “Programmable Ownership” (multisig, timelock, DAO governance), ERC-8039 enables the next evolution: **Provable Ownership**.

The Evolution:

Traditional EOA: ownership = private key

                (single point of failure)

                     ↓

Programmable Ownership: ownership = smart contract code

                (multisig, policies, governance)

                BUT all logic is public

                     ↓

Provable Ownership: ownership = zk circuit code

                (same capabilities, fully private)

                ENABLED BY ERC-8039 ✓

Examples:

| Ownership Model | Traditional (Public) | Provable (Private via ERC-8039) |

| Multisig | 3-of-5 signers visible on-chain | 3-of-5 signers hidden in ZK circuit |

| Weighted Voting | “Alice: 40%, Bob: 30%, Carol: 30%” public | Weights hidden, only threshold proven |

| Time-Locked | “Cannot withdraw until block X” visible | Time constraints hidden until met |

|*Role-Based | “CEO: unlimited, Manager: 10k, Employee: 1k” public | Roles and limits completely private |

Why This Matters:

Traditional programmable ownership sacrifices privacy - competitors can see your signers, attackers know whom to target, organizational structure is exposed. **Provable Ownership** via ERC-8039 provides the same programmability with complete confidentiality - prove authorization without revealing who, how, or why.

  1. Motivation

The Problem: Programming Smart Account Logic with ZK Circuits Requires Standardized Verification

Smart accounts revolutionized Ethereum by making account logic programmable. But they face two fundamental limitations:

1. Privacy: All logic is public - signers, thresholds, policies, and recovery mechanisms are visible on-chain

2. Computational Cost Complex logic is expensive - every computation consumes gas proportional to its complexity

Zero-knowledge circuits solve both problems:

On-Chain Logic: if (logic == on-chain code) { programmable && public && expensive }

ZK Circuit Logic: if (logic == zk circuit code) { programmable && private && succinct }

Use Case Dimension 1: Privacy (Hiding Information)

ZK circuits enable private account logic by keeping sensitive information off-chain:

  1. Private Authentication (Provable Ownership)

- Traditional: 3-of-5 multisig with public signers

- ZK Circuit: 3-of-5 multisig with hidden signers, weights, and threshold

  1. Privacy-Preserving Policies

- Traditional: Max spending $10k/day (public limit visible to competitors)

- ZK Circuit: Max spending $10k/day (private limit, corporate privacy preserved)

  1. Confidential Recovery

- Traditional: 2-of-3 guardians (public addresses, target for social engineering)

- ZK Circuit: 2-of-3 guardians (hidden identities, security through obscurity)

  1. Credential Verification

- Traditional: Store credentials on-chain (public, permanent exposure)

- ZK Circuit: Prove credentials without revealing them (private, selective disclosure)

Use Case Dimension 2: Succinctness (Efficient Verification)

ZK circuits enable gas-efficient verification of complex off-chain computations:

  1. Complex Policy Validation (Off-Chain Computation)

- Traditional: Evaluate 50 policy rules on-chain (~500k gas)

- ZK Circuit: Prove all 50 rules satisfied off-chain (~350k gas for verification)

- Benefit: Gas savings increase with complexity (100 rules = 10x cheaper)

  1. Historical State Verification (Co-Processor Pattern)

- Traditional: Cannot access old state (only current state available)

- ZK Circuit: Prove properties about any historical state (e.g., “account had balance > X at block 1M”)

- Benefit: Unlock time-based policies without on-chain state history

  1. Trust-Minimized Computation

- Traditional: Execute complex algorithm on-chain (might exceed block gas limit)

- ZK Circuit: Execute off-chain, prove correctness on-chain (always fits in block)

- Benefit: Arbitrary computation complexity with fixed verification cost

Key Insight: Succinctness is most valuable when:

- Logic is complex (many rules, deep computation)

- Historical data needed (past state, time-series)

- Computation exceeds block gas limit

The Critical Dependency

ZK circuit logic CANNOT work alone - proofs must be verified on-chain by smart contracts. This is the critical dependency that makes both privacy AND succinctness trustless.

The Challenge: Each proof system has a different verification interface:

// Groth16 (Circom)

function verifyProof(

uint256[2] calldata a,

uint256[2][2] calldata b,

uint256[2] calldata c,

uint256[] calldata input

) external view returns (bool);

// PLONK (various implementations)

function verify(

bytes calldata proof,

uint256[] calldata publicInputs

) external view returns (bool);

// UltraHonk (Noir/Barretenberg)

function verify(

bytes calldata proof,

bytes32[] calldata publicInputs

) external view returns (bool);

// SP1 zkVM

function verifyProof(

bytes32 programVKey,

bytes calldata publicValues,

bytes calldata proofBytes

) external view;

**Without a standard interface:**

- :cross_mark: Account contracts tightly coupled to specific proof systems

- :cross_mark: Cannot upgrade proof systems without contract changes

- :cross_mark: Cannot support multiple proof systems

- :cross_mark: Each account type needs custom verifier integration

- :cross_mark: No ecosystem tooling can work across implementations

**With ERC-8039:**

- :white_check_mark: Accounts work with any proof system

- :white_check_mark: Upgrade proof systems via adapter contracts

- :white_check_mark: Support multiple proof systems simultaneously

- :white_check_mark: Unified integration pattern across all accounts

- :white_check_mark: Ecosystem tooling works universally

**These proofs could be generated using:**

- Groth16 (fastest verification, smallest proofs)

- PLONK (universal setup, updatable)

- HONK (no trusted setup, efficient)

- STARKs (post-quantum, transparent)

- zkVMs (SP1, RISC0 - arbitrary program execution)

ERC-8039 enables smart accounts to:

1. Choose the best proof system for each use case (privacy vs succinctness)

2. Upgrade as proof systems improve (better performance, lower gas)

3. Support hybrid approaches (different proofs for different operations)

4. Remain future-compatible with emerging technologies

5. Combine privacy and succinctness in single applications

  1. The Solution: ERC-8039

ERC-8039 follows the proven ERC-1271 signature validation pattern:

// ERC-1271: Signature validation

interface IERC1271 {

function isValidSignature(

bytes32 hash,

bytes memory signature

    ) external view returns (bytes4 magicValue);

// Returns: 0x1626ba7e if valid

}

// ERC-8039: Proof verification

interface IERC8039 {

function verifyProof(

bytes calldata publicInputs,

bytes calldata proof

    ) external view returns (bytes4 magicValue);

// Returns: 0x534f5876 if valid

}

Why This Pattern Works:

1. Familiar to developers: Smart account developers already understand ERC-1271

2. Non-reverting: Returns magic value instead of reverting (gas efficient, composable)

3. Proof-system agnostic: Works with any proof format via adapters

4. Ecosystem compatible: Follows established standards conventions

Core Interface

/**

 * @title IERC8039

 * @notice Standard interface for verifying ZK proofs in smart accounts

 * Magic value: bytes4(keccak256("verifyProof(bytes,bytes)")) = 0x534f5876

 */

interface IERC8039 {

/**

     * @notice Verifies a zero-knowledge proof

     * @dev MUST NOT revert on invalid proofs; returns 0x00000000 instead

     * @param publicInputs The public inputs (instance)

     * @param proof The serialized proof (evidence)

     * @return magicValue 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 Format: keccak256("system-implementation")

     * @return proofType The proof system identifier

     */

function getProofType() external pure returns (bytes32 proofType);

/**

     * @notice Returns human-readable metadata

     * @dev Describes what statement/relation is being proven

     * @return metadata Application name, purpose, and version

     */

function metadata() external pure returns (string memory metadata);

}

Three Key Functions

1. `verifyProof()` - The Core Verification

Purpose: Verify ZK proof against public inputs

Design Principles:

- Non-reverting: Returns `0x00000000` on failure (like ERC-1271)

- Generic bytes: Supports any proof format

- View function: No state changes, gas-efficient

- Magic value: `0x534f5876` on success

2. `getProofType()` - Technical Identification

Purpose: Identify the proof system implementation

Format: `keccak256(“system-implementation”)`

Examples:

keccak256("groth16-circom")      // Groth16 via Circom/SnarkJS

keccak256("groth16-gnark")       // Groth16 via Gnark (Go)

keccak256("plonk-circom")        // PLONK via Circom

keccak256("honk-barretenberg")   // UltraHonk via Noir/Aztec

keccak256("sp1")                 // SP1 zkVM (Rust)

keccak256("risc0")               // RISC0 zkVM (Rust)

Use Cases:

- Verify proof format matches expectations

- Route verification logic based on proof system

- Automated tooling and introspection

- Compatibility checking

3. `metadata()` - Human-Readable Description

Purpose: Describe what statement is being proven

Format: `“ v - ”`

Examples:

“ZK MultiSig v1.0.0 - Private threshold signatures”

“Max Gas Price v1.0.0 - Private gas price limits”

“Spending Limit v2.0.0 - Private daily spending caps”

“Social Recovery v1.0.0 - Hidden guardian recovery”

“Time-Locked Recovery v1.5.0 - Private time-lock mechanisms”

“Age Verification v1.0.0 - Prove age > 18”

Use Cases:

- UI display and debugging

- Audit trails and logging

- Documentation and compliance

- User-facing applications

Separation of Concerns
The three functions provide complete context:

getProofType() → Technical context (“how” - proof system)

verifyProof() → Verification (“does it check out?”)

metadata() → Semantic context (“what” - application purpose)

  1. How ERC-8039 Enables Private & Efficient Account Logic

Architecture Overview

Implementation 1: zkSafe

Link: GitHub - MicrochainLabs/microchain-zk-signers · GitHub

Safe account with ZK proxy as owner

// Safe configuration

contract Safe {

address[] owners = [zkSignerProxy]; // Single owner: ZK proxy

uint256 threshold = 1;

}

// ZK Signer Proxy (uses ERC-8039)

contract ZKSignerProxy {

    IERC8039 public immutable verifier;  // ERC-8039 verifier

bytes32 public signersRoot;

constructor(address _verifier) {

        verifier = IERC8039(_verifier);

    }

// ERC-1271: Safe calls this to validate signatures

function isValidSignature(

bytes32 hash,

bytes memory signature

    ) external view returns (bytes4) {

// Prepare public inputs

bytes memory publicInputs = abi.encode(

            hash,

            stateRoot

        );

// Verify proof using ERC-8039

bytes4 result = verifier.verifyProof(publicInputs, signature);

// Convert ERC-8039 result to ERC-1271 magic value

if (result == 0x534f5876) {

return 0x1626ba7e; // ERC-1271 success

        }

return 0xffffffff; // ERC-1271 failure

    }

}

Flow:

1. User prepares Safe transaction off-chain

2. M signers sign transaction hash (private)

3. ZK proof generated: “M signatures valid”

4. Safe calls `zkSignerProxy.isValidSignature(txHash, zkProof)`

5. Proxy calls `verifier.verifyProof()` via ERC-8039

6. If proof valid → Safe executes transaction

Implementation 2: zkNexus (ERC-7579)

Link: GitHub - MicrochainLabs/microchain-zk-signers · GitHub

Biconomy Nexus with ZK validator module

// ZK Validator Module (uses ERC-8039)

contract ZKValidatorModule {

    IERC8039 public immutable verifier;  // ERC-8039 verifier

bytes32 public signersRoot;

constructor(address _verifier) {

        verifier = IERC8039(_verifier);

    }

// ERC-7579: EntryPoint calls this to validate UserOps

function validateUserOp(

UserOperation calldata userOp,

bytes32 userOpHash

    ) external returns (uint256) {

// Prepare public inputs

bytes memory publicInputs = abi.encode(

            userOpHash,

            signersRoot,

        );

// Verify proof using ERC-8039

bytes4 result = verifier.verifyProof(publicInputs, userOp.signature);

// Convert ERC-8039 result to ERC-7579 validation code

if (result == 0x534f5876) {

return VALIDATION_SUCCESS;

        }

return VALIDATION_FAILED;

    }

// Can run multiple validators with different proof systems!

    }

}

Flow:

1. User prepares UserOperation off-chain

2. M signers sign userOpHash (private)

3. ZK proof generated: “M signatures valid”

4. EntryPoint calls `zkValidatorModule.validateUserOp()`

5. Module calls `verifier.verifyProof()` via **ERC-8039**

6. If proof valid → EntryPoint executes UserOperation

  1. Proof-System Agnostic Design

With ERC-8039, we can use the adapter pattern to wrap proof-system-specific verifiers:

Examples

1. HONK Adapter (Noir/Barretenberg)

contract HonkProofVerifier is IERC8039 {

    HonkVerifier public immutable honkVerifier;

function verifyProof(

bytes calldata publicInputs,

bytes calldata proof

    ) external view returns (bytes4 magicValue) {

// Decode public inputs to bytes32[] (HONK format)

bytes32[] memory decodedInputs = abi.decode(publicInputs, (bytes32[]));

// Call native HONK verifier

bool isValid = honkVerifier.verify(proof, decodedInputs);

// Return ERC-8039 magic value

if (isValid) {

            magicValue = 0x534f5876;

        }

    }

function getProofType() external pure returns (bytes32) {

return keccak256("honk-barretenberg");

    }

function metadata() external pure returns (string memory) {

return "ZK MultiSig v1.0.0 - Private threshold signatures";

    }

}

2. Groth16 Adapter (Circom)

contract Groth16ProofVerifier is IERC8039 {

    IGroth16Verifier public immutable groth16Verifier;

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 to uint256[]

uint256[] memory inputs = abi.decode(publicInputs, (uint256[]));

// Call native Groth16 verifier

bool isValid = groth16Verifier.verifyProof(a, b, c, inputs);

// Return ERC-8039 magic value

if (isValid) {

            magicValue = 0x534f5876;

        }

    }

function getProofType() external pure returns (bytes32) {

return keccak256("groth16-circom");

    }

function metadata() external pure returns (string memory) {

return "ZK MultiSig v1.0.0 - Private threshold signatures";

    }

}

3. SP1 zkVM Adapter

contract SP1ProofVerifier is IERC8039 {

    ISP1Verifier public immutable sp1Verifier;

bytes32 public immutable programVKey;

function verifyProof(

bytes calldata publicInputs,

bytes calldata proof

    ) external view returns (bytes4 magicValue) {

// SP1 verifier reverts on failure, use try/catch

try sp1Verifier.verifyProof(programVKey, publicInputs, proof) {

// Success: proof valid

            magicValue = 0x534f5876;

        } catch {

// Failure: return 0x00000000 (default)

        }

    }

function getProofType() external pure returns (bytes32) {

return keccak256("sp1");

    }

function metadata() external pure returns (string memory) {

return "SP1 zkSafe v1.0.0 - Rust-based threshold signatures";

    }

}
  1. Security Considerations

Proof Replay Attacks

The application must include replay prevention in circuit design.

Account-Specific Binding

// Public inputs include account address

bytes memory publicInputs = abi.encode(

    messageHash,

    stateRoot,

address(this)  // Binds proof to this account

);

Nonce-Based Prevention

// Public inputs include incrementing nonce

bytes memory publicInputs = abi.encode(

    messageHash,

    signersRoot,

    stateRoot,

    nonce++  // Each proof uses unique nonce

);

Immutable Verifiers

// Make verifier immutable

contract ZKSignerProxy {

    IERC8039 public immutable verifier;  // Cannot be changed

constructor(address _verifier) {

        verifier = IERC8039(_verifier);

    }

}

Public Input Manipulation

Attacker modifies public inputs to make invalid proof appear valid.

  1. Ecosystem Impact

1. Standardized ZK Integration

Before ERC-8039:

// Every account needs custom integration

contract CustomAccount {

    HonkVerifier honkVerifier;

    Groth16Verifier groth16Verifier;

// ... custom verification logic for each system

}

With ERC-8039:

// Universal integration

contract StandardAccount {

    IERC8039 verifier;  // Works with ANY proof system

}

2. Ecosystem Tooling

Wallets & Interfaces:

// Universal proof verification check

interface IWallet {

async verifyProof(

verifierAddress: Address,

publicInputs: Bytes,

proof: Bytes

    ): Promise<boolean> {

const verifier = IERC8039__factory.connect(verifierAddress);

const result = await verifier.verifyProof(publicInputs, proof);

return result === "0x534f5876";

    }
}

SDKs & Libraries:

// Proof-system-agnostic SDK

class ProofVerifier {

async verify(verifier: IERC8039, publicInputs: any, proof: any) {

// Works with any ERC-8039 compliant verifier

const result = await verifier.verifyProof(

encodePublicInputs(publicInputs), proof);

return result === "0x534f5876";

    }

}