Extensible Execution Transaction for Account Capabilities

EXEC_TX introduces a new transaction type built on hook-based execution. Instead of adding new EIPs for account abstraction, privacy, sponsorship, and next-year’s features, we provide one protocol-layer envelope where hooks implement capability logic. The protocol handles the boring parts (nonce, gas, multi-phase routing); hooks handle the interesting parts (auth, escrow, proofs, policy).


The Problem

Currently, every new account capability requires:

  • New transaction type or separate infrastructure (EIP-4337 bundlers, relayers)
  • New wallet signing logic
  • New client validation rules
  • New mempool logic
  • New dApp integrations

This creates friction, fragmentation, and slows feature development.

Solution: One extensible transaction type where capability logic lives in contracts, not consensus.


Core Design Philosophy

Protocol handles invariants; applications handle capabilities.

Responsibility Layer Examples
Replay prevention Protocol 2D nonce, irreversible at inclusion
Gas metering Protocol Intrinsic + phases, deterministic
Phase atomicity Protocol All-or-nothing execution
Authorization Application ECDSA, ZK, multisig, custom schemes
Sponsorship Application Escrow, verification, settlement
Privacy Application Proofs, commitments, nullifiers
Policy Application Amount limits, whitelist, rate control

This separation enables protocol stability and application agility (new capabilities = new contracts, not new opcodes).


The Hook Model: Core Design

What Is a Hook?

A hook is a smart contract that the protocol calls at specific points in the EXEC_TX execution flow. Instead of protocol code handling auth, escrow, proofs, the protocol delegates to hooks. Hooks are the interface between the protocol’s stable rules and the application’s flexible capabilities.

Hook Interface:

interface IExecutionHook {
  function beforeExecution(Context calldata ctx, bytes calldata data) external;
  function afterExecution(Context calldata ctx, ExecutionResult calldata result, bytes calldata data) external;
}

Hooks receive a Context struct that includes:

  • sender (EXEC_TX.from — the real originator)
  • phase (which stage: PRE_VALIDATION=0, PRE_EXECUTION=1, POST_EXECUTION=2)
  • txHash (the signing hash for signature verification)
  • gasLimit, maxFeePerGas (for escrow calculations)
  • payerDataHash (for payer verification)

The Hook Lifecycle

EXEC_TX execution flow:

1. Base validation
   ├─ Decode EXEC_TX
   ├─ Verify signature (EOA) or isAuthorizedExecHook (contract account)
   └─ Load hook contract

2. PRE_VALIDATION (Validation Mode enforced)
   ├─ Protocol calls hook.preValidation(txData, hookData)
   ├─ Hook logic: verify signatures, check proofs, validate policy
   ├─ **Constraints**: No state writes, no block-dependent reads
   └─ Hook returns 0 (fail, tx rejected) or 1 (success, continue)

3. PRE_EXECUTION (if phaseMask bit 1 set)
   ├─ Protocol calls hook.preExecution(txData, hookData)
   ├─ Hook logic: lock funds, reserve capacity, emit intermediate state
   ├─ **Constraints**: None (full EVM access)
   └─ Hook returns 0 (fail, tx reverted) or 1 (success, continue)

4. Core CALL
   ├─ Protocol executes CALL(to, data, value)
   ├─ **No hook involved**
   └─ If reverts, tx reverted entirely (POST_EXECUTION skipped)

5. POST_EXECUTION (if phaseMask bit 2 set AND Core CALL succeeded)
   ├─ Protocol calls hook.postExecution(txData, hookData)
   ├─ Hook logic: release escrowed funds, emit nullifiers, settle accounts
   ├─ **Constraints**: None (full EVM access)
   └─ If hook reverts, entire tx reverted (Core CALL results rolled back)

6. Settlement
   └─ Charge actual gas consumed to payer (or fallback to from)

Why Single Hook Per Transaction?

Instead of protocol-defined multi-hook composition, applications compose via a Hook Dispatcher contract.

Protocol sees:       one hook (the dispatcher)
Dispatcher manages:  AuthModule + SponsorModule + PolicyModule

Why:

  • Protocol stays simple (no multi-hook ordering rules in consensus)
  • Apps stay flexible (dispatcher can evolve without protocol changes)
  • Proven pattern (EIP-4337 bundlers use this at scale)

Alternative: If protocol defined multi-hook semantics, every edge case (module ordering, failure policies, state sharing) would need a new EIP. Single hook + dispatcher avoids this.

Hook Authorization: Strict Client Consistency

For contract accounts, the protocol must verify hook authorization before any hook executes. This is done via:

interface IExecHookAuthorizer {
    function isAuthorizedExecHook(address target) external view returns (bool);
}

// Protocol calls (contract account tx):
STATICCALL(to=from, input=abi.encodeCall(isAuthorizedExecHook, HookSpec.target), gas=5000)

Why so strict?

Clients (geth, reth, erigon) must produce bit-perfect consistent results. Any divergence causes mempool partition.

Return value validation:
  ✅ Exactly 32 bytes: the canonical ABI encoding of bool true
  ✅ Byte pattern: 0x0000000000000000000000000000000000000000000000000000000000000001
  ❌ 0x0000...0000 (false) → reject
  ❌ 0x0000...0002 (non-canonical) → reject  
  ❌ shorter/longer responses → reject

Why canonical encoding matters:

Different smart contracts might return different truthy values:

  • Contract A: returns 1 (uint256)
  • Contract B: returns true (bool)
  • Both are “true” in Solidity semantics
  • But in ABI encoding, they’re different byte patterns

Without strict equality: clients could interpret the same return value differently, causing divergence.

For EOAs: Signature is authorization. No on-chain isAuthorizedExecHook call needed (signature already commits to the hook via the signing hash).

Revert Handling

When Behavior Result
isAuthorizedExecHook reverts Fails immediately Tx rejected
PRE_VALIDATION reverts Phase fails Tx rejected; validation gas charged
PRE_EXECUTION reverts Phase fails Tx reverted; all gas charged
Core CALL reverts Execution fails Tx reverted; POST_EXECUTION skipped
POST_EXECUTION reverts Phase fails Core results rolled back; entire tx reverted

Hook Data Encoding

Hook logic passed in hook.data (arbitrary bytes). Encoding is app-specific, not protocol-mandated.

// Simple: single purpose
hook.data = abi.encode(signature1, signature2)

// Complex: dispatcher routing
hook.data = abi.encode(phase0Data, phase1Data, phase2Data)

Hook detects phase via phaseMask or decodes phase-specific data.

Signature & Gas Semantics

Signature (for EOAs): Standard EIP-191 signature over EXEC_TX hash (fields in order: chainId, nonceKey, nonceSeq, from, to, value, data, gasLimit, maxFeePerGas, payer, payerData, hook, v, r, s).

Contract accounts: No signature. Hook validates authorization via isAuthorizedExecHook.

Gas Budget:

MAX_HOOK_GAS = gasLimit - MIN_CORE_GAS
MIN_CORE_GAS = 21_000
Intrinsic = 21_000 + calldata_bytes × 16 (for non-zero) / 4 (for zero)

Settlement: charge = (intrinsic + actual_hook_gas + actual_core_gas) × maxFeePerGas


The Execution Model: Multi-Phase Design

phaseMask: Phase Selection

An 8-bit value where bits control which phases execute:

phaseMask & 0x1 → PRE_VALIDATION runs (bit 0)
phaseMask & 0x2 → PRE_EXECUTION runs (bit 1)
phaseMask & 0x4 → POST_EXECUTION runs (bit 2)

Examples:

  • phaseMask = 1: PRE_VALIDATION only
  • phaseMask = 3: PRE_VALIDATION + PRE_EXECUTION
  • phaseMask = 7: All three phases
  • phaseMask = 0: No hook; Core CALL only (behaves like legacy tx)

Semantics: If hook is not null, PRE_VALIDATION always runs (bit 0 required).

Why Four Phases?

Phase When Hook Can Do Protocol Guarantees
PRE_VALIDATION Before Core Verify signatures, check proofs, validate policy No state writes; deterministic; mempool-safe
PRE_EXECUTION Before Core Lock escrow, reserve capacity Full EVM access; atomic with Core
Core CALL User intent (No hook) Unchanged from legacy; can do anything
POST_EXECUTION After Core (if succeeds) Release escrow, emit nullifiers Full EVM access; atomic with Core; if reverts, all reverts

Why this structure?

  • PRE_VALIDATION: Deterministic checks before touching state. Mempool nodes can validate locally without divergence.
  • PRE_EXECUTION: Setup before Core (lock funds, reserve). Core can assume resources are available.
  • Core CALL: Unchanged from legacy. User’s actual transaction.
  • POST_EXECUTION: Cleanup after Core succeeds. Escrow accounting is atomic: either Core succeeds AND funds are released, or neither happens.

Validation Mode: Ensuring Deterministic Validation

PRE_VALIDATION runs in Validation Mode — a restricted EVM context ensuring all nodes agree on validity.

The problem: If validation reads block-dependent data, different nodes might validate the same tx differently → mempool divergence.

