EIP-8369: VOPS Profiles for FOCIL Eligibility

Discussion topic for EIP-8369: VOPS Profiles for FOCIL Eligibility · Pull Request #12110 · ethereum/EIPs · GitHub

Abstract

This EIP describes two validity-only partial statelessness (VOPS) profiles for fork-choice enforced inclusion list (FOCIL) eligibility. Profile 1 covers regular transactions, every non-frame type without blobs, and keeps EIP-7805’s end-of-payload omission check. Profile 2 covers EIP-8141 frame transactions whose validation stays within a fixed account abstraction VOPS (AA-VOPS) state surface, including privacy transactions based on keyed nonces and recent roots. Profile 2 omission is checked at a builder-claimed transaction index in the payload, so it protects a transaction only if the transaction remains valid at every index the builder could claim. Transactions outside both profiles may still appear in inclusion lists (ILs), but FOCIL does not enforce them. Public mempool admission and FOCIL eligibility remain separate. This EIP is Informational; consensus enforcement belongs in an extension to EIP-7805.

Motivation

FOCIL (EIP-7805) requires builders to include IL transactions unless a missing transaction is invalid when appended to the payload or the payload does not have enough remaining gas to append it. That rule is cheap for regular transactions because the omission check depends mainly on gas, nonce, and balance. Frame transactions have programmable validation, so validity may also depend on keyed nonces, payer state, recent roots, or bounded account storage.

This EIP defines which transactions remain cheap enough to enforce. Profile 1 keeps EIP-7805’s end-of-payload rule. Profile 2 evaluates an omitted frame transaction at a builder-claimed index against a fixed validation state surface. Because the builder chooses the index, Profile 2 is weaker when another transaction can move a validation dependency within the block; Security Considerations defines that boundary.

FOCIL eligibility is not mempool policy. An includer may receive an eligible transaction through the public mempool, a custom mempool, or direct submission. The submission path affects whether a transaction reaches an IL, not whether its omission is enforceable.

One thing in the Security Considerations looks like it can be promoted from an observation into an enforcing rule. The current text says that for a position-stable Profile 2 transaction, builder-claimed-index evaluation is equivalent to EIP-7805’s end-of-payload rule. It also correctly identifies the opposite case: shared payer balance, mutable validation state, or nonce dependencies that can become valid later receive the weaker positional guarantee. If the result is invariant to position for the first class, is there any reason to give the builder an evaluation-index choice for that class at all?

I think the enforcing extension can use one rule:


if position_stable(validation_dependencies):
    evaluation_index = len(payload.transactions)
else:
    evaluation_index = committed_claim_or_end_default

This would not change the guarantee described by the EIP. It would make the existing equivalence operational:

  • position-stable dependencies inherit the normal EIP-7805 end-of-payload omission check;

  • position-dependent dependencies retain the claimed-index mechanism and its explicitly weaker guarantee.

The important requirement is that position_stable must itself be mechanically decidable from a closed protocol-defined dependency class. It cannot mean “the builder says this is stable” or require searching every possible insertion point.

The current text already gives useful boundaries for that predicate:

  • fresh nonzero keyed nonces with nonce_seq == 0 and recent roots are position-stable examples;

  • validation slots qualify only under a non-mutability condition;

  • payment capacity qualifies only when reserved for the transaction;

  • shared payer balance without reservation remains positional;

  • nonce/predecessor dependencies that can transition from invalid → valid remain positional.

So a useful conformance pair seems to be:


shared payer:
valid -> drain -> invalid -> refill -> valid
=> positional, builder claim matters

fresh keyed nonce:
valid -> consumed -> remains consumed
=> position-stable, end-of-payload rule

There are two practical benefits beyond making the guarantee explicit:

  1. builders do not need to commit claims for omissions whose verdict cannot change with position;

  2. builder discretion exists only where bounded replay actually requires it.

This seems cleaner than making claimed position part of every Profile-2 omission simply because some Profile-2 dependencies require it. Would it make sense for the Standards Track extension to define this position-stability predicate and use end-of-payload evaluation whenever it holds?

1 Like

Thanks for your comment, this makes sense as a possible optimization for the enforcing EIP but the current profile does’t provide a reliable way to classify the whole transaction as position stable imo

A fresh keyed nonce may be stable but there is still payer balance, storage, code, or delegation that remain mutable

so I would keep the current claimed-index rule and revisit this if the EIP is able to define a class of transactions that is stable

which is similar to the direction being explored here iiuc: https://ethresear.ch/t/order-dependence-as-the-classifying-dimension-for-frame-transaction-mempool-admission/25934

1 Like

Yes — that is the missing distinction. I was applying the stability property at the dependency level, but the optimization is only sound if it holds over the whole validation result. The current Profile 2 surface does not prove that, because a transaction can combine a fresh keyed nonce with mutable payer balance, storage, code, or delegation.

The order-dependence framing you linked looks like the right direction, but I think the FOCIL condition is slightly stronger than generic mempool order-independence.

To remove the claimed index safely, what we need is:

valid(T, S_end) => valid(T, S_i)

for every claimable index i

equivalently:

invalid(T, S_i) => invalid(T, S_end)

for every claimable index.

That is the property that prevents an earlier builder-selected state from creating an omission excuse when the transaction is valid at the end.

This also means a generic single-writer classification is not sufficient by itself. The queued-nonce case is the useful counterexample: the nonce state can be single-writer while the transaction still transitions:

invalid → valid

after its predecessor executes.

So I think the safe decomposition is:

current Profile 2
→ claimed-index by default

strict stable subprofile
→ end-of-payload only if the complete validation
dependency closure is mechanically decidable
and invalidity is guaranteed to persist to the end

Anything with mutable payer capacity, mutable storage, code/delegation, dynamically changing dependency closure, or another invalid → valid path simply stays in ordinary claimed-index Profile 2.

That suggests the next problem is narrower than classifying all Profile 2 transactions:

What is the smallest protocol-decidable Frame validation subprofile for which whole-transaction invalidity is guaranteed to persist to end-of-payload?

If that class can be defined mechanically, then the optimization becomes safe rather than heuristic. Otherwise I agree the current claimed-index rule should remain.