FOCIL 🤝 Native Account Abstraction

by Thomas Thiery

Thanks to Jihoon Song, Julian Ma, Toni Wahrstätter and Vitalik Buterin for their feedback and comments.

TL;DR: FOCIL checks omitted inclusion list (IL) transactions using a cheap nonce/balance proxy against post-state. This works for EOAs but not for native AA, where transaction validity requires executing VERIFY frames. This write-up defines a public mempool eligible FrameTx subset with a stricter omission check: bounded VERIFY execution against post-state, as well as a per-IL VERIFY-gas budget to cap attester work.

Context

After executing a block, FOCIL (EIP-7805) checks each IL transaction that was not included: if it fits in the remaining gas and passes a nonce/balance check against the post-state, the block fails IL conditions and attesters will not vote for it.

This proxy is cheap (one account lookup per excluded transaction, no execution) and works well for EOAs and EIP-7702, where omission conflicts surface through sender nonce and balance.

This proposal does not change FOCIL’s behavior for EOAs or EIP-7702 transactions. Everything below applies only to Frame Transactions (EIP-8141) in the eligible subset defined next.

Eligible FrameTxs

This note draws a hard line between two classes of FrameTxs.

Eligible FrameTxs are the subset that is simultaneously considered valid for the public mempool and FOCIL-enforceable. The eligible subset is drawn from the public-mempool-eligible set by design: any FrameTx unsafe for broad propagation is also unsafe for FOCIL enforcement, but the converse does not hold. All other FrameTxs can still be broadcast in opt-in alternative mempools or private orderflow channels; their omission is always excused.

This mirrors the broader AA ecosystem. ERC-4337 uses an alternative mempool; ERC-7562 defines stricter shared-mempool rules alongside separate alt-mempool rules. The same principle applies here: FOCIL enforcement only covers FrameTxs already cleared for broad public propagation.

Why native AA breaks the proxy

Unlike EOAs, Frame Transaction validity cannot be determined from sender state alone. VERIFY frames must execute against state and call APPROVE: the payer can only be discovered during this execution and can differ from the sender. This means nonce and balance checks can pass while VERIFY fails. There is no cheap proxy: attesters must replay the validation prefix to determine whether an excluded eligible FrameTx should have been included.

Conflicts through shared infrastructure

AA also introduces conflicts not visible from transaction bytes alone. Consider TX_A and TX_B both validating against the same sponsor:

// Pseudocode; APPROVE is an EVM opcode (0xaa per EIP-8141)
function validateSponsorship() {
    require(address(this).balance >= MIN_RESERVE)
    APPROVE(payment)
}

If TX_A is included and fees are collected, the sponsor’s balance may drop below MIN_RESERVE, making TX_B invalid at post-state. This resolves naturally under a post-state rule: anything invalid at the final post-state is excused. A builder could also deliberately mount a censorship attack by draining a sponsor to excuse many omissions at once; this costs block resources and is inherent to any post-state mechanism. The bounded state access rule below (constraint 5) limits this attack surface.

More broadly, shared contract storage could allow a single included transaction to render many excluded FrameTxs invalid at post-state, creating a systematic mass-omission excuse; constraint 5 rules this out.

Eligibility constraints

An eligible FrameTx is a FrameTx satisfying all of constraints 1–5 below. Constraints 1–4 are static checks evaluated at Tier 1; constraint 5 is a runtime check evaluated during Tier 3 execution. A FrameTx is eligible only if it passes all five.

Constraints split into two kinds:

Static checks are checkable from the frame list without any state access or execution. They are enforced at Tier 1.

Runtime checks cannot be evaluated statically. They are enforced as ineligibility conditions during Tier 3 VERIFY execution. A tx hitting an ineligibility condition is excused.

VERIFY gas weight

verify_gas(tx) = ÎŁ frame.gas_limit  for all frames with frame.mode == VERIFY

Computable from the frame list without any execution.

Parameters (initial, tunable)

  • MAX_VERIFY_GAS_PER_FRAMETX (e.g., 100,000)
  • MAX_VERIFY_GAS_PER_INCLUSION_LIST (e.g., 250,000)

Note: With these parameters, PQ signature verification (Falcon, Dilithium) exceeds MAX_VERIFY_GAS_PER_FRAMETX and would not be FOCIL-eligible. PQ FrameTxs could still propagate via alternative channels at first, and their omission would always be excused.

Static checks (Tier 1)