Solution: Validation Mode forbids block-dependent opcodes:

  • :cross_mark: State reads/writes: SLOAD, SSTORE, TSTORE, LOG*
  • :cross_mark: Block data: TIMESTAMP, BLOCKHASH, NUMBER, BASEFEE, COINBASE, PREVRANDAO, BLOBBASEFEE
  • :white_check_mark: Exceptions: BALANCE, SELFBALANCE (needed for payer checks; risk mitigated by PRE_EXECUTION locking)
  • :white_check_mark: Precompiles allowed (e.g., ecrecover for signature verification)

Recursive scope: Validation Mode applies to all EVM code reachable from the PRE_VALIDATION hook, including nested calls. Hooks cannot escape the restrictions by delegating.

This ensures: same tx validates the same way on all nodes, regardless of which block includes it.


Two-Dimensional Nonce: Replay Prevention + Parallelism

The Problem: Nonce Must Be Protocol-Level

Replay attacks are a consensus issue, not an application issue. Hooks cannot be trusted to manage nonce uniqueness because:

Scenario: Two nodes with same hook code

Node A state: user_nonces = {tx_hash_1: false}
Node B state: user_nonces = {tx_hash_1: false}

Same tx submitted to both:
  Node A: checks hook, sees nonce_hash already used → reject
  Node B: checks hook, sees nonce_hash not used yet → accept

Result: nodes disagree on same tx → consensus failure

The solution: Nonce is a protocol-level counter, not EVM state. The protocol enforces uniqueness before any hook runs.

How It Works

struct ExecutionNonce {
  uint64 nonceKey;   // Lane identifier (0 = default, 1..2^64-2 = custom lanes)
  uint64 nonceSeq;   // Sequence within lane
}

Protocol enforcement (at inclusion time, before any hook):

  1. Validate: nonceSeq == next[from][nonceKey]
  2. Consume: increment next[from][nonceKey]++ immediately (not subject to EVM revert)
  3. Guarantee: no two transactions in a block can have same (from, nonceKey, nonceSeq)

Result: Even if the transaction reverts at any phase (PRE_VALIDATION, PRE_EXECUTION, core, or POST_EXECUTION), the nonce slot is permanently consumed. No reuse, no replay, ever.

Why “Non-EVM State”?

// nonce counter is NOT stored in contract state (EVM)
// NOT at address(0) or any SSTORE location
// It's a protocol-internal counter, consumed at block inclusion time
// Therefore: EXEMPT from EVM revert

// Even if hook reverts:
if (hook_revert) {
  // all hook state changes: ROLLED BACK
  // all core execution state: ROLLED BACK
  // nonce consumption: PERMANENT
}

This is critical for security: if nonce consumption could be undone by a revert, attackers could:

  • Send tx with nonce=100
  • Let it fail
  • Resend same tx with nonce=100 again
  • Bypass replay protection

Independent Lanes for Parallelism

Lanes are independent nonce sequences:

Lane 0 (default):     seq=100, 101, 102, ...   (payment operations, must be sequential)
Lane 1 (intent):      seq=100, 101, 102, ...   (intent responses, can be parallel)
Lane 2 (governance):  seq=100, 101, 102, ...   (DAO votes, can be parallel)

All three can coexist in mempool without blocking each other

Parallel execution benefit:

  • User can submit 10 governance votes (lanes 2, different seqs) simultaneously
  • Can submit 1 payment (lane 0, seq=100) simultaneously
  • Payment failure doesn’t block votes
  • Protocol still prevents replays (within each lane)

EOA vs. Contract Account

Account Type Default Behavior Flexibility
EOA MUST use nonceKey = 0 Protocol enforces: no custom lanes
Contract account CAN use nonceKey = 0..2^64-2 Protocol allows: app chooses lane policy

Why this asymmetry?

  • EOAs use key-management wallets (hardware wallets, browser extensions) — need consistency
  • Contract accounts use smart contract logic — can implement sophisticated lane policies

Nonce Consumption Timing

When: Immediately after base validation passes, before any hook executes

Why this timing?

  • :white_check_mark: Ensures nonce is consumed even if PRE_VALIDATION hook reverts
  • :white_check_mark: Ensures nonce is consumed even if PRE_EXECUTION hook reverts
  • :white_check_mark: Ensures nonce is consumed even if core CALL reverts
  • :white_check_mark: Single atomic operation (no race condition)

Risk: If contract hook doesn’t check nonce uniqueness, same EXEC_TX can execute twice (replay attack).


Transaction Format (EIP-2718)

EXEC_TX is transaction type 0x08 (pending finalization).

RLP structure:

0x08 || RLP([
  chainId, nonceKey, nonceSeq, from, to, value, data,
  gasLimit, maxFeePerGas, payer, payerData,
  hook.target, hook.phaseMask, hook.gasLimit, hook.data,
  v, r, s  // For EOAs only; 0 for contract accounts
])

Fields:

  • nonceKey, nonceSeq: 2D nonce (uint256 each)
  • payer: address(0) if no sponsorship
  • payerData: bytes (empty if payer = address(0))
  • hook.target: address(0) if no hook
  • All other fields: same as legacy txs

Worked Examples: Hook in Action

Example 1: Simple Authorization (PRE_VALIDATION only)

// 2-of-3 multisig contract account sends tokens

EXEC_TX {
  from: safe_multisig_account,
  to: token_contract,
  data: transfer(recipient, amount),
  hook: {
    target: multisig_validator,  // Deployed once, reused by all 2-of-3 accounts
    phaseMask: 1,  // PRE_VALIDATION only
    gasLimit: 100_000,
    data: abi.encode([sig1, sig2])  // 2-of-3 signatures
  }
}

Hook execution:

contract MultisigValidator {
  function preValidation(bytes calldata txData, bytes calldata hookData) 
    external view returns (uint256) {
    // Decode signatures
    (bytes memory sig1, bytes memory sig2) = abi.decode(hookData, (bytes, bytes));
    
    // Recover signers from EXEC_TX signing hash
    address signer1 = recoverFromSignature(txData, sig1);
    address signer2 = recoverFromSignature(txData, sig2);
    
    // Verify both signers are in this account's 2-of-3 set
    Safe safe = Safe(msg.sender);  // msg.sender is the EXEC_TX from address
    require(safe.isOwner(signer1) && safe.isOwner(signer2), "Invalid signers");
    
    return 1;  // Success
  }
}

Why this matters: First time contract accounts have native base-layer signature validation. No bundler needed. Same validator contract reused by 100,000 Safe wallets.

Example 2: Privacy with ZK (PRE_VALIDATION only)

// Privacy token transfer with ZK proof

EXEC_TX {
  from: user,
  to: privacy_token_contract,
  data: [commitment_hash, encrypted_transfer_params],
  hook: {
    target: zk_verifier,
    phaseMask: 1,  // PRE_VALIDATION only
    gasLimit: 200_000,
    data: zk_proof  // Public proof
  }
}

Hook execution:

contract ZKVerifier {
  function preValidation(bytes calldata txData, bytes calldata hookData) 
    external view returns (uint256) {
    // Decode calldata to extract commitment_hash
    (bytes32 commitment, ) = abi.decode(txData, (bytes32, bytes));
    
    // Decode proof from hook.data
    bytes memory proof = hookData;
    
    // Verify proof (deterministic, no state reads)
    require(verifyProof(proof, commitment), "Invalid proof");
    
    return 1;  // Success
  }
}

Execution continuation:

  • Core: Privacy token contract receives encrypted params, updates shielded pool state
  • User retained private key off-chain; can decrypt when withdrawing in future tx

Why this works: Proof verification (public, deterministic) is decoupled from state mutation (encrypted). Privacy logic evolves in contracts without consensus changes.

Example 3: Multi-Module Composition with Dispatcher (All Phases)

// 2-of-3 multisig + amount limits + sponsored gas

EXEC_TX {
  from: safe_multisig_account,
  to: uniswap,
  data: swap_calldata,
  payer: relayer_sponsor,
  hook: {
    target: dispatcher,  // Routes to AuthModule, PolicyModule, EscrowModule
    phaseMask: 7,  // All three phases
    gasLimit: 500_000,
    data: abi.encode(
      [sig1, sig2],                    // For AuthModule (phase 0)
      {amount: 100e18, recipient: "0x..."},  // For PolicyModule (phase 1)
      relayer_address                  // For EscrowModule (phase 2)
    )
  }
}

Hook execution:

contract Dispatcher {
  AuthModule authModule;
  PolicyModule policyModule;
  EscrowModule escrowModule;

  function preValidation(bytes calldata txData, bytes calldata hookData) 
    external view returns (uint256) {
    (bytes[] memory sigs, , ) = abi.decode(hookData, (bytes[], bytes32, address));
    return authModule.verify(txData, sigs) ? 1 : 0;
  }

  function preExecution(bytes calldata txData, bytes calldata hookData) 
    external returns (uint256) {
    (, bytes32 constraints, address relayer) = abi.decode(hookData, (bytes[], bytes32, address));
    
    // Validate policy
    require(policyModule.validateConstraints(constraints, txData), "Policy violation");
    
    // Lock escrow for gas sponsorship
    require(escrowModule.lockEscrow(relayer, tx.gasprice * 500_000), "Escrow lock failed");
    
    return 1;
  }

  function postExecution(bytes calldata txData, bytes calldata hookData) 
    external returns (uint256) {
    (, , address relayer) = abi.decode(hookData, (bytes[], bytes32, address));
    
    // Release escrowed funds (calculate actual gas used, refund difference)
    uint256 actualGasCost = (initialGas - gasleft()) * tx.gasprice;
    escrowModule.releaseEscrow(relayer, actualGasCost);
    
    return 1;
  }
}

