EIP-7886 - Delayed execution

Created 2025-02-18
Status Draft
Category Core
Type Standards Track
Authors
Requires

Abstract

This proposal introduces a mechanism to make execution blocks statically verifiable through minimal checks that only require the previous state, without requiring execution of the block's transactions. This enables validators to attest to a block's validity without completing its execution.

Motivation

The primary advantage of this proposal is asynchronous block validation. In the current Ethereum protocol, blocks must be fully executed before validators can attest to them. This requirement creates a bottleneck in the consensus process, as attestors must wait for execution results before committing their votes, limiting the network's throughput potential.

By introducing a mechanism where execution payloads can be reverted rather than invalidating the entire block, execution is no longer an immediate requirement for validation. Instead, a block's validity can be determined based on its structural correctness and the upfront payment of transaction fees by senders. This allows attestation to happen earlier in the slot, independent of execution, potentially enabling higher block gas limits and significant throughput improvements across the network.

Specification

Header Changes

The block header structure is extended to support delayed execution:

@dataclass
class Header:
    # Existing fields
    parent_hash: Hash32
    ommers_hash: Hash32
    coinbase: Address

    # Pre-execution state root - this is the state root before executing transactions
    pre_state_root: Root  

    # Deferred execution outputs from parent block
    parent_transactions_root: Root    # Transaction root from parent block
    parent_receipt_root: Root         # Receipt root from parent block
    parent_bloom: Bloom               # Logs bloom from parent block
    parent_requests_hash: Hash32      # Hash of requests from the parent block
    parent_execution_reverted: bool   # Indicates if parent block's execution was reverted

    # Other existing fields
    difficulty: Uint
    number: Uint
    gas_limit: Uint
    gas_used: Uint                   # Declared gas used by txs, validated post-execution
    timestamp: U256
    extra_data: Bytes
    prev_randao: Bytes32
    nonce: Bytes8
    base_fee_per_gas: Uint
    withdrawals_root: Root
    blob_gas_used: U64               # Total blob gas used by transactions
    excess_blob_gas: U64
    parent_beacon_block_root: Root

The key changes are:

  1. pre_state_root: Represents the state root before execution (checked against the post-execution state of the parent block)
  2. parent_receipt_root: Receipt root from the parent block (deferred execution output)
  3. parent_bloom: Logs bloom from the parent block (deferred execution output)
  4. parent_requests_hash: Hash of requests from the parent block (deferred execution output)
  5. parent_execution_reverted: Indicates whether the parent block's execution was reverted

A block header MUST include all these fields to be considered valid under this EIP. The pre_state_root MUST match the state root after applying the parent block's execution. The parent execution outputs MUST accurately reflect the previous block's execution results to maintain chain integrity.

Chain State Tracking

The blockchain object is extended to track execution outputs for verification in subsequent blocks:

@dataclass
class BlockChain:
    blocks: List[Block]
    state: State
    chain_id: U64
    last_transactions_root: Root    # Transaction root from the last executed block
    last_receipt_root: Root         # Receipt root from last executed block
    last_block_logs_bloom: Bloom    # Logs bloom from last executed block
    last_requests_hash: Bytes       # Requests hash from last executed block
    last_execution_reverted: bool   # Indicates if the last block's execution was reverted

These additional fields are used to verify the deferred execution outputs claimed in subsequent blocks. The last_transactions_root, last_receipt_root, last_block_logs_bloom, last_requests_hash, and last_execution_reverted act as critical chain state references that MUST be updated after each block execution to ensure proper state progression. When a block's execution is reverted due to a gas mismatch, the last_execution_reverted field is set to True, which affects the base fee calculation of subsequent blocks.

Block Validation

Static validation is performed separately from execution. In this phase, all checks that can be done without executing transactions are performed:

def validate_block(chain: BlockChain, block: Block) -> None:
    # Validate header against parent
    validate_header(chain, block.header)

    # Validate deferred execution outputs from the parent
    if block.header.parent_transactions_root != chain.last_transactions_root:
        raise InvalidBlock
    if block.header.parent_receipt_root != chain.last_receipt_root:
        raise InvalidBlock
    if block.header.parent_bloom != chain.last_block_logs_bloom:
        raise InvalidBlock
    if block.header.parent_requests_hash != chain.last_requests_hash:
        raise InvalidBlock
    if block.header.pre_state_root != state_root(chain.state):
        raise InvalidBlock
    if block.header.parent_execution_reverted != chain.last_execution_reverted:
        raise InvalidBlock

    ...

    # Process all transactions trie and validate transactions
    total_inclusion_gas = Uint(0)
    total_blob_gas_used = Uint(0)
    withdrawals_trie = Trie(secured=False, default=None)

    # Track sender balances and nonces by address
    sender_balances = {}
    sender_nonces = {}

    # Calculate blob gas price based on excess blob gas
    blob_gas_price = calculate_blob_gas_price(block.header.excess_blob_gas)

    # Validate each transaction
    for i, tx in enumerate(map(decode_transaction, block.transactions)):
        # Validate transaction parameters (signature, fees, etc.)
        validate_transaction(tx, block.header.base_fee_per_gas, block.header.excess_blob_gas)

        # Recover sender
        sender_address = recover_sender(chain.chain_id, tx)

        # Calculate gas costs (both EIP-7623 intrinsic cost and floor cost)
        intrinsic_gas, calldata_floor_gas_cost = calculate_intrinsic_cost(tx)
        blob_gas_used = calculate_total_blob_gas(tx)

        # Track total gas usage (using the maximum of intrinsic gas and floor cost)
        total_inclusion_gas += max(intrinsic_gas, calldata_floor_gas_cost)
        total_blob_gas_used += blob_gas_used

        # Calculate maximum gas fee (including blob fees)
        effective_gas_price = calculate_effective_gas_price(tx, block.header.base_fee_per_gas)
        max_gas_fee = tx.gas * effective_gas_price + blob_gas_used * blob_gas_price

        # Verify sender is an EOA or has delegation support
        if sender_address not in sender_balances:
            account = get_account(chain.state, sender_address)
            is_sender_eoa = (
                account.code == bytearray()
                or is_valid_delegation(account.code)
            )
            if not is_sender_eoa:
                raise InvalidBlock
            sender_balances[sender_address] = account.balance
            sender_nonces[sender_address] = account.nonce

        # Verify sender has sufficient balance
        if sender_balances[sender_address] < max_gas_fee + Uint(tx.value):
            raise InvalidBlock

        # Verify correct nonce
        if sender_nonces[sender_address] != tx.nonce:
            raise InvalidBlock

        # Track balance and nonce changes
        sender_balances[sender_address] -= max_gas_fee + Uint(tx.value)
        sender_nonces[sender_address] += 1

    # Validate gas constraints
    if total_inclusion_gas > block.header.gas_used:
        raise InvalidBlock
    if total_blob_gas_used != block.header.blob_gas_used:
        raise InvalidBlock

    # Validate withdrawals trie
    for i, wd in enumerate(block.withdrawals):
        trie_set(withdrawals_trie, rlp.encode(Uint(i)), rlp.encode(wd))

    if block.header.withdrawals_root != root(withdrawals_trie):
        raise InvalidBlock

This validation function enforces several requirements:

  1. Clients MUST validate that the block's parent execution outputs match the chain's tracked last execution outputs.
  2. The pre_state_root MUST match the current state root to ensure proper state transition.
  3. All transactions MUST be statically validated for signature correctness and transaction type-specific requirements (EIP-1559, EIP-4844, etc.).
  4. Sender accounts MUST be externally owned accounts (EOAs) or have valid delegation support (EIP-7702).
  5. Senders MUST have sufficient balance to cover maximum possible gas fees plus transaction value.
  6. Transaction nonces MUST be correct and sequential per sender.
  7. Total inclusion gas (using the maximum of intrinsic gas and EIP-7623 floor cost) MUST not exceed the block's declared gas_used.
  8. Total blob gas MUST match the declared blob_gas_used.
  9. Withdrawal trie root MUST match its respective header field.

