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
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:
Private Authentication: Prove signature validation without revealing signers, or thresholds
Privacy-Preserving Policies: Enforce spending limits, gas prices, time-locks keeping constraints hidden
Confidential Recovery: Account recovery with hidden guardians and mechanisms
Credential Verification: Prove age, identity, credit score without revealing data
Succinctness Use Cases:
Complex Policy Validation: Execute sophisticated rules off-chain, verify cheaply on-chain
Historical State Verification: Prove properties about past blockchain state (co-processor pattern)
Trust-Minimized Computation: Execute arbitrary logic off-chain with on-chain verification
Infrastructure**:**
Proof-System Agnostic: Switch or support multiple proof systems without changing account logic
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.
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:
- Private Authentication (Provable Ownership)
- Traditional: 3-of-5 multisig with public signers
- ZK Circuit: 3-of-5 multisig with hidden signers, weights, and threshold
- Privacy-Preserving Policies
- Traditional: Max spending $10k/day (public limit visible to competitors)
- ZK Circuit: Max spending $10k/day (private limit, corporate privacy preserved)
- Confidential Recovery
- Traditional: 2-of-3 guardians (public addresses, target for social engineering)
- ZK Circuit: 2-of-3 guardians (hidden identities, security through obscurity)
- 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:
- 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)
- 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
- 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:**
-
Account contracts tightly coupled to specific proof systems
-
Cannot upgrade proof systems without contract changes
-
Cannot support multiple proof systems
-
Each account type needs custom verifier integration
-
No ecosystem tooling can work across implementations
**With ERC-8039:**
-
Accounts work with any proof system
-
Upgrade proof systems via adapter contracts
-
Support multiple proof systems simultaneously
-
Unified integration pattern across all accounts
-
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
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)
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
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";
}
}
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. 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";
}
}