Execution timeline:

  1. PRE_VALIDATION: Dispatcher.preValidation → AuthModule.verify → 2-of-3 signature check (~8K gas)
  2. PRE_EXECUTION: Dispatcher.preExecution → PolicyModule.validate (amount limits) → EscrowModule.lockEscrow (~10K gas)
  3. Core: Uniswap swap executes (~100K gas, depends on market)
  4. POST_EXECUTION (only because Core succeeded): Release escrow, refund relayer (~5K gas)
  5. Settlement: Relayer charged actual gas; fallback to Safe if relayer drained

Why this architecture: Single hook (dispatcher) routes to 3 internal modules. Protocol doesn’t care how many modules exist; dispatcher handles composition. Routing overhead is ~1K gas (negligible).


Advanced Patterns: Composition and Optimization

Pattern 1: Parallel Lanes for Retry Logic

Use 2D nonce lanes to decouple retry:

EXEC_TX (lane 0) {
  phaseMask: 7,
  data: primary_operation
}

EXEC_TX (lane 1) {
  phaseMask: 1,
  data: fallback_operation  // executes if lane 0 fails
}

Both pending simultaneously without blocking. Primary succeeds → fallback cancelled.

Pattern 2: Batch Atomicity Via Dispatcher

Multi-op atomicity via hook-based dispatcher:

contract BatchDispatcher {
  function preExecution(bytes calldata txData, bytes calldata hookData) 
    external returns (uint256) {
    (Operation[] memory ops) = abi.decode(hookData, (Operation[]));
    require(lockEscrow(sumCosts(ops)), "Escrow failed");
    return 1;
  }
  
  function postExecution(bytes calldata txData, bytes calldata hookData) 
    external returns (uint256) {
    (Operation[] memory ops) = abi.decode(hookData, (Operation[]));
    for (uint i = 0; i < ops.length; i++) {
      releaseEscrow(ops[i].actualCost);
    }
    return 1;
  }
}

Single Core CALL dispatches internally. All succeed or all revert.

Pattern 3: 2D Nonce Lanes for Parallel Workflows

Different lanes for different transaction types:

Lane 0: Payments (sequential)
Lane 1: Intent responses (parallel)
Lane 2: Governance votes (parallel)

User submits simultaneously:

Tx1: lane=0, seq=100  (payment)
Tx2: lane=2, seq=50   (vote)
Tx3: lane=1, seq=25   (intent)

All pending together. Lane 0 failure doesn’t block lanes 1 or 2.


Gas Accounting: Key Rules

  • Intrinsic: 21,000 + calldata cost + authorizer cost (5,000 for contract accounts)
  • Phase budgets: Each phase (PRE_VALIDATION, PRE_EXECUTION, POST_EXECUTION) gets HookSpec.gasLimit independently
  • Core reservation: If POST_EXECUTION is enabled, HookSpec.gasLimit gas is reserved and unavailable to Core CALL
  • Total limit: intrinsic + (phase_count × HookSpec.gasLimit) + MIN_CORE_GAS(21,000) ≤ gasLimit

Charge: total_gas_used × effectiveGasPrice to payer (or fallback to from if payer drained)


Validation Mode: Common Pitfalls and Enforcement

Opcodes Forbidden in Validation Mode

Strictly Forbidden (9 opcodes):
  SLOAD        0x54    (state read)
  SSTORE       0x55    (state write)
  TIMESTAMP    0x42    (block.timestamp)
  BLOCKHASH    0x40    (historical blocks)
  GASLIMIT     0x45    (block gas limit)
  BLOBBASEFEE  0x4a    (EIP-4844)
  PREVRANDAO   0x44    (randomness)
  COINBASE     0x41    (miner address)
  BASEFEE      0x48    (base fee)

Allowed despite apparent risk:
  BALANCE      0x31    (payer balance check)
  SELFBALANCE  0x19    (payer balance check)
  
Why BALANCE is allowed:
  - Critical for payer liquidity check
  - TOCTOU race mitigated by PRE_EXECUTION fund locking
  - If balance changes between PRE_VALIDATION and PRE_EXECUTION, lockEscrow will catch it

Client Enforcement

Each EVM client must implement Validation Mode identically. Implementation:

  • Use tracer to detect forbidden opcodes (SLOAD, SSTORE, TIMESTAMP, etc.)
  • Check PRE_VALIDATION return value (must be 0 or 1)
  • Reject if forbidden opcode detected

Client testing: Fuzz EXEC_TX with intentionally bad hooks. All clients must reject identically. This is critical for mempool consistency.


Security Considerations

Nonce Uniqueness for Contract Accounts

Protocol enforces ordering; contract accounts must prevent replay:

Vulnerable:

contract VulnerableHook {
  function preValidation(bytes calldata txData, bytes calldata hookData) 
    external view returns (uint256) {
    return isValidSignature(txData, hookData) ? 1 : 0;  // No nonce check!
  }
}

Same tx can execute twice. Correct pattern:

contract SafeHook {
  mapping(address => mapping(uint64 => bool)) executed;
  
  function preValidation(bytes calldata txData, bytes calldata hookData) 
    external view returns (uint256) {
    (address from, uint64 nonceSeq, ) = decodeTx(txData);
    require(!executed[from][nonceSeq], "Nonce already used");
    require(isValidSignature(txData, hookData), "Bad signature");
    return 1;
  }
  
  function preExecution(bytes calldata txData, bytes calldata hookData) 
    external returns (uint256) {
    (address from, uint64 nonceSeq, ) = decodeTx(txData);
    executed[from][nonceSeq] = true;  // Mark before Core, atomic with it
    return 1;
  }
}

Mark in PRE_EXECUTION, not POST: Ensures if Core fails, entire tx reverts and nonce flag rolls back.

Payer Fallback Risk

If payer drained between PRE_VALIDATION and execution, lockEscrow fails and entire tx reverts. If fallback address has no balance, tx fails.

Mitigations:

  1. Wallet UI: Highlight payer risk
  2. Hook pre-check: Verify payer balance before submission
  3. Escrow contracts: Use long-term escrow instead of ephemeral EOA payers

Integration Guidance

For EIP-4337 users, EXEC_TX offers native base-layer support (no bundler infrastructure). Key migration points: move verifier logic to PRE_VALIDATION hook, use protocol nonce instead of custom tracking, implement escrow in PRE_EXECUTION/POST_EXECUTION phases. Use standard tx mempool instead of UserOp mempool.


Gas Overhead and Mempool Efficiency

  • Protocol overhead: 5,000 gas (isAuthorizedExecHook, contract accounts only)
  • App hooks: 50–200K gas typical (auth, escrow, policy)
  • Mempool optimizations: Validation Mode determinism enables result caching and parallel validation without divergence risk

Future Directions

Post-Mainnet (no consensus changes):

  • Hook Registry: Off-chain metadata mapping for wallet UX
  • Standardized Modules: AuthModule, EscrowModule, PolicyModule shared libraries
  • Hook Gas Profiling: Tooling for accurate cost prediction
  • 2D Nonce UX Standard: How wallets present lanes to users

Potential Protocol v2 (future EIP):

  • Hook Metadata field for off-chain coordination
  • Conditional Execution: “Execute only if state[addr] == value”
  • Multi-Hook support with ordering guarantees

How Hook Design Solves The Problem

From “Upgrade for Each Feature” to “Deploy a Hook”

Old model (EIP-4337, bundler relayers):

  1. New feature needed → New EIP proposal → Client consensus → Mainnet upgrade → Wallets integrate → Dapps integrate
  2. Months to years per feature
  3. Each feature fragments infrastructure

New model (EXEC_TX + hooks):

  1. New feature needed → Write hook contract → Deploy → Dapps integrate
  2. Weeks per feature
  3. Single unified infrastructure (EXEC_TX)

Examples of Future Hooks (No Protocol Changes Needed)

  • Risk management: Risk hook validates transaction against user’s risk profile
  • Intent solving: Intent hook validates that on-chain state matches user’s intent
  • Privacy: Privacy hook verifies proofs against commitments
  • Quantum-safe auth: Hook implements post-quantum signature verification
  • Social recovery: Hook implements social recovery mechanisms
  • Delegation: Hook verifies delegated authority proofs
  • Batch operations: Hook validates batch structure and dependencies
  • MEV resistance: Hook implements encrypted calldata + revealing mechanics

All of these are possible without a single protocol change because the hook interface is generic.


Key Properties of the Hook Model

Properties That Make It Work

  1. Deterministic PRE_VALIDATION: All nodes agree on validity. No mempool partition risk.
  2. Atomic phases: PRE_EXECUTION and POST_EXECUTION atomic with Core. Escrow invariants hold.
  3. Authorization delegated: Contracts define validity (isAuthorizedExecHook). Protocol enforces structure.
  4. Fast iteration at app layer: New hooks = no consensus. Deploy and test weekly if needed.
  5. Composable via dispatcher: Single hook supports 100+ internal modules. Pattern proven at EIP-4337 scale.

Trade-Offs

