--- eip: 8224 title: Counterfactual Transaction description: A transaction type for protocol-native shielded gas funding via canonical fee-note contracts author: Ben Adams (@benaadams) discussions-to: https://ethereum-magicians.org/t/eip-8224-counterfactual-transaction/28205 status: Draft type: Standards Track category: Core created: 2026-04-11 requires: 196, 197, 1559, 2718, 2780, 3529, 4788, 6780, 7708, 7904 --- ## Abstract This EIP introduces a new [EIP-2718](./eip-2718.md) 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 Transaction[^contract-payer-tx] 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 Transaction[^contract-payer-tx] 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: 1. 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. 2. The fresh smart contract account cannot pay gas - it has no ETH until the initial private action executes. 3. 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. 4. 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.[^stealth-guide] ### 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 Transaction[^contract-payer-tx] 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.[^contract-payer-tx] 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.[^contract-payer-tx] ### 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`.[^contract-payer-tx] 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](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174). ### 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](./eip-2718.md) 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](./eip-1559.md) with the following constraints: - `chain_id`, `nonce`, `max_priority_fee_per_gas`, `max_fee_per_gas`, `gas_limit`, and `value` are RLP-encoded unsigned integers interpreted as `uint256` values, matching the integer-width conventions of [EIP-1559](./eip-1559.md). - `to`, `value`, `data`, and `access_list` follow the same semantics as [EIP-1559](./eip-1559.md). `to` MAY be empty to indicate contract creation. - `value` MUST be `0`. The field is included for RLP compatibility with [EIP-1559](./eip-1559.md), 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_contract` is the address of the canonical fee-note contract instance from which gas funding is authorized. During validation, `EXTCODEHASH(fee_note_contract)` MUST equal `FEE_NOTE_CODE_HASH`. - `fee_nullifier_hash` is a 32-byte value that uniquely identifies the fee note used to pay gas. - `fee_root` is a 32-byte value that identifies the canonical fee-note Merkle root in `fee_note_contract` against which the proof is verified. - `fee_denomination` is the full ETH value of the fee note. It MUST be greater than or equal to `gas_limit * max_fee_per_gas`. - `gas_refund_recipient` is 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_data` is the serialized zero-knowledge proof for spending a fee note from `fee_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](./eip-2718.md) `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: 1. **Deposit ETH fee notes**: users deposit ETH commitments into a canonical fee-note tree. 2. **Protocol reads**: during validation, the protocol reads nullifier status and recognized Merkle roots from the contract instance named by `fee_note_contract`. 3. **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 = 3 ``` The 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: 1. Set `storage[CURRENT_ROOT_SLOT] = new_root`. 2. Set `storage[keccak256(abi.encode(new_root, ACCEPTED_ROOT_SLOT_BASE))] = 1`. 3. 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 ```solidity // 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](./eip-196.md) (point addition and scalar multiplication) and [EIP-197](./eip-197.md) (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](#post-quantum-considerations)). Let `BN254_SCALAR_MODULUS = 21888242871839275222246405745257275088548364400416034343698204186575808495617`. The canonical fee-note circuit is constrained as follows: - The Merkle tree depth is `32`, allowing `2^32` fee-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 this `EMPTY_LEAF`, not from `Poseidon(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_secret` and `trapdoor`. - Wallets and note-generating implementations MUST domain-separate `nullifier_secret` generation by `fee_note_contract` so the same secret material is not reused across different canonical fee-note contract instances. - `note_value` is 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`, and `sender` are encoded as their `uint160` address values lifted into a field element. - `chain_id`, `fee_denomination`, and `gas_commitment` are encoded as unsigned big-endian integers and MUST be strictly less than `BN254_SCALAR_MODULUS`; otherwise the transaction is invalid. - `fee_nullifier_hash` and `fee_root` are encoded as 32-byte big-endian integers and MUST be strictly less than `BN254_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: 1. The prover knows a valid fee note `F` in the canonical fee-note tree at `fee_root`. 2. `fee_note_contract` is the canonical fee-note contract instance whose root set and nullifier set authorize this spend. 3. `fee_nullifier_hash` is correctly derived from `F`. 4. `fee_denomination` is correctly derived from `F` and is strictly greater than zero. 5. `fee_denomination >= gas_commitment`. 6. `gas_refund_recipient` is correctly bound into the proof statement so unused gas budget cannot be redirected by a third party. 7. `sender` is correctly bound into the proof statement so a fee proof cannot be front-run by a different sender. 8. `chain_id` is 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: 1. Reads the canonical fee-note verification key defined by this EIP. 2. Constructs the public input vector from the transaction fields and recovered sender. 3. 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](./eip-196.md) and [EIP-197](./eip-197.md) 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](./eip-2780.md) 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](./eip-196.md) and [EIP-197](./eip-197.md) precompiles. Its gas cost is derived from those precompile costs as repriced by [EIP-7904](./eip-7904.md) 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](./eip-2780.md): | 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](./eip-196.md) | | `ECADD` | 10 | 314 | 3,140 | [EIP-7904](./eip-7904.md) | | `ECPAIRING` base | 1 | 45,000 | 45,000 | [EIP-197](./eip-197.md) | | `ECPAIRING` per pair | 2 | 34,103 | 68,206 | [EIP-7904](./eip-7904.md) | | **Proof verification subtotal** | | | **176,346** | | If [EIP-196](./eip-196.md) or [EIP-197](./eip-197.md) 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](./eip-8038.md) 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](./eip-8037.md) revises state creation costs, the `GAS_STORAGE_SET` value MUST track those revised costs. Under [EIP-8037](./eip-8037.md)'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](./eip-7708.md) and [EIP-2780](./eip-2780.md), 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_cost ``` The `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](./eip-7708.md) `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: 1. The signature is valid and the sender is recoverable. 2. `chain_id` matches the current chain and satisfies `chain_id < BN254_SCALAR_MODULUS`. 3. `max_fee_per_gas >= base_fee_per_gas`. 4. `max_fee_per_gas >= max_priority_fee_per_gas`. 5. `gas_limit >= intrinsic_gas`. The intrinsic gas for a counterfactual transaction is defined in [Intrinsic Gas](#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. 6. `nonce == sender.nonce`. 7. `value == 0`. 8. `fee_note_contract` is 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 against `FEE_NOTE_EMPTY_STORAGE_ROOT`. 9. `fee_note_contract != sender`. 10. If `to` is non-empty, `fee_note_contract != to`. 11. `gas_refund_recipient != 0x0000000000000000000000000000000000000000`. 12. `fee_nullifier_hash < BN254_SCALAR_MODULUS`. 13. `fee_root < BN254_SCALAR_MODULUS`. 14. `fee_denomination < BN254_SCALAR_MODULUS`. 15. `gas_commitment = gas_limit * max_fee_per_gas` does not overflow `uint256` and satisfies `gas_commitment < BN254_SCALAR_MODULUS`. 16. `fee_denomination > 0` and `fee_denomination >= gas_commitment`. 17. The fee nullifier is unspent in `fee_note_contract`: `fee_note_storage[nullifier_slot(fee_nullifier_hash)] == 0`. 18. `fee_note_contract.balance >= fee_denomination`. 19. The ZK proof verifies against the canonical fee-note verification key with the public inputs derived from the transaction fields and recovered sender. 20. The `fee_root` public input is recognized by `fee_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](#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.[^contract-payer-tx] Clients MUST revalidate counterfactual transactions when: - The fee-note storage slot for the transaction's `fee_nullifier_hash` changes. - The `fee_note_contract` account's code hash changes such that it no longer satisfies validation check 8. - The `fee_note_contract` account'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: 1. **Compute effective gas price**: Let `gas_commitment = gas_limit * max_fee_per_gas` and `effective_gas_price = base_fee_per_gas + min(max_priority_fee_per_gas, max_fee_per_gas - base_fee_per_gas)`. 2. **Consume fee note**: The protocol writes the exact value `1` to `fee_note_contract`'s nullifier slot for `fee_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. 3. **Fee-note release**: Transfer `fee_denomination` from `fee_note_contract.balance` to `sender.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 because `fee_note_contract.balance < fee_denomination` at 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. 4. **Fee escrow**: Deduct `fee_denomination` from `sender.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. 5. **Nonce increment**: Set `sender.nonce = sender.nonce + 1`. 6. **Execute**: Execute the transaction body using ordinary [EIP-1559](./eip-1559.md) semantics. The call frame's `msg.value` is `0` regardless of the sender's actual balance. If `to` is non-empty, perform a message call from `sender` to `to` with `data` and `gas_limit - intrinsic_gas` available gas, where `intrinsic_gas` is defined in [Intrinsic Gas](#intrinsic-gas). If `to` is empty, create a contract using the transaction's initcode. 7. **Fee-note refund**: Add `fee_denomination - gas_used * effective_gas_price` to `sender.balance`. `gas_used` here is the final gas consumed after applying the [EIP-3529](./eip-3529.md) gas refund (capped at `floor(gas_used_before_refund / 5)`, i.e. ordinary integer division rounded down). This returns both unused gas budget and any fee-note value above `gas_commitment`. 8. **Refund return**: Transfer `fee_denomination - gas_used * effective_gas_price` from `sender.balance` to `gas_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. 9. **Priority fee**: Add `gas_used * (effective_gas_price - base_fee_per_gas)` to the coinbase balance. 10. **Base fee burn**: `gas_used * base_fee_per_gas` is 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](./eip-7708.md) Transfer log as specified in [Interaction with EIP-7708](#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 Transaction[^contract-payer-tx]**: 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: 1. 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. 2. The recipient calls `authorize` on the Contract Payer Transaction payer registry at `0x13`, registering the sender EOA as its authorized controller.[^contract-payer-tx] 3. 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.[^contract-payer-tx] 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.[^contract-payer-tx] 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](./eip-7708.md) 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](./eip-7708.md) 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](./eip-4788.md). 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](./eip-1559.md) and [EIP-7708](./eip-7708.md) machinery. ### Interaction with EIP-7702 If the sender has an [EIP-7702](./eip-7702.md) 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,[^tornado] Semaphore,[^semaphore] and Zcash.[^zcash] 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.[^contract-payer-tx] 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](./eip-7503.md), 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 Transaction[^contract-payer-tx] 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](./eip-2718.md) 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,[^contract-payer-tx] 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 Transaction[^contract-payer-tx] 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](./eip-8037.md)'s increased state creation costs (which scale with the block gas limit set by [EIP-7935](./eip-7935.md)), 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](#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.[^poseidon] 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-196.md), [EIP-197](./eip-197.md)), 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](./eip-7904.md). 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](./eip-2930.md)), `0x02` ([EIP-1559](./eip-1559.md)), `0x03` ([EIP-4844](./eip-4844.md)), `0x04` ([EIP-7702](./eip-7702.md)), or `0x07` (the Contract Payer Transaction).[^contract-payer-tx] 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: 1. Counterfactual transaction envelope parsing and signing-hash support. 2. fflonk verification in the transaction validation path. 3. The canonical fee-note contract implementation, including note deposits and Poseidon-based Merkle tree updates. 4. 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: ```solidity 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 ```solidity /// @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: 1. Alice deposits 10 ETH into `PrivacyPool`, receiving a withdrawal commitment. Separately, she deposits a `0.01 ETH` fee note into a canonical fee-note contract instance `feeNoteContract`. 2. Time passes. Alice generates a fresh EOA keypair. 3. Alice constructs a counterfactual transaction: - `to` = `PrivacyPool` address - `data` = `withdraw(withdrawalProof, withdrawalNullifierHash, salt, freshEOA, 10 ether, keccak256(initCode))` - `recipient` is the CREATE2 address derived from `salt`, `PrivacyPool`, and `initCode = SmartAccount.creationCode ++ abi.encode(freshEOA)` - `fee_note_contract` = `feeNoteContract` - `proof_data` = fflonk proof for spending the canonical fee note - `fee_nullifier_hash` = her fee note's nullifier - `fee_root` = a recognized accepted root of the canonical fee-note tree - `fee_denomination` = `0.01 ether` - `gas_refund_recipient` = `recipient` (unused gas budget goes to the new smart account) - Signed by the fresh EOA 4. The mempool validates: `EXTCODEHASH(feeNoteContract) == FEE_NOTE_CODE_HASH`, fee-note proof OK, fee nullifier unspent, fee root recognized in `feeNoteContract`, `0.01 ether >= gas_limit * max_fee_per_gas`, and `feeNoteContract.balance` is sufficient. 5. A block builder includes the transaction. 6. The protocol consumes the fee nullifier, releases `0.01 ether` from `feeNoteContract` into fee escrow, and executes the call. 7. `PrivacyPool.withdraw` verifies the withdrawal proof, marks the withdrawal nullifier as spent, deploys `SmartAccount` via `CREATE2` with `10 ETH`, and runs the payable constructor. The `SmartAccount` constructor calls `PAYER_REGISTRY.authorize(freshEOA)`. 8. After execution, gas is settled: `gas_used * effective_gas_price` goes to the priority fee and burn, and the remainder of the fee note (`0.01 ether - gas_used * effective_gas_price`) is sent to `gas_refund_recipient`. 9. Result: `SmartAccount` holds 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 type `0x07`.[^contract-payer-tx] 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](./eip-7904.md) 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](#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](./eip-7935.md)'s 60M gas limit (with [EIP-7825](./eip-7825.md)'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](./eip-7732.md) (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](#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](#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](#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](./eip-6780.md) `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](./eip-6780.md) 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](#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. [^stealth-guide]: Vitalik Buterin, "An Incomplete Guide to Stealth Addresses", `https://vitalik.eth.limo/general/2023/01/20/stealth.html`. [^contract-payer-tx]: "Contract Payer Transaction" pull request, `https://github.com/ethereum/EIPs/pull/11509`. [^tornado]: Tornado Cash Whitepaper v1.4, `https://berkeley-defi.github.io/assets/material/Tornado%20Cash%20Whitepaper.pdf`. [^semaphore]: Semaphore protocol overview and proposal, `https://semaphore.pse.dev/`, `https://docs.zkproof.org/pages/standards/accepted-workshop3/proposal-semaphore.pdf`. [^zcash]: Zcash Protocol Specification, `https://zips.z.cash/protocol/protocol.pdf`. [^poseidon]: 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`. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).