1. Validation-prefix ordering. VERIFY frames must precede all DEFAULT and SENDER frames. The only exception is contract creation frames (to == null ), which may appear before VERIFY frames to deploy the contract being validated. This ensures attesters only need to execute creation and VERIFY frames to determine validity, never regular execution frames

2. Bounded VERIFY gas per transaction. verify_gas(tx) ≤ MAX_VERIFY_GAS_PER_FRAMETX . Transactions exceeding this cap are unrestricted; omitting them is excused.

3. Per-inclusion-list VERIFY budget. For each IL, scan transactions in declared order and build a budgeted subset: include a candidate FrameTx only if adding it keeps the running verify_gas sum at or below MAX_VERIFY_GAS_PER_INCLUSION_LIST . An eligible FrameTx not in the budgeted subset of any IL is excused.

The same FrameTx may appear in multiple ILs at different positions and may be budgeted by some but not others. Attesters must evaluate all ILs and treat a FrameTx as budgeted if at least one IL budgets it. This provides a censorship resistance property: a single honest committee member who lists a FrameTx within budget is sufficient to make it enforceable, regardless of how other members order it.

4. Basic tx sanity. Chain ID match, max_priority_fee_per_gas ≤ max_fee_per_gas , max_fee_per_gas ≥ block.base_fee , parse validity, no blobs (blob_versioned_hashes empty, max_fee_per_blob_gas zero).

Runtime checks (Tier 3 ineligibility conditions)

5. Bounded state access. VERIFY may only read sender and payer account state (balance, nonce, code) and their first N storage slots, where N is in the range 2–4 (exact value to be benchmarked). Reads beyond this render the transaction ineligible.

Mempool nodes replay VERIFY at admission without executing a full block. Bounding reads to account state plus a small fixed number of storage slots means every node knows exactly what to fetch: a small, deterministic set, without being exposed to arbitrary storage lookups on demand. The constraint lives at the eligibility layer so that mempool admission and FOCIL enforcement stay in sync on the same eligible set.

The N-slot bound is chosen to align with AA-VOPS, which extends VOPS by having nodes cache a small number of storage slots per account. Most smart wallets store their nonce, owner, and guardian in slots 0–3, so N slots is enough to cover the common validation pattern. The exact value of N will be fixed when AA-VOPS is fully specified.

Recommended mempool policies (non-consensus)

Per-sender pending cap: at most one pending restricted FrameTx per sender (or a bounded nonce chain of depth D), mirroring EIP-7702.

Per-payer exposure tracking: track cumulative worst-case fee exposure per payer (tx_gas_limit × max_fee_per_gas ) and evict lowest-priority transactions when it exceeds state[payer].balance × α for some safety factor α < 1 .

Deterministic validation environment: reject FrameTxs whose VERIFY accesses volatile block-environment opcodes (TIMESTAMP , NUMBER , COINBASE , BLOCKHASH , etc.). This is a mempool admission rule, not a FOCIL eligibility constraint: attesters replay VERIFY against a fixed post-state where these opcodes are deterministic. Mempool nodes enforce this by tracing VERIFY on admission, following EIP-7562’s approach.

Building

For eligible FrameTxs, the nonce/balance proxy is replaced with bounded replay of the EIP-8141 validation prefix against post-state S. If an excluded eligible FrameTx is valid at S and fits within gas_left , the block does not satisfy IL conditions.

After building the payload, the builder iteratively appends remaining eligible IL FrameTxs:

  1. Try to append each remaining eligible IL FrameTx.
  2. If any succeeded, repeat from step 1 with updated state.
  3. Stop when a full round appends nothing.

This is O(k²) in the number of excluded eligible IL FrameTxs. Sequential nonces from the same sender are handled naturally: TX_A (nonce N) is appended first, then TX_B (nonce N+1) matches in a later pass.

The loop guarantees a fixed point: no omitted eligible FrameTx is valid at the final post-state. This is the correct target; greedy maximal packing is not required, and the fixed-point property is sufficient for full censorship resistance.

This is not a mandatory algorithm. Builders may use any strategy achieving the same result.

Engine API. EIP-7805 already proposes IL-related changes to newPayload and forkchoiceUpdated . With FrameTx-aware checking, the EL additionally needs the full ordered non-equivocating IL view to run Tier 1–3 logic. This is a refinement of the existing dependency and requires explicit coordination between the two EIPs.