Trade-Off Choice Rationale
Protocol vs App Protocol: nonce/phases/gas Enforces invariants. Apps enforce policy.
Single vs. Multiple Hooks Single hook Minimal consensus rules. Dispatcher proven.
Nonce Uniqueness EOA: enforced, Contract: delegated Safe by default. Flexibility for apps.
Validation Strictness Strict (9 opcodes forbidden) Mempool consistency. BALANCE allowed for payer checks.
Payer Fallback Fallback if drained Atomicity. Mitigation: wallet UI warnings.

Why This Design Will Endure

Stability Through Delegation

The power of EXEC_TX is that protocol stability and application evolution are decoupled.

Once EXEC_TX is deployed:

  • Protocol rules never change (nonce, gas, phase ordering)
  • Capabilities evolve in contracts (hooks, dispatchers, modules)

New capability needed in 5 years? Deploy a new hook contract. No EIP, no client upgrade, no consensus risk.

Future innovation examples (no protocol changes needed):
- Quantum-safe signatures: new AuthModule
- Encrypted calldata + MEV protection: new PrivacyModule  
- Advanced sponsorship: new SponsorModule
- Risk management: new PolicyModule
- Cross-chain intents: new SettlementModule

Protocol Minimal Closure

EXEC_TX’s design is based on finding the minimal set of protocol rules required for consensus safety, then delegating everything else to contracts.

This leaves room for evolution:

What the protocol MUST guarantee:
  ✅ Replay prevention (nonce)
  ✅ Gas metering consistency (deterministic)
  ✅ Phase atomicity (all-or-nothing)

What the protocol MUST NOT define:
  ❌ Authorization schemes
  ❌ Sponsorship policies
  ❌ Capability composition
  ❌ Application business logic

This separation is the key to longevity.


Open Questions for Community

  1. Validation Mode implementation consistency across clients?
  2. Hook gas limit flexibility vs. predictability?
  3. Payer fallback UX: accept silent fallback or require strict payer?
  4. 2D nonce lane presentation to end users?
  5. Dispatcher pattern standardization and audit practices?

Critical: Formal verification of Validation Mode opcode enforcement and client divergence prevention.


Update Log

External Reviews

None as of 2026-04-08.

Outstanding Issues

  • Hook contract ABI and interface standardization
  • Validation Mode implementation consistency across EVM clients
  • Phase context passing mechanism for hooks
  • Payer fallback semantics and UX mitigation
  • Contract account nonce safety guarantees
  • Hook revert data capture and error messaging
  • Gas metering for optional phases
  • 2D nonce wallet UX design
  • Dispatcher pattern data encoding standardization

Why Now?

Account abstraction fragmented into bundlers (4337), batch systems (8130), per-tx delegation (7702). EXEC_TX unifies them: one protocol envelope, pluggable hooks, contracts own security.


Related: EIP-155, EIP-1559, EIP-1153, EIP-2718, EIP-2929, EIP-4844, EIP-6780

1 Like

Two Core Design Challenges in EXEC_TX We Need to Discuss

I want to openly raise two fundamental challenges in this design that I think the community should grapple with. These aren’t edge cases—they’re about whether the core
architecture actually works.


Challenge 1: Validation Mode Consistency Across Clients

The Problem

EXEC_TX requires Validation Mode—a restricted EVM context where PRE_VALIDATION runs deterministically on all nodes. The protocol forbids certain opcodes and
enforces this recursively across all nested calls.

The goal is clear: same transaction must validate identically on all nodes, regardless of which block includes it.

Why This Matters

Implementation divergence isn’t a “normal bug”—it’s a consensus failure.

If geth’s opcode tracer slightly differs from reth’s, or erigon handles precompiles differently, or one client’s exception handling for stack overflow diverges from the
others, the result is:

  • Mempool partition: Node A accepts tx, Node B rejects it
  • Consensus risk: Block with tx that validates on one client but not another

The Hard Part

The spec can say “forbid SLOAD, TIMESTAMP, etc.” but making that work identically across three independent client implementations is genuinely difficult:

  • Opcode interception: How deep does it need to go? Just top-level or all nested calls?
  • Precompile handling: Do precompiles escape the restrictions? They don’t execute EVM code, but what about their behavior on different clients?
  • Exception boundaries: Stack overflow, invalid instruction, call depth exceeded—do all clients handle these identically?
  • Recursive scope: When a hook calls an external contract, does Validation Mode apply there too? How do we enforce it at depth?

Why ERC-7562 Learned This Hard

ERC-7562 had to define AA validation scope extremely carefully because this is where implementations diverge. And ERC-7562 is dealing with a simpler problem (a
single validation function) than EXEC_TX (multi-phase execution with recursive hooks).

What We Don’t Have

  • Formal verification that all clients implement Validation Mode identically
  • Comprehensive testing covering edge cases across three different EVM architectures
  • Reference implementation that other clients can benchmark against
  • Clear guidance on what happens when clients do diverge (which they probably will)

The Question

Is opcode-level interception actually sufficient? Or do we need:

  • Formal specification with mathematical proof?
  • Reference implementation in each client?
  • Fuzzing campaigns specifically designed to find client divergences?

I don’t know. And I think we need to discuss this openly before moving forward.


Challenge 2: Validation Mode Can’t Express Real Policies

The Problem

Real-world capability logic often depends on reading state:

Authorization policies that need SLOAD:
├─ Amount limits: “check if user exceeded daily quota”
├─ Whitelists: “check if recipient is approved”
├─ Risk controls: “check historical tx count”
├─ Permission hierarchies: “check user’s role”
└─ Delegation: “check if delegated authority is valid”

Time-dependent policies that need TIMESTAMP:
├─ Authorization expiry: “check if approval window is still open”
├─ Rate limiting: “check if time since last tx meets cooldown”
└─ Conditional logic: “enable feature after timestamp X”

But Validation Mode forbids SLOAD and TIMESTAMP.

Why This Constraint Exists

The constraint is necessary—if Validation Mode could read protocol-changing state, there’s no way to guarantee determinism across nodes. Different nodes might see
different state at different times, breaking the core promise.

So the constraint is correct. But it means…

What Actually Happens

Those policies don’t disappear. They get pushed elsewhere:

Option A: Move to PRE_EXECUTION
✓ Full EVM access
✗ But now policy can mutate state
✗ Loss of deterministic validation
✗ Complex to reason about

Option B: Move to application layer
✓ Explicit in business logic
✗ Not standardized
✗ Different for every hook
✗ Hard to audit across ecosystem

Option C: Hybrid
✗ Fragmented policy logic
✗ Impossible to understand intent
✗ Nightmare for wallets

The Honest Truth

EXEC_TX doesn’t unify capability logic. It unifies:

  • :white_check_mark: Transaction envelope (nonce, gas, phases)
  • :white_check_mark: Consensus invariants (atomicity, revert semantics)
  • :white_check_mark: Basic validation (signatures via precompiles)

But the hard part—business logic that depends on state—still scatters across:

  • Validation Mode with BALANCE/SELFBALANCE exceptions
  • PRE_EXECUTION side effects
  • Application layer conventions
  • Hook dispatcher internals

Is that acceptable? I genuinely don’t know.

The Question

Should Validation Mode be strictly read-only? Or should there be a path for carefully-managed state reads that preserve determinism? Or should we accept that
“validators” and “policies” are fundamentally different concerns that can’t be unified?


Invitation to Community

These aren’t rhetorical questions. I’m raising them because I think they matter for whether this design actually achieves its stated goals.

I want to hear from:

  • Client teams: Is client-consistent Validation Mode implementation tractable? What would you need?
  • AA researchers: Can we formalize Validation Mode? Should we?
  • Wallet developers: Would you need to understand dispatcher internals? Is that acceptable?
  • Theorists: Is there a way to support richer validation policies without sacrificing determinism?

What I’m not doing is pretending these are small problems or that we have all the answers yet.

Let’s discuss.


EXEC_TX: Two Fundamental Design Decisions — Final Specification (v1)

Dear readers,

As we advance the EXEC_TX specification, we present two fundamental binary design decisions with protocol-grade precision.


Decision 1: Validation Mode Constraint Strength

Problem Statement

During PRE_VALIDATION, nodes must execute wallet-provided verification logic. If validation logic can observe arbitrary chain state, different nodes may see different state, causing validation results to diverge and breaking consensus.

EXEC_TX enforces strict limitations on observable state during validation.

Our Position: Validation Profile MUST be “Strict”

Validation Execution is Deterministic; Validation Outcome is Non-Binding

Validation execution is deterministic across all nodes. Given the same transaction and block state, every honest node executing the Validation Profile produces identical results. However, validation success does not guarantee execution success. State changes between validation and execution may cause PRE_EXECUTION or Core CALL to fail, even if PRE_VALIDATION succeeded.

This separates two distinct concepts:

  • Validation as execution process: Deterministic (all nodes agree on what happened)
  • Validation as execution guarantee: Non-binding (state may change before execution)

Wallets and validators must treat validation and execution success as independent events.

Unified Failure Semantics

Any exception during PRE_VALIDATION or PRE_EXECUTION (including OOG, invalid opcode, or explicit revert) MUST result in transaction failure with no state changes applied.