When calculating inclusion gas, the implementation uses the maximum of the regular intrinsic gas cost and the EIP-7623 calldata floor cost, which ensures proper accounting for calldata gas regardless of execution outcome.

Block Execution with State Snapshots

After a block passes static validation, execution proceeds with the pre-charged transaction senders:

def apply_body(
    block_env: vm.BlockEnvironment,
    transactions: Tuple[Union[LegacyTransaction, Bytes], ...],
    withdrawals: Tuple[Withdrawal, ...],
) -> vm.BlockOutput:
    block_output = vm.BlockOutput()

    # Process system transactions first (beacon roots, history storage)
    process_system_transaction(
        block_env=block_env,
        target_address=BEACON_ROOTS_ADDRESS,
        data=block_env.parent_beacon_block_root,
    )

    process_system_transaction(
        block_env=block_env,
        target_address=HISTORY_STORAGE_ADDRESS,
        data=block_env.block_hashes[-1],  # The parent hash
    )

    # Process user transactions
    process_transactions(block_env, block_output, transactions)

    process_withdrawals(block_env, block_output, withdrawals)

    # Process requests (deposits, withdrawals, consolidations)
    process_general_purpose_requests(
        block_env=block_env,
        block_output=block_output,
    )

    return block_output

def process_transactions(
    block_env: vm.BlockEnvironment,
    block_output: vm.BlockOutput,
    transactions: Tuple[Union[LegacyTransaction, Bytes], ...],
) -> None:
    # Take a block-level snapshot of the state before transaction execution
    begin_transaction(block_env.state)

    # Decode transactions
    decoded_transactions = [decode_transaction(tx) for tx in transactions]

    # Pre-charge senders for maximum possible gas fees upfront
    for tx in decoded_transactions:
        deduct_max_tx_fee_from_sender_balance(block_env, tx)

    # Execute each transaction
    for i, tx in enumerate(decoded_transactions):
        process_transaction(block_env, block_output, tx, Uint(i))
        # Stop processing if execution is already reverted
        if block_output.execution_reverted:
            break

    # Validate declared gas used against actual gas used
    block_output.execution_reverted = (
        block_output.execution_reverted
        or block_output.block_gas_used != block_env.block_gas_used
    )

    # If execution is reverted, reset all outputs and rollback the state
    if block_output.execution_reverted:
        rollback_transaction(block_env.state)
        block_output.block_gas_used = Uint(0)
        block_output.transactions_trie = Trie(secured=False, default=None)
        block_output.receipts_trie = Trie(secured=False, default=None)
        block_output.receipt_keys = ()
        block_output.block_logs = ()
        block_output.requests = []
        block_output.execution_reverted = True
    else:
        # Commit the state if execution is valid
        commit_transaction(block_env.state)

During block execution:

  1. Clients MUST create a block-level snapshot before any transaction execution occurs. This happens after handling the system contracts from EIP-4788 and EIP-2935.

  2. Maximum fees MUST be deducted from sender balances upfront by:

def deduct_max_tx_fee_from_sender_balance(block_env, tx):
    effective_gas_price = calculate_effective_gas_price(tx, block_env.base_fee_per_gas)
    blob_gas_price = calculate_blob_gas_price(block_env.excess_blob_gas)
    blob_gas_used = calculate_total_blob_gas(tx)
    max_gas_fee = tx.gas * effective_gas_price + blob_gas_used * blob_gas_price
    sender = recover_sender(block_env.chain_id, tx)
    sender_account = get_account(block_env.state, sender)
    set_account_balance(block_env.state, sender, sender_account.balance - U256(max_gas_fee))
  1. Transactions are executed sequentially until executing a transaction would cause the block to exceed the gas limit.

  2. After execution, clients MUST verify that the total gas used matches the declared gas_used in the block header.

  3. If the actual gas used doesn't match the declared value, clients MUST:

  4. Set execution_reverted to True
  5. Roll back to the snapshot taken before execution
  6. Reset all execution outputs (receipts, logs, etc.)
  7. Return zero gas used

  8. The block itself remains valid, but execution outputs are not applied to the state.

  9. General purpose requests as defined in EIP-7685 are skipped when execution is reverted.

  10. The execution outputs (last_transactions_root, last_receipt_root, last_block_logs_bloom, last_requests_hash, last_execution_reverted) are updated in the chain state based on the execution results, and will be verified in subsequent blocks.