Building optimizations (non-normative). Pre-order same-sender FrameTxs by nonce. Group FrameTxs sharing a payer and select against that payer’s budget, reducing the loop to a small number of passes in typical conditions.

Verification

Two properties are tracked independently throughout verification. Eligibility is determined by constraints 1–5: a FrameTx that fails any constraint is excused regardless of whether it would execute successfully. Validity is determined by EIP-8141 execution semantics: a FrameTx is valid at a given state if no invalidity condition is triggered. A FrameTx must be both eligible and valid at post-state S, and satisfy the Tier 2 checks, to constitute a FOCIL violation.

Each tier operates on the output of the previous. Tier 1 takes all transactions from all ILs and produces a candidate set C of excluded FrameTxs passing static checks. Tier 2 takes C and filters to transactions satisfying nonce and gas fit. Tier 3 takes that filtered set, applies runtime checks, and identifies transactions whose omission violates IL conditions.

Valid at post-state Invalid at post-state
Eligible FOCIL violation if omitted (assuming Tier 2 passes). Example: well-formed VERIFY, APPROVE called, payer solvent. Excused. Example: VERIFY executes but APPROVE not called, or payer balance insufficient.
Ineligible Excused. Example: VERIFY reads a third-party storage slot beyond the N-slot bound (constraint 5) but would otherwise succeed. Excused. Example: violates constraint 5 and APPROVE not called.

Tier 2 (nonce and gas fit) is a further condition on the eligible+valid cell; failure there is also excused.

Attesters scan all IL transactions (not only excluded ones) to determine which FrameTxs satisfy the static checks at Tier 1. Only excluded FrameTxs in C proceed to Tier 2 and Tier 3.

Tier 1: static checks (no state access, no execution)

For each transaction T across all ILs:

  • If T is included in the block: skip.
  • If T is not a FrameTx: apply the standard nonce/balance proxy and skip.
  • If T does not pass constraints 1–4: excused.
  • Otherwise: add T to candidate set C.

C is the input to Tier 2.

Constraints 1–4 recap:

  • VERIFY-first ordering holds.
  • verify_gas(tx) ≤ MAX_VERIFY_GAS_PER_FRAMETX .
  • Tx is in the budgeted subset of at least one IL.
  • Tx sanity passes (including no blobs).

Tier 2: nonce + gas fit (post-state reads only)

  • tx.nonce == S[tx.sender].nonce
  • tx_gas_limit ≤ gas_left

where:

  • gas_left = block_gas_limit − total_gas_used (including appended IL FrameTxs)
  • tx_gas_limit = FRAME_TX_INTRINSIC_COST + calldata_cost + ÎŁ frame.gas_limit

Fail either check: excused. Otherwise proceed to Tier 3.

Tier 3: bounded validation prefix replay (post-state)

Execute the EIP-8141 validation prefix (the ordered sequence of VERIFY frames) against post-state S, replaying exact frame semantics including ORIGIN behavior and approval state. Apply runtime checks (constraint 5); ineligible: excused.

After the validation prefix completes, if a payer is identified, check solvency:

required_balance = tx_gas_limit × min(tx.max_fee_per_gas, block.base_fee + tx.max_priority_fee_per_gas)

Insolvent: excused. Valid at post-state and fits within gas_left : block does not satisfy IL conditions.

FrameTx eligibility and validity conditions at Tier 3

A FrameTx is valid at a given state if it hits no invalidity condition. A non-VERIFY frame reverting does not invalidate the transaction; a VERIFY frame terminating without APPROVE does.

  • tx.chain_id == block.chain_id
  • tx.max_priority_fee_per_gas ≤ tx.max_fee_per_gas
  • tx.max_fee_per_gas ≥ block.base_fee
  • tx.nonce == state[tx.sender].nonce
  • All VERIFY frames terminate with APPROVE; revert, out-of-gas, or termination without APPROVE: invalid.
  • sender_approved must be established before payer_approved can be set; payer_approved must be true after all VERIFY frames complete.

An ineligibility condition (constraint 5) excuses the transaction regardless of whether the remaining validity conditions would have been satisfied.

Security considerations

DoS resistance

  • Per-tx: verify_gas(tx) ≤ MAX_VERIFY_GAS_PER_FRAMETX by construction.
  • Per-IL: each IL contributes at most MAX_VERIFY_GAS_PER_INCLUSION_LIST of Tier 3 work. Attesters deduplicate by tx hash across ILs, executing the validation prefix once per unique tx.
  • Per-slot: worst case with a 16-member IL committee is 16 × MAX_VERIFY_GAS_PER_INCLUSION_LIST . With suggested parameters: 4,000,000 VERIFY-gas per slot. An adversary controlling m slots forces at most m × MAX_VERIFY_GAS_PER_INCLUSION_LIST .