This rule applies uniformly to:

  • Out-of-gas conditions (gas consumed >= validationGasLimit)
  • Invalid opcodes or forbidden operations
  • Explicit revert() calls from hook logic
  • Arithmetic errors or stack underflow
  • Any other exception during validation or pre-execution phases

No partial state changes. No recovery mechanisms. Complete atomicity: either all validation succeeds and execution proceeds, or transaction fails entirely.

Transaction Envelope: Gas Limit Fields

EXEC_TX transactions include two independent gas limit fields:

  • validationGasLimit: uint64 — Maximum gas allocation for PRE_VALIDATION phase execution. Subject to protocol constant ceiling of 100,000.
  • executionGasLimit: uint64 — Maximum gas allocation for Core CALL execution. Standard Ethereum gas limit semantics.

The two gas limits are independent envelope fields, not a split of a single limit.

These fields are metered independently:

  • :white_check_mark: Validation gas consumption does NOT reduce execution gas allocation
  • :white_check_mark: Execution gas consumption does NOT depend on validation gas usage
  • :white_check_mark: Each phase is independently charged and accounted
  • :cross_mark: These are NOT two parts of a single gas limit
  • :cross_mark: Unused validation gas does NOT roll over to execution

Permitted Operations During PRE_VALIDATION

  1. Computational operations: arithmetic, logic, hashing, signature verification (ECRECOVER)

  2. Calldata access: CALLDATALOAD, CALLDATACOPY

    • :white_check_mark: Calldata is immutable transaction data, identical across all nodes
    • :white_check_mark: Permits parsing signatures, hook data, and validation parameters
    • :white_check_mark: Essential for any verification logic to function
  3. Limited account information: BALANCE, SELFBALANCE (current account only)

    • :warning: Critical caveat: Non-binding for execution
    • :cross_mark: Cannot be used to make decisions about execution success
    • :white_check_mark: Informational only for mempool filtering
  4. STATICCALL with strict constraints:

    • :white_check_mark: Permitted: Precompiles (SHA256, RIPEMD160, BLAKE2b, MODEXP, etc.)
    • :white_check_mark: Permitted: Protocol-defined addresses (see Appendix A for enumeration)
    • :cross_mark: Forbidden: Arbitrary contract calls (prevents delegation of validation logic)

Forbidden Operations

  • State modification (SSTORE)
  • Block context (NUMBER, TIMESTAMP, BLOCKHASH, COINBASE, DIFFICULTY, GASLIMIT, PREVRANDAO, BLOBBASEFEE)
  • Sender-controllable context (CALLVALUE, GASPRICE, ORIGIN)

The STATICCALL Closure Problem

Validation Profile must be closed under the entire call tree. If hooks STATICCALL to arbitrary contracts, those contracts may themselves STATICCALL elsewhere, delegating validation logic to unpredictable code. Each node computes different results based on its view of chain state.

By restricting to precompiles and protocol-defined addresses, we ensure the entire computation tree is locally verifiable on every node.

Gas Accounting in Validation

Gas accounting in PRE_VALIDATION MUST follow the exact same rules as EVM execution gas accounting defined in the protocol, with no special-case deviations. This includes:

  • :white_check_mark: Memory expansion costs (identical to EVM)
  • :white_check_mark: Subcall gas costs (for STATICCALL to precompiles)
  • :white_check_mark: Precompile pricing (identical to EVM)
  • :white_check_mark: Code execution costs
  • :cross_mark: NO special “discount” for validation gas
  • :cross_mark: NO custom pricing rules
  • :cross_mark: NO client-specific optimizations

All clients must measure gas consumption using the same rules. Divergent gas accounting = consensus fork.

Validation Gas Limit is a Protocol Constant: 100,000 Gas Maximum

This is a protocol constant defined by the specification and enforced identically by all clients.

  • :white_check_mark: Hard-coded in EXEC_TX specification
  • :white_check_mark: All Ethereum clients (Geth, Reth, Besu, Nethermind, etc.) MUST enforce identically
  • :white_check_mark: Any transaction with validationGasLimit > 100,000 is invalid
  • :cross_mark: NOT client-configurable
  • :cross_mark: NOT a recommended value
  • :cross_mark: NOT subject to per-client optimization

This constant is fixed in this specification. Future changes to this value require a full hard fork and protocol amendment with all stakeholders.

Violation = consensus fork:
If Geth enforces 100,000 and Reth enforces 120,000, a transaction with 110,000 gas fails on Geth but succeeds on Reth, causing permanent chain split.

Transactions with validationGasLimit > 100,000 are invalid by protocol rules and MUST be rejected by all clients during transaction validation.

  • Consensus validation layer rejects any transaction where validationGasLimit > 100,000
  • This rejection occurs during transaction validation, before mempool acceptance
  • Mempool implementations may use additional filtering strategies, but consensus-layer rejection is mandatory for all clients
  • All clients must measure gas consumption identically using EVM-equivalent rules
  • Divergent gas measurement or different consensus-layer validation = consensus fork

During PRE_VALIDATION execution:

  • Gas is tracked separately from execution gas for metering purposes only
  • If PRE_VALIDATION consumes >= validationGasLimit, it reverts with OUT_OF_GAS
  • Validation gas consumption does NOT reduce execution gas allocation
  • Total transaction cost includes both validation and execution gas

Semantics of validation OOG:

  • Transaction fails immediately during PRE_VALIDATION
  • No partial state changes are persisted
  • Validation gas is burned (no refund to payer)

Fee Calculation and Settlement

Validation gas is metered separately, charged under the same fee parameters as execution gas, and follows the same refund rules unless validation fails, in which case no refund applies to the failed validation phase.

Detailed semantics:

  • :white_check_mark: Validation gas and execution gas are tracked in separate metering domains
  • :white_check_mark: Both phases use the same baseFeePerGas and maxPriorityFeePerGas from the transaction
  • :white_check_mark: Total transaction fee = (validation gas consumed + execution gas consumed) × effective gas price
  • :white_check_mark: Gas refunds apply to total transaction gas consumption (validation + execution combined)
  • :white_check_mark: If PRE_VALIDATION succeeds and later phases succeed, standard refund rules apply to total gas
  • :cross_mark: If PRE_VALIDATION fails, no refund applies to the failed validation phase
  • :cross_mark: Validation and execution gas are NOT charged as separate line items
  • :cross_mark: Validation gas is NOT discounted or specially refunded
  • :cross_mark: Validation and execution gas do NOT have independent fee schedules

Fee settlement responsibility:

The entity responsible for paying total transaction fees (validation + execution) is determined by hook logic. Unless specified otherwise by the hook, the transaction sender is responsible for all costs. If a payer sponsors the transaction, the payer is responsible for covering both validation and execution costs under the same fee terms.

PRE_VALIDATION vs. PRE_EXECUTION: Admission Checks vs. Binding Authorization

The protocol distinguishes two validation phases with clear semantic boundaries:

PRE_VALIDATION (admission checks only):

  • Lightweight checks using calldata and precompiles
  • Results are informational for wallet UX and mempool filtering
  • May perform only admission checks (format validation, initial signature checks, etc.)
  • Results are NOT binding for state-dependent authorization
  • May influence acceptance into the mempool, but MUST NOT be used as an authorization guarantee by wallets, builders, or validators

PRE_EXECUTION (binding authorization):

  • Full state access with all current account information available
  • Hooks re-validate all state-dependent conditions (balance, nonce, account settings, payer commitment)
  • Failure here is final and binding
  • PRE_EXECUTION is the only phase whose result is binding for state-dependent authorization
  • Transaction fails atomically if any state-dependent check fails

Critical distinction:

  • :white_check_mark: PRE_VALIDATION may perform only admission checks; results are informational
  • :white_check_mark: PRE_EXECUTION is the only phase whose result is binding for state-dependent authorization
  • :cross_mark: PRE_VALIDATION results are NOT semi-binding or conditionally binding
  • :cross_mark: PRE_VALIDATION may influence mempool acceptance, but must not be used as an authorization guarantee
  • :cross_mark: Wallets may NOT assume PRE_VALIDATION success implies execution success
  • :cross_mark: Builders may NOT include transactions based on PRE_VALIDATION alone

Critical protocol requirements:

  • :white_check_mark: PRE_EXECUTION MUST re-validate all balance/nonce conditions, even if PRE_VALIDATION passed
  • :white_check_mark: If PRE_EXECUTION validation fails, transaction fails atomically (unified failure semantics apply)
  • :cross_mark: Wallets may NOT assume PRE_VALIDATION success implies PRE_EXECUTION success
  • :cross_mark: Builders may NOT include transactions based on PRE_VALIDATION alone

All hook checks depending on account.balance, account.nonce, or global state MUST occur in PRE_EXECUTION, not PRE_VALIDATION.


Decision 2: Dispatcher Architecture — Reference Implementation vs. Protocol Mandate

Problem Statement

Hooks must route requests to different modules (AuthModule, PaymasterModule, etc.). Should the protocol mandate a specific dispatcher interface, or allow hooks to implement custom routing logic?

Our Position: Canonical Reference Dispatcher, Not Protocol-Enforced

We define a canonical reference dispatcher as a coordination point, without protocol-level mandate.

Baseline Semantics (Reference Implementation):

interface IExecHookAuthorizer {
    function isAuthorizedExecHook(
        address account,
        bytes calldata hookData
    ) external view returns (bool authorized, bytes memory moduleData);
}