Execution SHALL be stopped and the payload reverted after exceeding the block gas limit:

def process_transaction(...)
  if block_output.block_gas_used + tx.gas > block_env.block_gas_limit:
        block_output.execution_reverted = True
        return
  ...

Rationale

Deferred Execution Outputs

The core innovation of deferring execution outputs to the next block enables static and stateful validation without requiring immediate execution. The pre_state_root provides a cryptographically verifiable starting point for validation, while parent execution outputs create a chain of deferred execution results that maintains the integrity of the blockchain state.

This approach eliminates the execution bottleneck in the validation pipeline by allowing validators to attest to a block's validity based on its structure and the pre-charged transaction fees, without waiting for execution results.

Pre-Charging Mechanism

Pre-charging senders with the maximum possible fees before execution provides a crucial guarantee that transactions have sufficient balance to be included in the block. This mechanism is compatible with existing fee models, including EIP-1559 dynamic fee transactions and EIP-4844 blob transactions.

By tracking sender balances and nonces during validation, the protocol can enforce transaction validity without execution, enabling earlier block attestation.

State Snapshot Architecture

The block-level snapshot mechanism provides a way to revert execution when necessary. This approach allows clients to roll back the entire block's execution if the actual gas used does not match the declared gas in the header, without invalidating the block structure itself.

This provides two key benefits:

  1. It allows validators to attest to blocks before execution is complete
  2. It ensures execution is eventually performed correctly, with economic penalties for incorrect gas declarations

Execution Reversion Handling

When a block's execution is reverted due to gas mismatch:

  1. The block itself remains valid and part of the canonical chain
  2. The last_execution_reverted flag is set to True in chain state
  3. The next block must include this flag as parent_execution_reverted
  4. Base fee calculations for subsequent blocks use 0 as parent gas used when parent execution was reverted

Backwards Compatibility

This EIP requires a hard fork, as it alters the block validation and execution process.

Security Considerations

Execution Correctness Guarantees

The protocol ensures execution correctness through these primary mechanisms:

  1. Deferred execution outputs MUST match in subsequent blocks, creating a chain of verifiable execution results.
  2. State rollback MUST occur if the actual gas used doesn't match the declared value, providing an economic incentive for correct gas declaration.
  3. The parent_execution_reverted flag ensures that blocks acknowledge when parent execution has been reverted, maintaining chain integrity.
  4. Static and stateful validation guarantees that all transactions are properly authorized and senders have sufficient funds for maximum fees.

Base Fee Dynamics with Reverted Execution

When a block's execution is reverted, the next block's base fee calculation treats the parent's gas used as zero, regardless of what was declared in the header. This ensures that base fee adjustments remain responsive to actual chain usage, and prevents manipulation of the fee market through incorrect gas declarations.

Data Availability and Economic Incentives

Block proposers MUST declare correct gas usage or lose transaction fees when execution is reverted. This aligns incentives for correct gas declaration and ensures execution integrity.

Even when a block's execution is reverted due to incorrect gas declaration, the transaction data (calldata, EIP-2930 access lists, and blob data) MUST still be stored by all nodes for syncing and block validation purposes. This requirement creates a potential attack vector where malicious actors could attempt to place large amounts of data on-chain at a reduced cost by intentionally invalidating blocks through incorrect gas declarations.

However, this attack is not economically sustainable for several reasons:

  1. Block proposers who invalidate blocks through incorrect gas declarations lose all execution layer rewards associated with the block.
  2. The attack requires control of block production, which is a scarce resource in the consensus layer.

The economic costs of forgoing block rewards significantly outweigh any potential benefits, making such attacks financially impractical under normal network conditions.

Copyright

Copyright and related rights waived via CC0.