Abstract
This EIP introduces a new EIP-2718 transaction type - the counterfactual transaction - for protocol-native shielded gas funding without requiring the controlling EOA to hold public ETH. Instead of charging gas to tx.sender or to some application-specific payer after the call completes, the transaction carries a zero-knowledge proof that authorizes gas payment from a dedicated ETH-denominated fee note in a canonical protocol fee-note system.
Validation requires no EVM code execution - only ZK proof verification (bounded cryptographic computation), a canonical code-hash check, and fixed storage reads for fee-nullifier and fee-root status - making it compatible with inclusion lists and stateless validation.
This EIP solves the gas-bootstrapping problem for transactions whose first call cannot safely rely on post-execution balances. The Contract Payer Transaction1 is one important companion use case, but not a protocol requirement of this transaction type. A dedicated fee note spend against a canonical fee-note contract provides a pre-execution, mempool-safe gas guarantee while preserving privacy. Any portion of that fee note not consumed by gas is intentionally returned as public ETH to gas_refund_recipient, making the fee note a bounded ETH exit path as well as a gas funding mechanism. This is what allows arbitrary applications, including third-party privacy pools of other assets, to use the transaction type: the application call can deliver asset X, while the fee note provides the ETH bootstrap.
No ETH is left stranded behind a spent fee nullifier: the fee note is fully consumed on execution, with the remainder sent to gas_refund_recipient.
Motivation
The Bootstrapping Problem
The Contract Payer Transaction1 enables a smart contract account to pay its own gas when called by an authorized EOA. After some initial transaction funds or configures a fresh smart contract account and the account calls authorize on the payer registry, the controlling EOA can operate gaslessly forever. No ETH funding transaction to the EOA is needed, so there is no on-chain link between the EOA and the private source of that initial funding.
However, the initial private transaction itself needs gas. This creates a circular dependency:
- The fresh EOA cannot pay gas - it has no ETH, and receiving ETH from any source creates a traceable on-chain link that defeats the privacy goal.
- The fresh smart contract account cannot pay gas - it has no ETH until the initial private action executes.
- The system has sufficient value to pay for gas, but the ETH needed for gas is still locked behind private authorization that has not yet been exercised on-chain.
- A third-party relayer paying gas leaks metadata (timing, IP, relayer identity) and requires trust in an external party.
The transaction can authorize enough ETH to cover gas, but under the current execution model, gas must be escrowed before execution begins. The value exists; it is simply locked behind an operation that has not yet been permitted to run.
This EIP is intended to solve that bootstrap problem for legitimate privacy-preserving onboarding and application interaction. Its purpose is to remove the forced on-chain linkage created when a fresh controlling EOA must first receive public ETH solely to pay gas, while preserving ordinary transaction execution semantics once the transaction begins.
The same linkage problem also appears in stealth-address workflows: a fresh stealth recipient can receive assets privately, but still needs a non-linkable way to pay for its first transaction.2
Counterfactual Validation as the Solution
This EIP resolves the circular dependency by separating application execution from gas authorization. The transaction includes a proof that the sender controls a dedicated fee note that is sufficient to pay up to gas_limit * max_fee_per_gas.
The fee note is consumed at the transaction-envelope level, so gas can be paid even if execution reverts. The application call itself still happens in ordinary EVM execution, preserving normal transaction semantics. Because the proof only reveals a nullifier and public transaction parameters, no information about the depositor's identity leaks through the gas payment mechanism beyond what the fee-note system already reveals.
The fee note is therefore not "gas-only" in the narrow sense. Its unused portion intentionally emerges as public ETH at gas_refund_recipient. That bounded ETH egress is a feature: it bootstraps the recipient account with ETH and also lets third-party privacy pools of non-ETH assets compose with this transaction type, because only the gas side must be ETH-denominated.
Relationship to Contract Payer Transaction
The Contract Payer Transaction1 is an optional companion for the smart-account lifecycle use case. Together, the two transaction types provide full-lifecycle privacy for smart contract accounts:
| Phase | Mechanism | Used |
|---|---|---|
| Initial funding | This EIP's counterfactual transaction | Once per account |
| Ongoing operation | Contract Payer Transaction | Every subsequent transaction |
For smart-account bootstrapping, the counterfactual transaction is needed only once - to bootstrap the smart contract account with ETH and register the controlling EOA in the Contract Payer Transaction payer registry.1 All subsequent transactions use standard sponsored transactions with static registry validation.
This also makes the transaction type useful beyond account bootstrapping and beyond ETH-only pools. Any application can be called using a fee note spend from a canonical fee-note contract for gas, and a third-party privacy pool can use this transaction type to withdraw some pool-specific asset while the fee note supplies the ETH bootstrap needed to enter the optional Contract Payer Transaction lifecycle if the application chooses to use it.1
Compatibility with Partial Statelessness (VOPS)
Under Validity-Only Partial Statelessness (VOPS), this EIP's validation reads are bounded to:
- One account lookup for the canonical fee-note contract instance referenced by the transaction, including its code hash and balance.
- One storage slot from that contract for the fee nullifier status.
- One known storage slot from that contract for the fee-note system's accepted Merkle root set.
- Cached canonical verification key data.
A VOPS-style node that opts into this transaction type would need to retain the storage trie for canonical fee-note contract instances it serves, in addition to the Contract Payer Transaction payer registry at 0x13.1 It would also need a cached one-time attestation that each referenced instance was first observed with the canonical pristine storage root FEE_NOTE_EMPTY_STORAGE_ROOT. Because the code hash, storage layout, and pristine storage root are fixed by this EIP, this remains a bounded exception of the same class as other canonicalized contracts.
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.
Parameters
| Parameter | Value |
|---|---|
COUNTERFACTUAL_TX_TYPE | 0x08 |
FEE_NOTE_CODE_HASH | keccak256(canonical fee-note runtime bytecode) |
FEE_NOTE_EMPTY_STORAGE_ROOT | Storage root of the canonical pristine fee-note instance state |
Counterfactual Transaction
A new EIP-2718 transaction is introduced where the TransactionType is COUNTERFACTUAL_TX_TYPE and the TransactionPayload is the RLP serialization of:
rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas,
gas_limit, to, value, data, access_list,
fee_note_contract, fee_nullifier_hash, fee_root, fee_denomination, gas_refund_recipient,
proof_data, y_parity, r, s])The fields follow the same semantics as EIP-1559 with the following constraints:
chain_id,nonce,max_priority_fee_per_gas,max_fee_per_gas,gas_limit, andvalueare RLP-encoded unsigned integers interpreted asuint256values, matching the integer-width conventions of EIP-1559.to,value,data, andaccess_listfollow the same semantics as EIP-1559.toMAY be empty to indicate contract creation.valueMUST be0. The field is included for RLP compatibility with EIP-1559, but counterfactual transactions MUST NOT carry an ETH value transfer from the sender. The sender has no public ETH; ETH movement occurs only through application execution and, if applicable, the protocol's post-execution gas-budget refund.fee_note_contractis the address of the canonical fee-note contract instance from which gas funding is authorized. During validation,EXTCODEHASH(fee_note_contract)MUST equalFEE_NOTE_CODE_HASH.fee_nullifier_hashis a 32-byte value that uniquely identifies the fee note used to pay gas.fee_rootis a 32-byte value that identifies the canonical fee-note Merkle root infee_note_contractagainst which the proof is verified.fee_denominationis the full ETH value of the fee note. It MUST be greater than or equal togas_limit * max_fee_per_gas.gas_refund_recipientis the address that receives unused gas budget from the fee note after execution as a real public ETH transfer. It MUST NOT be the zero address.proof_datais the serialized zero-knowledge proof for spending a fee note fromfee_note_contract. Its format is fixed by this EIP's canonical proof system, and it MUST use the single canonical byte encoding published in the reference implementation. Alternative encodings of the same proof are invalid.
The nonce is the sender's nonce, incremented as normal. The sender is recovered from the signature (y_parity, r, s) using standard ECDSA recovery over the signing hash:
keccak256(COUNTERFACTUAL_TX_TYPE || rlp([chain_id, nonce, max_priority_fee_per_gas,
max_fee_per_gas, gas_limit, to, value, data, access_list,
fee_note_contract, fee_nullifier_hash, fee_root, fee_denomination, gas_refund_recipient, proof_data]))This signing hash binds the full transaction body, including to, data, nonce, and access_list. Those application-execution fields are intentionally not ZK public inputs; they are authorized by the ECDSA signature rather than by the fee-note proof.
The EIP-2718 ReceiptPayload for this transaction is rlp([status, cumulative_transaction_gas_used, logs_bloom, logs]).
Canonical Fee Note Contract
This EIP does not assign a fixed address to the canonical fee-note contract. Instead, a contract instance is recognized by its runtime code hash plus a one-time initial-state attestation. A fee_note_contract address is canonical for this EIP if and only if EXTCODEHASH(fee_note_contract) == FEE_NOTE_CODE_HASH and the instance has been observed on chain in the exact pristine storage state defined by this EIP.
The canonical fee-note contract serves three functions:
- Deposit ETH fee notes: users deposit ETH commitments into a canonical fee-note tree.
- Protocol reads: during validation, the protocol reads nullifier status and recognized Merkle roots from the contract instance named by
fee_note_contract. - Protocol writes: during execution, the protocol marks fee nullifiers as spent in that contract's state.
Any application MAY be targeted by a counterfactual transaction. The protocol is only aware of the canonical ETH fee-note system. Application-specific private state transitions, such as privacy-pool withdrawals, remain ordinary EVM execution and are not protocol-registered.
Code hash alone is not sufficient because constructor logic can pre-populate storage while still returning the canonical runtime bytecode. To prevent malicious deployers from seeding fraudulent accepted roots or other hidden state, clients MUST treat a fee-note contract instance as canonical only after verifying that, immediately after creation and before any subsequent state-changing interaction, the account's storage root is exactly FEE_NOTE_EMPTY_STORAGE_ROOT. An instance first observed on chain with any other initial storage root is permanently non-canonical and MUST NOT be used for counterfactual transactions.
Storage Layout
Every canonical fee-note contract instance uses the following fixed storage regions:
CURRENT_ROOT_SLOT = 0
NEXT_LEAF_INDEX_SLOT = 1
NULLIFIER_SLOT_BASE = 2
ACCEPTED_ROOT_SLOT_BASE = 3The spent flag for a given fee_nullifier_hash is located at:
storage[keccak256(abi.encode(fee_nullifier_hash, NULLIFIER_SLOT_BASE))]A nullifier slot value of zero means unspent; the exact value 1 means spent. No other value is valid.
An accepted root flag is stored at:
storage[keccak256(abi.encode(root, ACCEPTED_ROOT_SLOT_BASE))]Let leaf_index_before be the value of storage[NEXT_LEAF_INDEX_SLOT] immediately before a successful deposit appends a new commitment. After recomputing the new Merkle root new_root, the contract MUST:
- Set
storage[CURRENT_ROOT_SLOT] = new_root. - Set
storage[keccak256(abi.encode(new_root, ACCEPTED_ROOT_SLOT_BASE))] = 1. - Set
storage[NEXT_LEAF_INDEX_SLOT] = leaf_index_before + 1.
The reference implementation MUST publish canonical deployment bytecode that initializes storage[CURRENT_ROOT_SLOT] to the canonical empty-tree root derived from this EIP's canonical depth-32 Poseidon Merkle tree and initializes storage[keccak256(abi.encode(empty_root, ACCEPTED_ROOT_SLOT_BASE))] to 1. That empty-tree root MUST be derived from the domain-separated constant EMPTY_LEAF = uint256(keccak256("EIP_8224_EMPTY_LEAF_V1")) mod BN254_SCALAR_MODULUS, not from Poseidon(0, 0, 0) or any all-zero leaf initialization. All other accepted-root slots MUST be zero until written by deposits. The reference implementation MUST also publish the resulting pristine account storage root as FEE_NOTE_EMPTY_STORAGE_ROOT.
A root is recognized if and only if storage[keccak256(abi.encode(root, ACCEPTED_ROOT_SLOT_BASE))] != 0.
Accepted roots are append-only. Once a root is accepted, later deposits MUST NOT revoke it. No alternative accepted-root storage layout or lookup algorithm is permitted.
Contract Interface
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.28;
/// @title Fee Notes - EIP-8224
/// @notice Canonical code-hash-recognized contract for ETH-denominated fee notes.
/// Users deposit ETH commitments here; the protocol validates
/// and spends fee notes for counterfactual transactions.
contract FeeNotes {
event Deposited(bytes32 indexed commitment, uint256 amount);
/// @notice Deposit ETH into the canonical fee-note tree.
/// @param commitment The fee-note commitment appended to the tree.
function deposit(bytes32 commitment) external payable;
/// @notice Returns true if `root` is in the append-only accepted root set.
function isKnownRoot(bytes32 root) external view returns (bool);
/// @notice Returns true if `nullifierHash` has been spent.
function isSpent(bytes32 nullifierHash) external view returns (bool);
/// @notice Returns the current Merkle root.
function currentRoot() external view returns (bytes32);
/// @notice Returns the total number of deposited notes.
/// This is intended for wallet and frontend privacy warnings.
function noteCount() external view returns (uint256);
}Deposits use arbitrary ETH values. deposit(commitment) MUST revert unless msg.value > 0. A canonical fee-note contract instance MUST append commitments sequentially to a single Merkle tree, update NEXT_LEAF_INDEX_SLOT, recompute the current root, and mark that root as accepted in the exact append-only accepted-root set defined above. deposit MUST revert if NEXT_LEAF_INDEX_SLOT >= 2^32 (tree full). The deposited msg.value is the note's committed value. This EIP does not require on-chain commitment uniqueness checks: if two deposits use the same commitment, they represent the same underlying note material and one of the resulting notes may become permanently unspendable after the first spend consumes the shared nullifier. Wallets and note-generating implementations MUST therefore use cryptographically secure randomness for both nullifier_secret and trapdoor. Implementations SHOULD expose enough information for wallets and frontends to estimate overall and value-cluster anonymity-set size, for example via noteCount and off-chain indexing.
ZK Proof Verification
Proof System
This draft uses fflonk over the BN254 (alt_bn128) curve as its current canonical proof system. Verification uses the same elliptic curve arithmetic as EIP-196 (point addition and scalar multiplication) and EIP-197 (pairing check). This bounds verification to a fixed number of curve operations regardless of application complexity while retaining compatibility with the existing BN254 precompiles.
Future revisions of this EIP may replace the canonical proof system before finalization. After activation, any such change would require a hard fork (see Post-Quantum Considerations).
Let BN254_SCALAR_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617.
The canonical fee-note circuit is constrained as follows:
- The Merkle tree depth is
32, allowing2^32fee-note leaves. - Commitments and Merkle internal nodes use Poseidon over the BN254 scalar field.
- The canonical empty leaf is the fixed domain-separated field element
EMPTY_LEAF = uint256(keccak256("EIP_8224_EMPTY_LEAF_V1")) mod BN254_SCALAR_MODULUS. The empty tree and all default empty-subtree hashes are derived from thisEMPTY_LEAF, not fromPoseidon(0, 0, 0)or any all-zero leaf initialization. - A fee note is represented by
(note_value, nullifier_secret, trapdoor). - The note commitment is
Poseidon(note_value, nullifier_secret, trapdoor). - The nullifier hash is
Poseidon(nullifier_secret). - Wallets and note-generating implementations MUST use cryptographically secure randomness for both
nullifier_secretandtrapdoor. - Wallets and note-generating implementations MUST domain-separate
nullifier_secretgeneration byfee_note_contractso the same secret material is not reused across different canonical fee-note contract instances. note_valueis the ETH amount deposited into the selected fee-note contract instance for this note.- The circuit constrains
note_value > 0. - The circuit constrains
fee_denomination == note_value, proving the public input matches the note's committed value. - The circuit proves a correct Merkle path from the note commitment to
fee_root. - The exact Poseidon instance used by this EIP, including state width, round counts, MDS matrix, and round constants, is consensus-critical. All implementations MUST use the single canonical Poseidon parameterization published in the reference implementation for this EIP.
Equivalent algebraic formulations are not permitted: all implementations MUST use this exact commitment, nullifier, and Merkle-hash scheme.
Public Input Encoding
All fflonk public inputs are single BN254 scalar-field elements. Clients MUST construct them as follows:
fee_note_contract,gas_refund_recipient, andsenderare encoded as theiruint160address values lifted into a field element.chain_id,fee_denomination, andgas_commitmentare encoded as unsigned big-endian integers and MUST be strictly less thanBN254_SCALAR_MODULUS; otherwise the transaction is invalid.fee_nullifier_hashandfee_rootare encoded as 32-byte big-endian integers and MUST be strictly less thanBN254_SCALAR_MODULUS; otherwise the transaction is invalid.
gas_commitment is computed as the exact integer product gas_limit * max_fee_per_gas. If this multiplication overflows uint256 or yields a value greater than or equal to BN254_SCALAR_MODULUS, the transaction is invalid.
Public Inputs
The ZK proof MUST commit to the following public inputs:
| Index | Public Input | Description |
|---|---|---|
| 0 | fee_note_contract | The canonical fee-note contract instance (matches tx.fee_note_contract) |
| 1 | fee_nullifier_hash | The fee nullifier (matches tx.fee_nullifier_hash) |
| 2 | fee_root | A recent Merkle root of the canonical fee-note tree (matches tx.fee_root) |
| 3 | fee_denomination | The full fee-note denomination (matches tx.fee_denomination) |
| 4 | gas_commitment | Equal to gas_limit * max_fee_per_gas from the transaction |
| 5 | gas_refund_recipient | The address receiving unused gas budget |
| 6 | sender | The recovered transaction sender |
| 7 | chain_id | The transaction chain id (matches tx.chain_id) |
Proof Statement
The proof demonstrates, without revealing the note's private witness values or depositor identity:
- The prover knows a valid fee note
Fin the canonical fee-note tree atfee_root. fee_note_contractis the canonical fee-note contract instance whose root set and nullifier set authorize this spend.fee_nullifier_hashis correctly derived fromF.fee_denominationis correctly derived fromFand is strictly greater than zero.fee_denomination >= gas_commitment.gas_refund_recipientis correctly bound into the proof statement so unused gas budget cannot be redirected by a third party.senderis correctly bound into the proof statement so a fee proof cannot be front-run by a different sender.chain_idis correctly bound into the proof statement so a fee proof cannot be replayed across chains even if all other public inputs match.
The point is not application-specific solvency. It is that the transaction carries an independently spendable ETH fee authorization whose minimum value is known to the protocol before execution begins.
Verification Procedure
During validation, the protocol:
- Reads the canonical fee-note verification key defined by this EIP.
- Constructs the public input vector from the transaction fields and recovered sender.
- Performs fflonk verification using BN254 pairing arithmetic.
gas_commitment is not a transaction field. The verifier computes it as gas_limit * max_fee_per_gas when constructing the public input vector.
proof_data decoding and BN254 point validation are consensus-critical parts of step 3. Implementations MUST apply the same malformed-input acceptance rules as the EIP-196 and EIP-197 precompiles for protocol-level verification: off-curve points, invalid field elements, points at infinity where not permitted by the canonical verifier format, incorrect subgroup membership, malformed lengths, and any other invalid proof encoding MUST cause verification to fail cleanly and the transaction to be rejected as invalid. They MUST NOT crash the client, trigger undefined behavior, or produce implementation-dependent acceptance results.
This is bounded cryptographic computation with no EVM code execution and no state-dependent branching.
Intrinsic Gas
The intrinsic gas for a counterfactual transaction includes the cost of protocol-level proof verification and fee-note state operations, in addition to the standard EIP-2780 base transaction cost. Each component is derived from the prevailing gas schedule for the corresponding primitive, so the transaction's gas consumption reflects the actual computational and state-access work performed by the network.
The proof verification uses the same BN254 elliptic curve arithmetic as the EIP-196 and EIP-197 precompiles. Its gas cost is derived from those precompile costs as repriced by EIP-7904 to target 60 Mgas/s throughput. Although the verification is performed at the protocol level rather than through EVM precompile calls, the gas charged MUST reflect the equivalent precompile cost so that counterfactual transactions pay for the computation they impose on the network.
Base Transaction Validation
These costs are shared with all transaction types per EIP-2780:
| Primitive | Gas | Description |
|---|---|---|
ECRECOVER | 3,000 | ECDSA signature recovery |
STATE_UPDATE | 1,000 | Sender nonce write |
COLD_ACCOUNT_COST_NOCODE | 500 | Sender account access |
| Base subtotal | 4,500 |
fflonk Proof Verification
For the canonical fflonk verifier with 8 public inputs, verification requires computing a batched opening linear combination with 10 scalar multiplications and 10 point additions, followed by a 2-pair pairing check. The gas cost is derived from the BN254 precompile schedule:
| Primitive | Count | Unit Cost | Gas | Source |
|---|---|---|---|---|
ECMUL | 10 | 6,000 | 60,000 | EIP-196 |
ECADD | 10 | 314 | 3,140 | EIP-7904 |
ECPAIRING base | 1 | 45,000 | 45,000 | EIP-197 |
ECPAIRING per pair | 2 | 34,103 | 68,206 | EIP-7904 |
| Proof verification subtotal | 176,346 |
If EIP-196 or EIP-197 precompile gas costs are revised by a future repricing EIP, the proof verification component of the intrinsic gas MUST track those revised costs.
Fee-Note State Access
These protocol-level reads occur during transaction validation. They are charged at the prevailing cold storage and account access costs:
| Primitive | Gas | Description |
|---|---|---|
GAS_COLD_SLOAD | 2,100 | Nullifier status read |
GAS_COLD_SLOAD | 2,100 | Accepted-root status read |
GAS_COLD_ACCOUNT_ACCESS | 2,600 | fee_note_contract code-hash and balance check |
| State access subtotal | 6,800 |
If EIP-8038 revises cold storage and account access costs, these values MUST track those revised costs.
Fee-Note State Mutation
These protocol-level state changes occur during execution and are NOT reverted if the application call reverts:
| Primitive | Gas | Description |
|---|---|---|
GAS_STORAGE_SET | 20,000 | Nullifier mark spent (0 → 1) |
STATE_UPDATE | 1,000 | fee_note_contract balance debit |
COLD_ACCOUNT_COST_NOCODE | 500 | gas_refund_recipient settlement access |
STATE_UPDATE | 1,000 | gas_refund_recipient settlement credit |
| State mutation subtotal | 22,500 |
If EIP-8037 revises state creation costs, the GAS_STORAGE_SET value MUST track those revised costs. Under EIP-8037's multidimensional gas metering, the nullifier write's state gas component follows the GAS_STORAGE_SET decomposition defined by that EIP.
ETH Transfer Logs
Per EIP-7708 and EIP-2780, each protocol-level settlement transfer log uses the LOG3 cost for 3 indexed topics and 32 bytes of data (375 + 375 × 3 + 8 × 32 = 1,756 gas):
| Primitive | Gas | Description |
|---|---|---|
| Transfer log | 1,756 | Fee-note release (execution step 3) |
| Transfer log | 1,756 | Refund return (execution step 8) |
| Transfer log subtotal | 3,512 |
Total Intrinsic Gas
The total counterfactual transaction intrinsic gas is:
intrinsic_gas = base_cost + proof_verification + state_access
+ state_mutation + transfer_logs
+ calldata_cost + access_list_cost
= 4,500 + 176,346 + 6,800 + 22,500 + 3,512
+ calldata_cost + access_list_cost
= 213,658 + calldata_cost + access_list_costThe proof_data field is included in the RLP-encoded transaction payload and contributes to calldata_cost at the standard per-byte rate. At approximately 512 bytes, proof_data contributes approximately 8,192 gas to calldata_cost (assuming predominantly non-zero bytes).
The refund-settlement access, balance credit, and transfer log are charged unconditionally as part of the fixed intrinsic gas. When fee_denomination - gas_used * effective_gas_price == 0, the refund settlement remains part of the protocol-defined transaction lifecycle, leaves balances unchanged, and emits a zero-amount settlement log in the EIP-7708 Transfer format.
The fee note covers this cost naturally: since fee_denomination >= gas_limit * max_fee_per_gas (validation check 16) and gas_limit >= intrinsic_gas (validation check 5), the fee note's value must be sufficient to cover the full intrinsic gas including proof verification. A counterfactual transaction with no application calldata or access list requires a minimum gas_limit of approximately 222,000. At representative max_fee_per_gas values:
max_fee_per_gas | Minimum note value |
|---|---|
| 5 gwei | ~0.00111 ETH |
| 20 gwei | ~0.00444 ETH |
| 50 gwei | ~0.0111 ETH |
Any pool-specific tiering or wallet-side note sizing policy is outside consensus. The protocol only requires the chosen note value to cover the transaction's gas commitment.
Transaction Validation
A counterfactual transaction is valid if and only if all of the following hold:
- The signature is valid and the sender is recoverable.
chain_idmatches the current chain and satisfieschain_id < BN254_SCALAR_MODULUS.max_fee_per_gas >= base_fee_per_gas.max_fee_per_gas >= max_priority_fee_per_gas.gas_limit >= intrinsic_gas. The intrinsic gas for a counterfactual transaction is defined in Intrinsic Gas and includes the base transaction cost, fflonk proof verification cost, fee-note state access and mutation costs, and ETH transfer log costs, in addition to calldata and access list costs.nonce == sender.nonce.value == 0.fee_note_contractis a canonical fee-note contract instance: it is a contract account,EXTCODEHASH(fee_note_contract) == FEE_NOTE_CODE_HASH, and the instance has previously passed the required initial-state attestation againstFEE_NOTE_EMPTY_STORAGE_ROOT.fee_note_contract != sender.- If
tois non-empty,fee_note_contract != to. gas_refund_recipient != 0x0000000000000000000000000000000000000000.fee_nullifier_hash < BN254_SCALAR_MODULUS.fee_root < BN254_SCALAR_MODULUS.fee_denomination < BN254_SCALAR_MODULUS.gas_commitment = gas_limit * max_fee_per_gasdoes not overflowuint256and satisfiesgas_commitment < BN254_SCALAR_MODULUS.fee_denomination > 0andfee_denomination >= gas_commitment.- The fee nullifier is unspent in
fee_note_contract:fee_note_storage[nullifier_slot(fee_nullifier_hash)] == 0. fee_note_contract.balance >= fee_denomination.- The ZK proof verifies against the canonical fee-note verification key with the public inputs derived from the transaction fields and recovered sender.
- The
fee_rootpublic input is recognized byfee_note_contract, meaning it is in that contract's append-only accepted root set.
If any check fails, the transaction is invalid and MUST NOT be included in a block.
Implementations SHOULD perform validation checks in ascending cost order and SHOULD run the ZK proof verification in check 19 last, after all cheaper format, range, nonce, code-hash, value, nullifier, balance, and root-recognition checks have succeeded.
Check 18 is also an inclusion-time consensus condition. Block builders MUST re-check that fee_note_contract.balance >= fee_denomination immediately before inclusion. If execution reaches step 3 of Transaction Execution and that transfer cannot complete because the contract balance is insufficient, the block is invalid.
The protocol does not require the sender or any application-specific recipient to hold ETH before execution. Gas safety comes from the fee note proved in check 19, not from pre-funded public balances. The fee note is intentionally not limited to fee settlement only: any unused portion may emerge as public ETH at gas_refund_recipient.
Mempool Rules
Clients MUST NOT hold more than one counterfactual transaction per fee_nullifier_hash in the transaction pool. If a new counterfactual transaction arrives with a fee_nullifier_hash that already has a pending transaction, the client SHOULD replace the existing transaction only if the new transaction has strictly higher max_fee_per_gas and max_priority_fee_per_gas.
Clients MUST NOT hold more than one counterfactual transaction per recovered sender address in the transaction pool, consistent with the Contract Payer Transaction constraint on sponsored transactions.1
Clients MUST revalidate counterfactual transactions when:
- The fee-note storage slot for the transaction's
fee_nullifier_hashchanges. - The
fee_note_contractaccount's code hash changes such that it no longer satisfies validation check 8. - The
fee_note_contractaccount's balance decreases such that it may no longer satisfy validation check 18. Balance increases MUST NOT trigger revalidation, because they cannot invalidate balance sufficiency. - The sender's nonce changes.
Clients MUST NOT accept or forward a counterfactual transaction referencing a fee_note_contract instance that has not yet passed the required initial-state attestation. Implementations SHOULD track canonicalized fee-note instances as part of chain state processing: when a contract account with EXTCODEHASH == FEE_NOTE_CODE_HASH is first created, the client SHOULD verify that its immediate post-creation storage root equals FEE_NOTE_EMPTY_STORAGE_ROOT before allowing later counterfactual transactions to reference it.
Because accepted roots are append-only, deposits that add new roots do not invalidate pending transactions that reference older accepted roots.
Fee-nullifier changes are O(1): each fee nullifier maps to at most one pending transaction. Balance changes are not O(1), because they can invalidate multiple pending transactions that depend on the same fee_note_contract balance. Clients SHOULD therefore track pending transactions per fee_note_contract and index them by required fee_denomination (or an equivalent threshold ordering) so balance-triggered revalidation can stop once remaining balance is sufficient for the rest of the queue.
Transaction Execution
Execution of a valid counterfactual transaction proceeds as follows:
- Compute effective gas price: Let
gas_commitment = gas_limit * max_fee_per_gasandeffective_gas_price = base_fee_per_gas + min(max_priority_fee_per_gas, max_fee_per_gas - base_fee_per_gas). - Consume fee note: The protocol writes the exact value
1tofee_note_contract's nullifier slot forfee_nullifier_hash, using the same slot computation as validation check 17. This state change is part of transaction-level fee settlement and is NOT reverted if the EVM call later reverts. - Fee-note release: Transfer
fee_denominationfromfee_note_contract.balancetosender.balance. This settlement transfer is a direct protocol-level balance adjustment. It MUST NOT invoke recipient code. This releases the fee note's full value. If this transfer cannot complete becausefee_note_contract.balance < fee_denominationat execution time, the block is invalid. This is not a transaction-level revert path and MUST NOT be handled by partially executing or reverting only step 2. - Fee escrow: Deduct
fee_denominationfromsender.balance. This locks the full fee-note value so the fee-note funds are never visible to the sender during execution. No EVM-observable state may exist between steps 3 and 4. - Nonce increment: Set
sender.nonce = sender.nonce + 1. - Execute: Execute the transaction body using ordinary EIP-1559 semantics. The call frame's
msg.valueis0regardless of the sender's actual balance. Iftois non-empty, perform a message call fromsendertotowithdataandgas_limit - intrinsic_gasavailable gas, whereintrinsic_gasis defined in Intrinsic Gas. Iftois empty, create a contract using the transaction's initcode. - Fee-note refund: Add
fee_denomination - gas_used * effective_gas_pricetosender.balance.gas_usedhere is the final gas consumed after applying the EIP-3529 gas refund (capped atfloor(gas_used_before_refund / 5), i.e. ordinary integer division rounded down). This returns both unused gas budget and any fee-note value abovegas_commitment. - Refund return: Transfer
fee_denomination - gas_used * effective_gas_pricefromsender.balancetogas_refund_recipient.balance. This settlement transfer is a direct protocol-level balance adjustment. It MUST NOT invoke recipient code. This settlement step is unconditional: when the amount is zero it leaves balances unchanged but still occurs for intrinsic-gas accounting and log-emission purposes. - Priority fee: Add
gas_used * (effective_gas_price - base_fee_per_gas)to the coinbase balance. - Base fee burn:
gas_used * base_fee_per_gasis burned.
Steps 3-4 and 7-8 are each atomic: no EVM-observable state may exist between the two steps of either pair. The sender's balance is transiently increased (step 3) then immediately decreased (step 4), so the fee-note funds are never visible to the sender during execution. (If the sender already holds ETH from other sources, that pre-existing balance is unaffected and remains available during execution.) Similarly, the refund (step 7) is immediately forwarded (step 8). The net effect is: fee_denomination is deducted from the canonical fee-note contract instance selected by fee_note_contract, gas_used * effective_gas_price goes to gas settlement (priority fee and burn), and the remainder goes to gas_refund_recipient. The two cross-account settlement transfers - fee-note release (step 3, fee_note_contract -> sender) and refund return (step 8, sender -> gas_refund_recipient) - are direct protocol-level balance adjustments and MUST NOT invoke recipient code. They each emit an EIP-7708 Transfer log as specified in Interaction with EIP-7708, including a zero-amount refund-return log when the settlement amount is zero. Steps 4 and 7 are balance adjustments on the sender's own account and do not emit Transfer logs.
Critical difference from the Contract Payer Transaction1: In sponsored transactions, gas funding uses effective_gas_price and unused gas is returned to the payer. Here, the protocol releases the full fee_denomination, uses gas_used * effective_gas_price for settlement, and sends the remainder to gas_refund_recipient, not back to fee_note_contract. The fee note is fully consumed; the selected fee-note contract instance does not retain the spread.
Execution failure semantics: If the call or creation in step 6 reverts, the application effects are reverted as usual, but steps 2-4 (fee-note consumption and fee escrow) are NOT reverted and gas is still charged. This matches the normal Ethereum rule that the gas payer remains liable even when execution fails. The selected fee-note contract instance loses fee_denomination regardless of call outcome; the refund recipient receives the unused portion. By contrast, insufficient balance at step 3 is not an execution failure that the transaction may absorb: it is a consensus failure and makes the block invalid.
Interaction with Contract Payer Transaction
If a counterfactual transaction bootstraps a smart contract account:
- The recipient smart contract account holds the application-delivered asset(s) and, if chosen as
gas_refund_recipient, any unused ETH from the fee note. - The recipient calls
authorizeon the Contract Payer Transaction payer registry at0x13, registering the sender EOA as its authorized controller.1 - All subsequent transactions from the EOA to the smart contract account use Contract Payer Transactions (
0x07) with standard pre-execution gas escrow and registry validation.1
In that bootstrap case, the counterfactual transaction is needed only once. After the account is funded and registered in the payer registry, the full Contract Payer Transaction lifecycle takes over.1 Counterfactual transactions remain available for any later call that wants protocol-native shielded gas funding instead of contract-payer sponsorship.
Interaction with EIP-7708
EIP-7708 requires that all ETH transfers emit logs. The two new settlement transfers introduced by this EIP - the fee-note release (step 3, fee_note_contract -> sender) and the refund return (step 8, sender -> gas_refund_recipient) - MUST each emit exactly one EIP-7708 Transfer log:
| Field | Fee-note release log (step 3) | Refund return log (step 8) |
|---|---|---|
address | SYSTEM_ADDRESS | SYSTEM_ADDRESS |
topics[0] | keccak256('Transfer(address,address,uint256)') | keccak256('Transfer(address,address,uint256)') |
topics[1] | fee_note_contract | sender |
topics[2] | sender | gas_refund_recipient |
data | fee_denomination | fee_denomination - gas_used * effective_gas_price |
Where SYSTEM_ADDRESS is 0xfffffffffffffffffffffffffffffffffffffffe as defined by EIP-4788.
The fee-note release log is emitted before any logs produced by the transaction call, and the refund return log is emitted after all execution logs. This ordering is consensus-critical. If the refund amount is zero, the refund return log still MUST be emitted with zero data so the transaction's settlement lifecycle and intrinsic-gas accounting remain fixed. All other logs produced by the transaction body are emitted by the standard EIP-1559 and EIP-7708 machinery.
Interaction with EIP-7702
If the sender has an EIP-7702 delegation designator, the delegation is resolved as normal for code execution purposes. The ZK proof verification and fee-note-based gas authorization are independent of the sender's delegation status.
Rationale
What is the architectural prior art?
The fee-note construction follows the established commitment-tree plus nullifier pattern used by earlier privacy systems such as Tornado Cash,3 Semaphore,4 and Zcash.5 This EIP applies that pattern narrowly to gas funding and bootstrap liquidity, rather than trying to enshrine a general-purpose private transfer system.
That narrow scope is intentional. The protocol validates only a bounded fee-note spend against a canonical code-hash-recognized contract interface and its gas-funding semantics. It does not validate arbitrary privacy-pool logic, arbitrary note systems, or arbitrary post-state solvency claims. Application-specific private state transitions remain ordinary contract execution. This keeps the consensus surface smaller than broader protocol-native privacy-transfer proposals while still addressing a concrete and otherwise unresolved bootstrap problem.
Why a dedicated fee note instead of charging the recipient or post-state application balance?
The public mempool needs a pre-execution answer to "who can definitely pay for worst-case gas?" A proof that some application recipient will have ETH after execution is not sufficient, because gas must still be payable if the call reverts. A dedicated fee note gives the protocol a static, pre-execution gas authorization that does not rely on post-state balances.
This also reduces privacy risk relative to sending ETH to the controlling EOA just to fund gas. The fee note is simply another spend from the selected fee-note contract instance's note set. It authorizes gas without requiring any prior public funding transaction to the EOA.
Why canonical protocol-native shielded gas funding instead of per-application registration?
The fee lane should be uniform even when the application logic is not. By making fee authorization canonical and protocol-native, this EIP avoids requiring each privacy pool or application contract to register proof systems, nullifier layouts, or validation metadata with the protocol.
That separation is the main composability win: the protocol validates only the fee-note gas authorization against a canonical contract interface, while the transaction body can perform any ordinary call or contract creation. A privacy pool withdrawal becomes just one application of the transaction type rather than the thing the protocol is directly validating.
Why canonical code-hash recognition instead of a fixed predeploy?
This EIP needs a canonical fee-note interface and implementation, but it does not need a canonical address. Recognizing fee-note contract instances by exact runtime code hash keeps the protocol's validation surface bounded while avoiding a fork-time requirement for the protocol to install bytecode at a particular address.
Code hash alone is not enough, because constructor logic can return the canonical runtime bytecode while seeding fraudulent storage. For that reason, this EIP also requires a one-time initial-state attestation against the canonical pristine storage root FEE_NOTE_EMPTY_STORAGE_ROOT. The protocol still depends on one exact fee-note implementation, one exact storage layout, one exact pristine initial state, and one exact proof system. The difference is that the protocol recognizes deployed instances of that implementation rather than creating one itself. The transaction names the instance through fee_note_contract, and the proof binds that address so the same proof cannot be replayed against a different recognized instance.
This keeps the consensus rules precise without making the protocol responsible for deploying or naming a single globally reserved fee-note address, while still preventing constructor-time storage seeding attacks.
Why allow the fee note to emerge as public ETH?
This EIP intentionally allows the unused portion of the fee note to emerge as public ETH at gas_refund_recipient. That makes the fee note more than a pure gas-payment artifact: it is also an ETH release path whose public value is the note's public fee_denomination.
This is pragmatic for bootstrapping. If the refund recipient is the new smart account, the same transaction both performs the private application call and leaves the account with some public ETH for future operation. It also enables third-party privacy pools of other assets: the application logic can release asset X, while the fee note supplies the ETH needed to bootstrap the account into the Contract Payer Transaction sponsorship model.1
This path is intentionally ETH-only. It does not provide a generic protocol withdrawal mechanism for arbitrary assets; those remain the responsibility of the called application's own execution logic. The design goal is bootstrap liquidity for subsequent operation rather than general-purpose asset withdrawal. Because fee_denomination is public, the amount of ETH emerging through the refund path is transparent even though the fee-note source remains shielded. How large those notes should be, and whether implementations should cap them for privacy or UX reasons, is left to pool and wallet policy rather than fixed by consensus.
This also distinguishes the design from broader privacy-transfer proposals such as EIP-7503, which aim at protocol-native private ETH transfer semantics rather than narrowly scoped mempool-safe gas funding.
Why ZK proofs instead of payer registry authorization?
The Contract Payer Transaction1 uses the payer registry for authorization: registry[tx.to] == sender. This requires the payer contract to have previously called authorize(sender). That works well for persistent smart accounts, but not for shielded fee sources where revealing the authorization relationship on-chain defeats the privacy goal. A ZK proof provides authorization without revealing which fee-note deposit is being spent. The proof replaces the registry lookup with a cryptographic check that is equally static from the protocol's perspective.
Why not let the application recipient "pay at the end"?
That would reintroduce the bootstrap problem. The recipient has no ETH before execution, so a public-mempool validator or block builder would have no unconditional guarantee of payment if the call fails. The fee note solves this cleanly by making gas payment independent of whether the application call itself succeeds.
Why a separate transaction type?
A dedicated EIP-2718 type allows the sender to explicitly signal shielded fee-note semantics. Without this, the protocol would need heuristics to determine whether a transaction should use ordinary sender-paid gas, Contract Payer Transaction sponsorship,1 or protocol-native shielded gas funding. The separate type also allows mempool rules (per-nullifier deduplication) that are specific to counterfactual transactions.
Why per-nullifier mempool deduplication?
Each gas authorization has a unique fee nullifier. Once that nullifier is spent, the corresponding proof cannot be replayed. Deduplicating by fee nullifier ensures O(1) revalidation when the relevant state changes.
Why fund gas at max_fee_per_gas instead of effective_gas_price?
The Contract Payer Transaction1 transfers gas_limit * effective_gas_price from the payer, letting the spread between max_fee_per_gas and effective_gas_price remain with the payer. This makes sense for ongoing sponsorship where the payer is a persistent smart account that wants to minimize gas costs. For a fee note, the situation is different: the note is a one-time deposit being fully consumed. The user chose a note value to cover worst-case gas; any portion not needed for gas should flow to the refund recipient, not remain stranded in the selected canonical fee-note contract instance. Releasing the full fee_denomination ensures the entire fee note value is accounted for: gas costs go to settlement, and the remainder goes to gas_refund_recipient.
How does the anonymity set bootstrap?
Recognized fee-note contract instances have a real cold-start problem: early depositors have weak privacy because the available note set is initially small. This EIP does not solve that at the protocol layer.
Reasonable bootstrap paths include:
- privacy-pool frontends depositing fee notes alongside ordinary pool deposits,
- wallets opportunistically creating fee notes during normal user activity,
- or application-specific onboarding flows that create fee notes ahead of the first counterfactual transaction.
Users SHOULD treat small anonymity sets as weak privacy rather than assuming recognized fee-note contract instances are private by default from genesis. Wallets and frontends SHOULD surface per-instance and value-pattern privacy warnings when the available crowd is too small for the user's stated privacy goals.
What is the deposit cost tradeoff?
The fee-note contract must update a Poseidon-based Merkle tree on each deposit. Without a dedicated Poseidon precompile, this is expected to be materially more expensive than a simple ETH transfer.
For small notes, deposit gas overhead may be a significant fraction of the note's value. For a depth-32 Poseidon tree, a deposit requires 32 storage writes for Merkle path updates. Under EIP-8037's increased state creation costs (which scale with the block gas limit set by EIP-7935), these writes become progressively more expensive - approximately 20% more at a 60M gas limit compared to the current GAS_STORAGE_SET of 20,000. Combined with the proof verification cost included in Intrinsic Gas, very small notes may be economically unattractive on L1 at elevated gas prices. This is a practical tradeoff of protocol-native shielded gas funding. A future hard fork MAY introduce a Poseidon precompile or other optimization if deposit cost proves too high in practice.
Poseidon is used because it is efficient inside zero-knowledge circuits relative to conventional hash functions, even though it is comparatively expensive to compute in today's EVM without a dedicated precompile.6
This EIP deliberately leaves note sizing policy to pools and wallets. While a fee note can be used as a shielded ETH exit via the refund path, that is not its primary purpose, and implementations SHOULD discourage uniquely identifying note values that unnecessarily weaken privacy.
Why fflonk over BN254?
fflonk keeps verification on the existing BN254 precompiles (EIP-196, EIP-197), so no new cryptographic primitives are required. With chain_id bound into the proof, the verifier uses a 2-pair pairing check plus 10 scalar multiplications and 10 point additions, for 176,346 gas of proof verification under EIP-7904. Verification cost remains bounded and independent of application complexity, making it suitable for mempool validation where predictable cost is essential.
Compared with the earlier Groth16 draft, the operational tradeoff is still favorable: proof size grows to roughly 512 bytes, but total intrinsic gas for a minimal transaction still lands at only about 222,000 gas once calldata is included. That keeps required note values within practical ranges while reducing verifier cost materially.
For this transaction type, proof size and verifier cost matter more than prover throughput. Every full node must validate every counterfactual transaction, and every proof is carried inside a transaction. That makes compact proofs and cheap verification unusually important.
fflonk also uses a universal setup. The fee-note circuit can therefore reuse an existing BN254 powers-of-tau transcript instead of requiring a circuit-specific ceremony for this EIP. If the circuit changes, new proving and verification keys can be derived against the same universal setup rather than repeating a bespoke Phase 2 ceremony.
PLONK was also considered. It shares the same universal-setup advantage, but for this circuit shape it appears to give larger proofs and slightly higher verification gas than fflonk. Groth16 remains attractive when absolute minimum proof size is the only goal, but that advantage is outweighed here by the circuit-specific trusted-setup burden and the lack of a meaningful verifier-cost win.
Transparent systems such as STARKs were also considered. Their main advantages are no trusted setup and a stronger long-term post-quantum story. Their main disadvantage for this EIP is proof size: carrying tens of kilobytes of proof data in every counterfactual transaction would impose substantial bandwidth and mempool overhead. Absent new precompiles or a different protocol design, they are not the best initial fit for this transaction type.
Hybrid designs such as STARK-in-SNARK were not chosen. For a small fixed circuit like the fee-note spend, they add substantial prover complexity without preserving the main benefits usually sought from transparent proofs. In particular, the outer SNARK still determines compact verification and still reintroduces trusted-setup or non-post-quantum assumptions depending on the wrapper used.
Backwards Compatibility
This EIP introduces a new transaction type and a canonical code-hash-recognized fee-note contract interface. It does not modify the behavior of existing transaction types.
The COUNTERFACTUAL_TX_TYPE value 0x08 does not conflict with any finalized transaction types: 0x01 (EIP-2930), 0x02 (EIP-1559), 0x03 (EIP-4844), 0x04 (EIP-7702), or 0x07 (the Contract Payer Transaction).1
No existing application contract is required to register with the protocol in order to be targeted by a counterfactual transaction.
This EIP does not define an ordinary non-counterfactual withdrawal path for canonical fee notes. A future hard fork that deprecates or replaces this transaction type MUST preserve spendability of existing fee notes through an equivalent replacement transaction type or an explicit migration path; it MUST NOT strand already-deposited notes.
Reference Implementation
Advancing this EIP to cross-client review requires publication of the canonical fee-note deployment bytecode, canonical runtime bytecode, canonical runtime code hash, the canonical pristine storage root FEE_NOTE_EMPTY_STORAGE_ROOT, the canonical verification key, the canonical circuit artifacts, the canonical Poseidon parameterization and constants, the canonical proof_data byte encoding, and executable cross-client test vectors. Those test vectors MUST include nullifier-slot and accepted-root-slot derivation vectors, canonical and non-canonical proof-encoding vectors, and malformed BN254 proof-input vectors. These are consensus-critical, not optional implementation details.
Until those exact artifacts and cross-client test vectors are published, this EIP remains incomplete and MUST NOT advance beyond draft status.
Client implementations need four non-trivial components:
- Counterfactual transaction envelope parsing and signing-hash support.
- fflonk verification in the transaction validation path.
- The canonical fee-note contract implementation, including note deposits and Poseidon-based Merkle tree updates.
- The protocol storage-write path that marks fee nullifiers as spent during execution.
These items SHOULD be tracked explicitly in devnet planning and cross-client implementation matrices before the EIP is considered ready for mainnet review.
Fee Note Contract
To be provided - the canonical deployment bytecode, runtime bytecode, runtime code hash, pristine storage root, verification key, canonical proof-data encoding, and slot-computation / malformed-proof test vectors for the fee-note contract implementation recognized by this EIP.
Example Privacy Pool Integration
A privacy pool can support counterfactual transactions without any protocol registration. It just exposes its normal withdrawal entrypoint:
interface IPayerRegistry {
function authorize(address sender) external;
}
IPayerRegistry constant PAYER_REGISTRY = IPayerRegistry(0x0000000000000000000000000000000000000013);
contract PrivacyPool {
mapping(bytes32 => bool) public nullifiers;
/// @notice Process a withdrawal. Called via counterfactual transaction.
/// The protocol verifies only the fee-note proof. The contract
/// verifies its own withdrawal proof and state transition.
function withdraw(
bytes calldata withdrawalProof,
bytes32 withdrawalNullifierHash,
bytes32 salt,
address initialController,
uint256 amount,
bytes32 expectedRecipientCodeHash
) external {
require(!nullifiers[withdrawalNullifierHash], "Nullifier already spent");
require(_verifyWithdrawalProof(withdrawalProof, withdrawalNullifierHash, salt, initialController, amount), "Invalid proof");
nullifiers[withdrawalNullifierHash] = true;
bytes memory initCode = abi.encodePacked(type(SmartAccount).creationCode, abi.encode(initialController));
require(keccak256(initCode) == expectedRecipientCodeHash, "Unexpected initcode");
address deployed;
assembly {
deployed := create2(amount, add(initCode, 0x20), mload(initCode), salt)
}
require(deployed != address(0), "Create2 failed");
}
function _verifyWithdrawalProof(
bytes calldata, /* withdrawalProof */
bytes32, /* withdrawalNullifierHash */
bytes32, /* salt */
address, /* initialController */
uint256 /* amount */
) internal view returns (bool) {
revert("Example only: proof verifier not implemented");
}
receive() external payable {}
}Example Bootstrap Flow
/// @notice A minimal illustrative smart contract account deployed by the privacy pool
/// during withdrawal. Calls authorize in its constructor to enable
/// Contract Payer Transaction sponsored transactions immediately.
/// @dev This example is not production-safe: it has no ownership transfer,
/// no recovery flow, and no upgrade or governance mechanism.
contract SmartAccount {
address public controller;
constructor(address initialController) payable {
controller = initialController;
// Register the controller for Contract Payer Transaction sponsored transactions.
// After this, the controller EOA never needs ETH again.
PAYER_REGISTRY.authorize(initialController);
}
function execute(address target, uint256 val, bytes calldata data) external {
require(msg.sender == controller, "Not controller");
(bool ok,) = target.call{value: val}(data);
require(ok, "Execution failed");
}
receive() external payable {}
}The full bootstrap flow:
- Alice deposits 10 ETH into
PrivacyPool, receiving a withdrawal commitment. Separately, she deposits a0.01 ETHfee note into a canonical fee-note contract instancefeeNoteContract. - Time passes. Alice generates a fresh EOA keypair.
- Alice constructs a counterfactual transaction:
to=PrivacyPooladdressdata=withdraw(withdrawalProof, withdrawalNullifierHash, salt, freshEOA, 10 ether, keccak256(initCode))recipientis the CREATE2 address derived fromsalt,PrivacyPool, andinitCode = SmartAccount.creationCode ++ abi.encode(freshEOA)fee_note_contract=feeNoteContractproof_data= fflonk proof for spending the canonical fee notefee_nullifier_hash= her fee note's nullifierfee_root= a recognized accepted root of the canonical fee-note treefee_denomination=0.01 ethergas_refund_recipient=recipient(unused gas budget goes to the new smart account)- Signed by the fresh EOA
- The mempool validates:
EXTCODEHASH(feeNoteContract) == FEE_NOTE_CODE_HASH, fee-note proof OK, fee nullifier unspent, fee root recognized infeeNoteContract,0.01 ether >= gas_limit * max_fee_per_gas, andfeeNoteContract.balanceis sufficient. - A block builder includes the transaction.
- The protocol consumes the fee nullifier, releases
0.01 etherfromfeeNoteContractinto fee escrow, and executes the call. PrivacyPool.withdrawverifies the withdrawal proof, marks the withdrawal nullifier as spent, deploysSmartAccountviaCREATE2with10 ETH, and runs the payable constructor. TheSmartAccountconstructor callsPAYER_REGISTRY.authorize(freshEOA).- After execution, gas is settled:
gas_used * effective_gas_pricegoes to the priority fee and burn, and the remainder of the fee note (0.01 ether - gas_used * effective_gas_price) is sent togas_refund_recipient. - Result:
SmartAccountholds the withdrawn ETH plus the unused portion of the fee note. The fresh EOA is authorized in the payer registry. All future transactions use Contract Payer Transaction type0x07.1
The same structure works for third-party privacy pools of other assets. In that case, step 7 would transfer asset X to SmartAccount, while the fee note still provides the ETH bootstrap via step 8.
Security Considerations
Mempool Denial of Service
The canonical fflonk proof verification in a counterfactual transaction requires 10 ECMUL, 10 ECADD, and one 2-pair ECPAIRING - equivalent to 176,346 gas at the EIP-7904 60 Mgas/s throughput target, or approximately 2.8-5.3 ms wall-clock time on current hardware. This cost is included in the transaction's intrinsic gas and paid by the fee note for included transactions.
However, mempool validation of invalid counterfactual transactions imposes the same CPU cost without gas payment. Implementations SHOULD perform all cheaper prechecks (format, range, nonce, code hash, value, nullifier, balance, and root-recognition checks 1-18 and 20) before running proof verification in check 19.
At EIP-7935's 60M gas limit (with EIP-7825's 16M per-transaction cap), a worst-case block of minimal counterfactual transactions contains approximately 270 transactions at ~222,000 gas each, requiring roughly 270 × 4.0 ms ≈ 1.08 seconds of proof verification. Over a 12-second slot this implies a steady-state validation rate of approximately 22 transactions per second, consuming about 90 ms of CPU per second. Implementations SHOULD rate-limit counterfactual transaction proof verifications per peer, but per-peer throttling alone is insufficient because a Sybil attacker can spread load across many peers. Implementations SHOULD therefore also enforce a global node-level CPU budget for unconfirmed counterfactual proof verification, independent of peer count, and defer or drop excess unverifiable traffic once that budget is exhausted. Peers that send counterfactual transactions at a high rate and repeatedly fail local validation, including proof verification, SHOULD be disconnected or otherwise penalized by the peer-scoring system. Clients SHOULD NOT forward or gossip a counterfactual transaction until they have locally completed the required validation, including proof verification in check 19. The per-sender and per-nullifier mempool caps provide additional natural bounds, but they do not eliminate the need for global proof-verification budgeting, front-door throttling of unique invalid proofs, and peer penalties for repeated invalid counterfactual transaction spam.
If EIP-7732 (ePBS) is active, execution validation is deferred from the attestation window to the next slot, giving validators 6-9 seconds for full block validation. Block builders SHOULD self-regulate counterfactual transaction inclusion based on their available proof-verification budget.
Nullifier Frontrunning
Between mempool acceptance and block inclusion, the fee nullifier could be spent by a different transaction, or the selected fee-note contract instance's balance could fall below fee_denomination. Either condition invalidates inclusion. Block builders MUST therefore re-check both fee-nullifier state and balance sufficiency at inclusion time. The nullifier risk is bounded: each fee nullifier can be spent exactly once, so at most one counterfactual transaction per fee nullifier can be invalidated. Balance insufficiency is also consensus-critical: if step 3 of Transaction Execution cannot complete because the contract balance is insufficient, the block is invalid.
Fee Authorization Consumption
The fee nullifier is consumed in step 2 of Transaction Execution, outside normal EVM rollback semantics, so that gas remains payable even if the call in step 6 reverts. This is intentional, but it means users MUST treat the fee note as irrevocably spent once the transaction is included, regardless of whether the application call itself succeeds. The unused portion of the fee note is still refunded to gas_refund_recipient, so the user's loss is bounded to gas_used * effective_gas_price, not the full fee_denomination.
Proof Malleability
The ZK proof binds to the transaction's public inputs (fee_note_contract, fee_nullifier_hash, fee_root, fee_denomination, gas_commitment, gas_refund_recipient, sender, and chain_id). If any of those values changes, the proof becomes invalid. Implementations MUST verify that the public inputs derived from the transaction and recovered sender match those committed to in the proof, to prevent an attacker from reusing a valid proof with modified transaction parameters, replaying it against a different canonical fee-note contract instance, or replaying it on another chain.
This binding is intentionally asymmetric. The ZK proof authenticates only the fee-authorization fields, while ordinary transaction fields such as to, data, nonce, and access_list are authenticated only by the ECDSA signature over the full transaction payload. This is a design choice, not an omission: the protocol proves that a valid fee note authorizes gas for the recovered sender, and the sender's signature separately authorizes the application call that consumes that gas.
Privacy Leakage via Gas
The gas used by a counterfactual transaction is publicly visible. If different private actions lead to meaningfully different gas consumption, gas usage could leak information about the underlying application state transition. Applications that care about privacy SHOULD keep gas cost as independent as possible from private amounts and branches.
Canonical Circuit Trust
The canonical fee-note verification key determines what fee proofs the protocol accepts. If the circuit or verification key is wrong, invalid fee-note spends could be accepted or valid fee-note spends rejected. The fee-note circuit and verification key are therefore consensus-critical artifacts and MUST be audited and published canonically as part of this EIP.
Because the current draft proof system is fflonk, the fee-note system depends on the integrity of the chosen universal BN254 trusted setup. If that setup's toxic waste is compromised, an attacker may be able to forge proofs and extract ETH from canonical fee-note contract instances without valid deposits. The setup transcript selection, verifier-generation procedure, and verification method are therefore security-critical parts of the deployment process.
Any change to the fee-note circuit under fflonk still requires new proving and verification keys, but it does not require a new circuit-specific ceremony so long as the same universal BN254 setup remains acceptable. This reduces operational risk during iteration, but the circuit and Poseidon parameterization are still consensus-critical and must be stabilized before the EIP can progress beyond draft review.
Protocol-level verification MUST also preserve the failure semantics of the underlying BN254 precompile model. Malformed proof_data is a normal invalid-transaction condition, not an exceptional execution path. Client implementations therefore need explicit negative test vectors covering malformed proof encodings, off-curve points, forbidden points at infinity, and subgroup failures so these cases deterministically reject rather than causing client crashes or consensus splits.
Privacy Leakage via Fee Notes
A separate fee note introduces another spent nullifier that can be correlated with the transaction. This is preferable to publicly funding the controlling EOA, but it is still an additional observable artifact. Deposit-side privacy is not provided by the canonical fee-note contract itself: deposit(...) is a normal public transaction that reveals the depositor address and note value. The privacy property is that later spending reveals only the nullifier and public transaction bindings, not the link back to a particular deposit. In this design, the fee note is also an intentional bounded public ETH exit path via gas_refund_recipient, so users and applications should treat it as part of the overall privacy surface rather than as invisible fee plumbing. Canonical fee-note contract instances SHOULD maintain large anonymity sets, and users SHOULD avoid deterministic fee-note selection patterns. Wallets MUST domain-separate nullifier-secret generation by fee_note_contract; reusing the same nullifier_secret across different canonical fee-note contract instances produces the same fee_nullifier_hash, which is tracked independently by consensus but creates avoidable cross-instance linkage. Wallets MUST also use cryptographically secure randomness for both nullifier_secret and trapdoor; otherwise duplicate commitments can occur and later deposits may be effectively locked once the shared nullifier is spent.
Protocol-Level Storage Writes
Step 2 of Transaction Execution requires the protocol to write directly to the selected canonical fee-note contract instance's nullifier storage. This is still consensus-critical. Implementations must ensure this write path is bounded (one storage slot, deterministic location, exact written value 1), gated by the exact FEE_NOTE_CODE_HASH, and cannot be abused to corrupt unrelated contract state.
Force-Fed ETH
ETH can be forced into fee_note_contract by mechanisms such as coinbase payment or SELFDESTRUCT from another account. Such ETH is not associated with any fee note and cannot be withdrawn except indirectly through later valid fee-note spends that already satisfy this EIP's proof and root-recognition rules. This can create stranded balance, but it does not break soundness: force-fed ETH increases available balance and therefore cannot invalidate validation check 18 or authorize spending without a valid proof.
Code-Hash Recognition and EIP-6780
The safety of runtime code-hash recognition assumes the post-EIP-6780 SELFDESTRUCT semantics used on Ethereum mainnet. Without that restriction, metamorphic-contract patterns can weaken code-hash-based identity assumptions. This EIP therefore assumes EIP-6780 semantics and MUST NOT be activated on chains that do not enforce equivalent SELFDESTRUCT restrictions.
Interaction with Base Fee Volatility
Because the fee note commits to a public fee_denomination, while actual gas is charged at gas_used * effective_gas_price, the consumed fee authorization is conservative by construction. Unused fee-note value is refunded to gas_refund_recipient.
Balance-Churn Revalidation
Changes to fee_note_contract.balance can invalidate pending counterfactual transactions if the remaining balance falls below their fee_denomination. See Mempool Rules for the per-contract value ordering approach that bounds this churn.
Post-Quantum Considerations
BN254 and fflonk are not post-quantum secure. A sufficiently capable quantum adversary could break the underlying discrete-log assumptions and forge proofs. This is not an immediate practical risk, but it is a real long-term limitation.
Before finalization, future revisions of this EIP may replace the current draft proof system with a different canonical proof system. After activation, the proof system, verification key, and canonical recognized fee-note contract implementation could be replaced only by a future hard fork.
Such a hard fork can preserve existing fee notes only if it keeps the same note commitment, nullifier, and Merkle semantics, or defines an explicit migration path for legacy notes. Changing the verifier alone is compatible with existing notes; changing the note model is not automatically compatible.
Copyright
Copyright and related rights waived via CC0.
Footnotes
-
"Contract Payer Transaction" pull request,
https://github.com/ethereum/EIPs/pull/11509. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16 ↩17 -
Vitalik Buterin, "An Incomplete Guide to Stealth Addresses",
https://vitalik.eth.limo/general/2023/01/20/stealth.html. ↩ -
Tornado Cash Whitepaper v1.4,
https://berkeley-defi.github.io/assets/material/Tornado%20Cash%20Whitepaper.pdf. ↩ -
Semaphore protocol overview and proposal,
https://semaphore.pse.dev/,https://docs.zkproof.org/pages/standards/accepted-workshop3/proposal-semaphore.pdf. ↩ -
Zcash Protocol Specification,
https://zips.z.cash/protocol/protocol.pdf. ↩ -
Lorenzo Grassi, Dmitry Khovratovich, Christian Rechberger, Arnab Roy, Markus Schofnegger, "Poseidon: A New Hash Function for Zero-Knowledge Proof Systems",
https://eprint.iacr.org/2019/458. ↩