// Standard dispatcher routes on moduleData prefix:
//   [0:32]   = moduleId (keccak256("auth"), keccak256("paymaster"), etc.)
//   [32:]    = module-specific calldata

Important: The prefix routing format is part of the reference implementation semantics, not a consensus requirement. Alternative routing schemes are protocol-compatible; the reference format is a coordination point for ecosystem standardization.

Dispatcher Logic is Not Part of Consensus

Dispatcher logic is not part of consensus. The protocol treats hook execution as opaque bytecode execution; no consensus rule depends on dispatcher semantics.

This means:

  • :white_check_mark: Wallets are free to implement custom dispatcher logic
  • :white_check_mark: Builders and validators do not need to understand dispatcher internals
  • :white_check_mark: Different chains may have different dispatcher implementations
  • :cross_mark: The protocol does NOT prescribe dispatcher structure
  • :cross_mark: No consensus rule interprets or validates dispatcher semantics

Why Not Protocol-Enforced?

A protocol-mandated dispatcher interface freezes routing semantics into the spec, making evolution impossible without protocol upgrade. Different wallets have different needs:

  • Smart account wallets may want registry-based dispatch
  • Multisig wallets may want custom validation pipelines
  • Privacy wallets may want encrypted module routing

By providing a reference dispatcher without mandating it, we allow experimentation.

Integration Pressure Argument

In practice, alternative dispatcher implementations may be theoretically compatible with EXEC_TX, but practical wallet and infrastructure support will gravitate toward the canonical reference dispatcher because:

  1. Wallet UX pressure: Wallets handling both standard and custom dispatchers face complexity
  2. Validator/builder optimization: Infrastructure optimizes for the reference dispatcher first
  3. Auditability and trust: Security researchers audit the canonical dispatcher first

This creates natural integration pressure that coordinates the ecosystem without protocol enforcement. Wallets are free to deviate, but practical adoption gravitates toward the standard.

Critical Note for Implementers

Alternative dispatcher implementations that deviate from baseline semantics may be protocol-compatible, but practical adoption requires conformance to canonical dispatcher semantics. The burden of compatibility falls on the non-standard implementation.


Precision on Protocol-Defined Addresses

The Governance Trap

“Protocol-defined addresses” requires precise boundaries. Without clarity, this term risks becoming a governance entry point.

Proposed Definition

Protocol-defined addresses are a fixed, globally agreed set explicitly enumerated in this specification. This set may only be updated via hard fork. No expansion, no L2 customization, no chain-specific variants.

The exact address list and the exact STATICCALL-safe functions on those addresses must be enumerated in the specification appendix. This includes:

  • :white_check_mark: Which addresses are allowed
  • :white_check_mark: Which selectors/functions on those addresses are STATICCALL-safe
  • :white_check_mark: Which calls are forbidden even on protocol-defined addresses
  • :cross_mark: No interpreter discretion
  • :cross_mark: No “future expansion” placeholders

What this means:

  • :white_check_mark: The spec names specific addresses (precompiles 0x01–0x09)
  • :white_check_mark: The list is complete and immutable in this version
  • :white_check_mark: The selector whitelist is append-only: additions require hard fork and spec revision
  • :cross_mark: No runtime governance
  • :cross_mark: No L2-specific protocol-defined addresses
  • :cross_mark: No soft-fork extensions
  • :cross_mark: Changes to the allowed address/function set require hard fork and spec revision
  • :white_check_mark: Changes to the allowed address/function set are NOT subject to governance voting

Immutability Rule:

Any change to the allowed address/function set requires a hard fork and a spec revision. The address/function whitelist is not a governance-updateable configuration; it is part of the protocol specification itself.

Why This is Critical

If the spec leaves room for interpretation, three bad outcomes follow:

  1. Governance trap: Every new system contract becomes a vote
  2. Consensus risk: Clients disagree on which addresses are “protocol-defined”
  3. L2 inconsistency: L2s define their own addresses, fragmenting semantics

By making the set immutable and explicitly enumerated, we avoid all three.


Summary of Both Decisions

Decision Our Position Critical Constraint
Validation Execution Deterministic; outcome non-binding Execution is deterministic; guarantee is not
Failure Semantics Unified: all exceptions → full atomicity No partial state changes; complete tx failure
Gas Limit Fields Two independent fields: validationGasLimit + executionGasLimit Independent metering; no rollover between phases
Validation Mode Strict (precompiles + protocol-defined addresses) No arbitrary contracts; closed under call tree
Gas Limit Cap Protocol constant: 100,000 gas (fixed) Hard-coded, identical accounting across all clients; changes require hard fork
Gas Limit Declaration Per-transaction with protocol ceiling tx.validationGasLimit ≤ 100,000; execution uses tx.executionGasLimit
Gas Fee Calculation Independent metering; joint settlement under same rules Validation + execution independently metered; combined for total cost; same refund rules apply
Validation Authorization Admission checks in PRE_VALIDATION; binding in PRE_EXECUTION Only PRE_EXECUTION results bind state-dependent decisions
State-Dependent Validation PRE_EXECUTION is authoritative All state checks in PRE_EXECUTION
Protocol-Defined Addresses Fixed set (precompiles 0x01–0x09), hard-fork updates only Enumerated in spec appendix; immutable in v1
Dispatcher Reference, not enforced; not consensus Integration pressure coordinates; semantics opaque to protocol
POST_EXECUTION Atomicity Part of same atomic unit as Core CALL Not a separate commit boundary; any failure reverts both

What We’re Explicitly Stating

  1. Validation execution is deterministic; validation outcome is non-binding. This separates the execution guarantee from the state guarantee.

  2. Unified failure semantics apply universally. Any exception during validation or pre-execution → complete atomic failure with no state changes.

  3. Two independent gas limit fields with independent metering and joint settlement. validationGasLimit (≤ 100,000) and executionGasLimit are metered separately; combined for total transaction fee under same fee parameters and refund rules.

  4. Validation gas limit is a fixed protocol constant. The cap is 100,000 gas; all clients enforce identically; changes require hard fork.

  5. PRE_VALIDATION performs admission checks; PRE_EXECUTION provides binding authorization. PRE_VALIDATION may influence mempool acceptance but must not be used as an authorization guarantee. Only PRE_EXECUTION results are binding for state-dependent decisions.

  6. State-dependent checks belong in PRE_EXECUTION. All balance/nonce/account logic executes after state is finalized.

  7. Protocol-defined addresses are precompiles (0x01–0x09), fixed and hard-fork-upgradeable. This list is immutable in v1. Changes require hard fork.

  8. The dispatcher is reference-based, not consensus-enforced. Wallets are free to implement custom dispatchers; the protocol treats hook execution as opaque bytecode. Coordination happens through integration pressure, not protocol rules.

  9. POST_EXECUTION is part of the same atomic execution unit as Core CALL. Not a separate commit boundary; may observe Core CALL effects; any failure reverts all state changes from both phases.


Specification Appendices

Appendix A: Allowed Protocol-Defined Precompile Targets (v1)

The following addresses are reserved for protocol-defined STATICCALL targets in Validation Profile. This list is immutable in v1 specification.

Initial Protocol-Defined Precompile Targets (v1 - Final):

Address Purpose EVM Specification Notes
0x0000000000000000000000000000000000000001 ECRECOVER Standard precompile Signature recovery
0x0000000000000000000000000000000000000002 SHA-256 Standard precompile Hash computation
0x0000000000000000000000000000000000000003 RIPEMD-160 Standard precompile Hash computation
0x0000000000000000000000000000000000000004 Identity Standard precompile Data copy
0x0000000000000000000000000000000000000005 MODEXP Standard precompile Modular exponentiation
0x0000000000000000000000000000000000000006 BN254 ADD Standard precompile Elliptic curve operations
0x0000000000000000000000000000000000000007 BN254 MUL Standard precompile Elliptic curve operations
0x0000000000000000000000000000000000000008 BN254 PAIRING Standard precompile Elliptic curve operations
0x0000000000000000000000000000000000000009 BLAKE2b Standard precompile Hash computation

Status: This list is final for v1 specification. All entries above are immutable until hard fork.

Future Additions: Any protocol-defined addresses beyond those listed above require hard fork amendment and specification revision (v2 or later).

Immutability and Update Path:

  • This list is fixed in v1 specification and may only be modified via hard fork
  • No runtime modifications or governance-based updates are permitted
  • No L2-specific variants or customizations are allowed
  • Changes to this set require specification amendment and full protocol upgrade

For each precompile target:

  • All calls must follow standard EVM precompile semantics
  • Gas costs follow standard precompile pricing (per EVM spec)
  • STATICCALL is the only permitted execution mode
  • All operations are governed by the EVM specification; no additional restrictions apply

Appendix B: Validation Profile Constraints (v1)