These figures bound attester work only. Builder-side replay cost is higher; see Builder complexity in Open questions.

Mass invalidation

Constraint 5 rules out storage-based shared-validation dependencies, which covers the main mass-invalidation vector. A shared payer balance can still be drained to invalidate multiple FrameTxs, bounded by block resource cost.

Censorship resistance

An eligible FrameTx valid at post-state S and fitting within gas_left must be included. There is no mechanism for builders to declare an excuse for such a transaction.

Alternative: index-based validation

If the append loop proves impractical, builders could excuse exclusion by proving a transaction was invalid at a declared index. The builder provides a tx hash and claimed index; attesters reconstruct state at that index using BAL (EIP-7928) or similar block-level access data and re-execute the validation prefix. The VERIFY gas bound is still required regardless.

This eliminates the O(k²) append loop but introduces a new network object (the builder’s index claim) and shifts the burden of proof to the builder, who has full control over which index they declare.

Open questions

Builder complexity. How costly is iterative retry for EL clients in practice, and how much slot-time latency does it add? As a reference point: with 4 out of 16 IL committee members malicious, attesters faces up to 1,000,000 VERIFY-gas of replay work, roughly 1.7% of the 60M block gas limit. But builders face significantly more: the O(k²) append loop runs k(k+1)/2 replays in the worst case. With k=16 unique excluded eligible FrameTxs, that is 16×17/2 × MAX_VERIFY_GAS_PER_FRAMETX = 13.6M VERIFY-gas, roughly 22.7% of the 60M block gas limit.

AA-VOPS cache size. The first N storage slots bound in constraint 5 needs to be calibrated against the AA-VOPS cache design. The precise value of N (2–4) is to be benchmarked against real smart wallet validation patterns.

Witnesses for expanded reads. Beyond the AA-VOPS cache, if restricted FrameTxs can attach witnesses (values + Merkle proofs) for additional validation reads, what freshness and root rules apply, and who refreshes proofs as the head changes?

Parameter changes. MAX_VERIFY_GAS_PER_FRAMETX and MAX_VERIFY_GAS_PER_INCLUSION_LIST need to be benchmarked.

Engine API details. Precise changes to newPayload / forkchoiceUpdated for passing ordered IL sets to the EL needs concrete specifications.

3 Likes

Quick follow-up note

It looks like the index-based solution would probably be the best way forward to:

  • Avoid O(k²) verification costs for builders
  • Keep FOCIL eligibility rules minimal and based only on VERIFY costs

As mentioned in the post, the index-based validation approach works like this: “The builder provides a tx hash and claimed index; attesters reconstruct state at that index using BAL (EIP-7928) or similar block-level access data and re-execute the validation prefix.”

The attack surface is bounded by the limited (VOPS) state that VERIFY frames can access: a builder would need to control accounts whose VOPS storage slots are read during VERIFY execution, which becomes increasingly impractical the more accounts the VERIFY frame depends on. The most realistic instance is when a builder is also the sponsor and drains the sponsor’s balance to invalidate a dependent transaction, only to refill it later in the same slot. But even then, the builder (being the payer) would lose reputation from doing this and wouldn’t be selected as a payer in the future.

Since builder-side verification cost is no longer the bottleneck, we could increase MAX_VERIFY_GAS_PER_INCLUSION_LIST to something like 2²⁰ gas. The worst case (16 ILs full of independent frame txs, each filling MAX_VERIFY_GAS_PER_INCLUSION_LIST) would then be 16 × 2²⁰ = 2²⁴, around 28% of the 60M total block gas limit.

Importantly, a 2²⁰ MAX_VERIFY_GAS_PER_INCLUSION_LIST limit would be high enough for transactions from privacy protocols to benefit from FOCIL’s censorship resistance guarantees. More generally, this opens up a separation between FOCIL inclusion eligibility and mempool admission rules: includer nodes could individually choose to join custom mempools (e.g., ones carrying privacy protocol transactions) and include those transactions in their ILs, even if the default public mempool does not carry them. This also means future mempool rules can evolve independently without requiring matching changes to FOCIL eligibility.

Thanks to @Nerolation and @vbuterin for helpful discussions and ideas on this!