Permitted opcodes (strict subset for deterministic execution):

  • Arithmetic: ADD, MUL, SUB, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND
  • Comparison: LT, GT, SLT, SGT, EQ, ISZERO
  • Bitwise: AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR
  • Cryptography: SHA3 (KECCAK256), ECRECOVER (via STATICCALL to 0x01)
  • Context: ADDRESS, CALLER (caller context only)
  • Transaction Input: CALLDATALOAD, CALLDATASIZE, CALLDATACOPY (immutable tx data)
  • Code Inspection: CODESIZE, CODECOPY (own hook code only)
  • Account Info: BALANCE, SELFBALANCE (current account only; non-binding for execution)
  • Program Counter: PC, MSIZE
  • Gas Metering: GAS (permitted, but must NOT be used for control flow decisions; informational only)
  • Control Flow: JUMP, JUMPI, JUMPDEST, REVERT (explicit failure)
  • Stack: PUSH0, PUSH1–PUSH32, DUP1–DUP16, SWAP1–SWAP16
  • System: STOP

Forbidden opcodes (everything else, including):

  • State Access: SSTORE, SLOAD (state modification/reading)
  • External Calls: DELEGATECALL, CALL, CALLCODE (external state access)
  • Restricted STATICCALL: STATICCALL to arbitrary addresses (only precompiles and protocol-defined addresses allowed)
  • Contract Creation: SELFDESTRUCT, CREATE, CREATE2
  • State Inspection: EXTCODESIZE, EXTCODECOPY, EXTCODEHASH (arbitrary contract inspection forbidden)
  • Logging: LOG0, LOG1, LOG2, LOG3, LOG4 (information leakage forbidden in validation)
  • Block Context: NUMBER, TIMESTAMP, BLOCKHASH, COINBASE, DIFFICULTY, GASLIMIT, PREVRANDAO, BLOBBASEFEE
  • Sender-Controllable: CALLVALUE, GASPRICE, ORIGIN
  • All other opcodes not explicitly listed above

Gas measurement rules:

Reference EVM gas accounting with no deviations. All clients must measure identically.


Appendix C: Dispatcher Reference Implementation

  • IExecHookAuthorizer interface specification
  • Prefix routing format (reference semantics only)
  • Example module encoding schemes
  • Canonical address deployments (reference only)
  • Implementation guidelines for wallet integrators

Appendix D: Execution Flow Pseudocode

The following pseudocode defines the execution flow and transaction state management. All clients MUST implement semantics equivalent to this logic.

function validateTransaction(tx):
    // Check tx's declared validation gas limit against protocol cap
    if tx.validationGasLimit > 100000:
        return REJECT
    
    // Execute PRE_VALIDATION with tx-declared validation gas limit
    // PRE_VALIDATION performs admission checks only
    // Results are informational; not binding for authorization
    try:
        result = executePreValidation(tx, gasLimit=tx.validationGasLimit)
        if result == FAILED:
            return REJECT
    except Exception as e:
        // Any exception = rejection, no state changes
        return REJECT
    
    return ACCEPT


function executeTransaction(tx):
    // Execute PRE_EXECUTION with full state access
    // This is the ONLY phase whose result binds state-dependent authorization
    // PRE_EXECUTION results are authoritative and binding
    try:
        result = executePreExecution(tx)
        if result == FAILED:
            return FAILED (no state changes)
    except Exception as e:
        return FAILED (no state changes)
    
    // Execute Core CALL with tx-declared execution gas limit
    try:
        result = executeCoreCall(tx, gasLimit=tx.executionGasLimit)
        if result == FAILED:
            return FAILED (no state changes from core call onward)
    except Exception as e:
        return FAILED (no state changes)
    
    // Execute POST_EXECUTION in same atomic transaction context
    // POST_EXECUTION is part of the same atomic execution unit as Core CALL
    // It is NOT a separate commit boundary
    // It executes in the same atomic unit as Core CALL and may observe 
    // all state changes made by Core CALL execution. However,
    // any failure in POST_EXECUTION MUST revert all state changes from both
    // Core CALL and POST_EXECUTION atomically.
    try:
        result = executePostExecution(tx)
        if result == FAILED:
            // Any failure = atomic reversion of both Core CALL and POST_EXECUTION
            return FAILED (revert all state from Core CALL and POST_EXECUTION)
    except Exception as e:
        // Any exception = atomic reversion
        return FAILED (revert all state from Core CALL and POST_EXECUTION)
    
    // Transaction fully succeeded
    return SUCCESS


function executePreValidation(tx, gasLimit):
    // Isolated validation context with independent gas metering
    // PRE_VALIDATION performs admission checks only (format validation, initial signature checks)
    // Results are informational for mempool filtering, NOT binding for authorization
    // Validation gas is metered independently from execution gas
    context = new EVMContext()
    context.allowedOpcodes = VALIDATION_PROFILE_OPCODES
    context.allowedStaticCalls = PRECOMPILES + PROTOCOL_DEFINED_ADDRESSES
    context.gasLimit = gasLimit
    context.gasCounter = 0
    
    // Execute hook with gas tracking (separate from execution gas)
    try:
        result = context.executeCode(
            address=tx.hook,
            data=tx.hookData,
            sender=tx.from
        )
        if context.gasCounter > gasLimit:
            return FAILED  // OOG in validation
        return result
    except OutOfGasException:
        return FAILED  // OOG in validation


function executePreExecution(tx):
    // Full state access; all state-dependent checks here
    // PRE_EXECUTION results are BINDING for state-dependent authorization
    // This is the ONLY phase whose result is authoritative for state decisions
    return tx.hook.isAuthorizedExecHook(
        account=tx.from,
        hookData=tx.hookData
    )


function executeCoreCall(tx, gasLimit):
    // Execute the actual user operation with tx-declared execution gas limit
    // Separate and independent from validation gas
    return call(
        to=tx.to,
        data=tx.data,
        value=tx.value,
        sender=tx.from,
        gasLimit=gasLimit
    )


function executePostExecution(tx):
    // Post-execution in same atomic unit; may observe Core CALL effects
    // POST_EXECUTION is part of the same atomic execution unit as Core CALL (not a separate commit boundary)
    // Failure here reverts both Core CALL and POST_EXECUTION effects atomically
    if tx.hook has POST_EXECUTION capability:
        return tx.hook.postExecution(
            account=tx.from,
            hookData=tx.hookData,
            coreCallResult=last_call_result  // Results of Core CALL available here
        )
    else:
        return SUCCESS

Key properties of this pseudocode:

  1. Independent gas limit fields: tx.validationGasLimit (≤ 100,000) and tx.executionGasLimit are separate parameters with independent metering
  2. Independent gas metering: Validation gas counter and execution gas counter are completely separate
  3. Joint fee settlement: Both gas amounts combined for total transaction cost under same fee parameters and refund rules
  4. Unified failure semantics: Any exception at any phase → complete atomic reversion
  5. Admission vs. binding: PRE_VALIDATION admission checks only (informational, may influence mempool); PRE_EXECUTION provides binding authorization
  6. POST_EXECUTION atomicity: Part of same atomic unit as Core CALL (not separate commit boundary); may observe effects; any failure reverts both atomically
  7. No partial execution: All or nothing at every phase
  8. State observation rules: POST_EXECUTION can read Core CALL effects but cannot commit selectively

POST_EXECUTION Transaction Context and Atomicity

POST_EXECUTION is part of the same atomic execution unit as Core CALL. It is not a separate commit boundary. POST_EXECUTION may observe Core CALL effects within a single atomic execution:

  • :white_check_mark: POST_EXECUTION may read state modified by Core CALL
  • :white_check_mark: POST_EXECUTION may make additional state changes
  • :white_check_mark: POST_EXECUTION may determine gas refunds or escrow releases based on Core CALL results
  • :white_check_mark: POST_EXECUTION is part of the same atomic execution unit as Core CALL
  • :cross_mark: POST_EXECUTION is NOT a separate commit boundary
  • :cross_mark: Core CALL and POST_EXECUTION are NOT staged commits
  • :cross_mark: Partial execution is forbidden
  • :white_check_mark: Any failure in POST_EXECUTION reverts all state changes from both Core CALL and POST_EXECUTION atomically

Implementation requirement: Clients must not implement POST_EXECUTION as a separate transaction phase with separate rollback logic. Both phases are part of a single atomic execution unit: all succeed or all fail. POST_EXECUTION is not a separate commit boundary.


Specification Status

This is the initial version (v1) of the EXEC_TX specification. The following items are locked and immutable in this version:

  1. :white_check_mark: Validation execution is deterministic with non-binding outcomes (immutable in v1)
  2. :white_check_mark: Unified failure semantics apply across all phases (immutable in v1)
  3. :white_check_mark: Gas limit cap is 100,000 (immutable in v1; hard fork required to change)
  4. :white_check_mark: Two independent gas limit fields with separate metering (immutable in v1)
  5. :white_check_mark: Validation Profile constraints (permitted/forbidden opcodes in Appendix B) (immutable in v1)
  6. :white_check_mark: Protocol-defined address set (precompiles 0x01–0x09 in Appendix A) (immutable in v1)
  7. :white_check_mark: Dispatcher is reference-based, not protocol-enforced (immutable in v1)
  8. :white_check_mark: POST_EXECUTION is atomic with Core CALL (immutable in v1)

Future amendments require hard fork and specification version revision.


Looking forward to your thoughts.

EXEC_TX: Positioning and Scope Clarification

What This Proposal Is NOT

This is not an Account Abstraction (AA) design proposal.

This is not intended to compete with ERC-4337, EIP-8130, EIP-8141, or similar account-centric approaches.

What This Proposal IS

EXEC_TX is a transaction-level execution framework centered around a protocol-defined hook system over the transaction lifecycle.

The core contribution is not a specific feature set (AA, sponsorship, privacy, etc.), but a general execution interface that can support them all without standardizing any one of them.


Core Design Thesis

The Abstraction Target: Transaction, Not Account

Existing Account Abstraction approaches primarily operate at the account level:

  • Account creation, validation, and execution logic are bundled
  • Protocol often defines or restricts capability semantics (signing, authorization, payment)

EXEC_TX abstracts at the transaction level:

  • Protocol defines deterministic execution phases for every transaction
  • A single hook entry point is exposed
  • Capability logic is delegated to application-defined dispatchers

Deterministic Protocol-Level Phases

EXEC_TX defines four deterministic execution phases:

PRE_VALIDATION (deterministic, non-binding)
    ↓
PRE_EXECUTION (deterministic, binding for authorization)
    ↓
EXECUTION (Core CALL + state changes)
    ↓
POST_EXECUTION (deterministic cleanup and settlement)

Every client executes these phases identically. The execution model is deterministic. The execution outcome is not necessarily bound by pre-execution checks.

Single Hook Entry Point with Application-Defined Dispatch

Protocol responsibility:

  • :white_check_mark: Enforce phase boundaries
  • :white_check_mark: Enforce gas metering and atomicity
  • :white_check_mark: Call the hook with deterministic inputs

Hook/application responsibility:

  • :white_check_mark: Implement capability logic (auth, sponsorship, policy)
  • :white_check_mark: Route to modules via dispatcher (standard or custom)
  • :white_check_mark: Express business logic without protocol constraints

Separation: Deterministic Validation vs. Stateful Authorization

This design deliberately separates two concerns:

PRE_VALIDATION: Deterministic Admission Checks

  • Executed on every node identically
  • No state dependencies (call-only inputs)
  • Results are informational and non-binding
  • Purpose: Filter transactions for mempool efficiency, provide early feedback to wallets

PRE_EXECUTION: Stateful Authorization and Policy

  • Executed only by blocks being built
  • Full state access
  • Results are binding for authorization
  • Purpose: Enforce real-world policies (balance, nonce, session rules, agent constraints)

This separation is a key design choice because:

  1. Consensus safety: Deterministic validation ensures nodes agree on admission criteria
  2. Policy expressiveness: Stateful authorization allows arbitrarily complex policies without consensus risk
  3. Mempool efficiency: Non-binding PRE_VALIDATION allows builders to reject transactions early without validation cost
  4. Real-world correctness: PRE_EXECUTION can enforce rules that depend on current state (balance, nonce, time-based policies)

Applications Enabled by This Framework

Within this transaction abstraction model, the following applications become natural to express:

1. Privacy-Preserving Transactions

Example: Proof-based validation + conditional execution

PRE_VALIDATION:
  - Parse ZK proof from calldata
  - STATICCALL to precompile for proof verification
  - Proof is deterministic; all nodes agree on validity

PRE_EXECUTION:
  - Check that prover has sufficient balance/nonce for the hidden action
  - Validate proof corresponds to current state commitment

EXECUTION:
  - Execute the hidden action based on proof

This is not naturally expressed in account-centric models without introducing additional transaction types or conventions.

2. Programmable Policies and Session Rules

Example: Multi-factor authentication, spending limits, time-locks

PRE_EXECUTION:
  - Check if this action is within session scope
  - Verify 2FA nonce has been consumed
  - Enforce daily spending limit

EXECUTION:
  - Execute the action

These policies are stateful and account-specific; they belong in PRE_EXECUTION, not protocol.

3. Multi-Party and Cross-Account Coordination

Example: Intent settlement, DEX routing, batch swaps

PRE_VALIDATION:
  - Verify order signature matches intent structure

PRE_EXECUTION:
  - Check that counterparty's conditions are met
  - Ensure atomic settlement across multiple accounts

EXECUTION:
  - Settle the trade

4. Agent-Based Execution Flows

Example: AI agents acting under human-defined constraints

PRE_VALIDATION:
  - Verify agent's authorization signature

PRE_EXECUTION:
  - Check that proposed action is within agent's delegated scope
  - Verify budget constraints

POST_EXECUTION:
  - Update agent's spend counter
  - Log execution for audit trail

5. Application-Specific Execution Semantics

Any domain with custom validation or execution rules can implement them via hooks:

  • Messaging/social: proof of identity + rate-limiting
  • Gaming: state channel settlement + fraud proofs
  • Finance: oracle settlement + atomic options execution
  • IoT: signed commands + state machine enforcement

Why Account Abstraction is Just One Use Case

Under this framework, Account Abstraction is one possible application of the transaction abstraction:

EXEC_TX Transaction Framework
├── Account Abstraction (custom validation + sponsorship)
├── Privacy Transactions (proof-based validation)
├── Programmable Policies (session rules, limits)
├── Multi-Party Coordination (intents, DEX routing)
├── Agent Execution (AI agents under constraints)
└── ... (any application with custom execution logic)

AA is not the defining goal; it’s just one use case that happens to fit naturally into this model.


Deployment Context: L2 and Long-Term Value

EXEC_TX is particularly well-suited for L2 environments:

Why L2s Benefit Most

  1. Rapid Iteration: Hook and dispatcher patterns can evolve quickly without hard forks
  2. Custom Execution Models: Applications can deploy their own execution semantics without L1 validation
  3. Bundled Capabilities: Privacy, agents, policies, and coordination can coexist in a single framework
  4. Protocol Stability: L2 achieves expressiveness through hooks, not through protocol amendment

Long-Term Value

The value of EXEC_TX is not measured in features delivered at launch, but in:

  • :white_check_mark: Extensibility: New applications can be enabled without protocol changes
  • :white_check_mark: Stability: The protocol interface remains stable while applications evolve
  • :white_check_mark: Composability: Different execution models can coexist and interact
  • :white_check_mark: Decentralization: Application logic is not baked into protocol; communities can define their own rules

Key Design Decisions Reflected in This Positioning

1. Deterministic Validation with Non-Binding Outcomes

Implication: Wallets and builders must not assume PRE_VALIDATION success implies execution success.

Benefit: Allows nodes to perform admission checks without consensus risk, while preserving real-world correctness via stateful PRE_EXECUTION.

2. Single Hook, Application-Defined Dispatch

Implication: Protocol does not standardize dispatcher semantics or capability composition.

Benefit: Applications are free to express arbitrarily complex policies; future capabilities can be added without protocol amendment.

3. Protocol-Defined Gas and Atomicity, Application-Defined Policy

Implication: The protocol enforces invariants (phase boundaries, gas metering, atomicity), but not policies (who can spend, what actions are allowed, etc.).

Benefit: Protocol remains minimal and stable; policy innovation happens at application layer.


Design Philosophy: Minimal Protocol, Maximal Application Layer

A guiding principle of EXEC_TX is to keep the protocol surface minimal while pushing capability design to the application layer.

This approach has several benefits:

  • :white_check_mark: Protocol Stability: The core execution interface remains unchanged as applications evolve
  • :white_check_mark: Reduced Complexity: The protocol does not need to define or arbitrate capability semantics
  • :white_check_mark: Innovation Velocity: New transaction capabilities can be deployed without protocol amendments
  • :white_check_mark: Decentralized Governance: Communities can define their own execution rules through hooks and dispatchers, rather than lobbying for protocol changes

In this sense, EXEC_TX reduces protocol complexity while enabling greater expressiveness at the application layer.


Summary: Three Core Claims

Claim 1: Transaction Abstraction is Orthogonal to Account Abstraction

You can have AA without transaction abstraction (ERC-4337, traditional smart wallets).

You can have transaction abstraction without AA (EXEC_TX can support any execution model).

EXEC_TX provides the latter; it enables AA as one application, not as the defining goal.

Claim 2: Separating Deterministic Validation from Stateful Authorization is Essential

Deterministic validation ensures network consistency.

Stateful authorization ensures real-world correctness.

Conflating them leads to either:

  • Overly restrictive policies (if stateful logic must be deterministic)
  • Consensus risk (if deterministic logic depends on state)

EXEC_TX cleanly separates these concerns.

Claim 3: This Framework Unlocks Applications That Are Difficult to Express in Account-Centric Models

Privacy transactions, agent execution, multi-party coordination, and custom execution semantics all become natural.

These applications are not edge cases; they are representative of a broader class of emerging transaction requirements.


Conclusion

EXEC_TX is a transaction abstraction framework, not an account abstraction proposal.

The broader value of this design is in providing a stable, extensible, protocol-level execution interface for future transaction capabilities, of which Account Abstraction is one supported use case.

It may be more useful to evaluate this proposal as “How well does this provide a foundation for arbitrary transaction-level innovation?” rather than as “How well does this solve Account Abstraction?”

A key strength of this design is that it:

  1. :white_check_mark: Enforces determinism where needed (network consistency)
  2. :white_check_mark: Enables expressiveness where needed (application policies)
  3. :white_check_mark: Separates these concerns cleanly (preventing consensus risk)
  4. :white_check_mark: Delegates capability design to applications (enabling long-term evolution)

In summary: EXEC_TX introduces a protocol-level transaction abstraction via lifecycle hooks, with Account Abstraction as one supported use case rather than its defining objective.