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.
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.
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:
pre_state_root
: Represents the state root before execution (checked against the post-execution state of the parent block)parent_receipt_root
: Receipt root from the parent block (deferred execution output)parent_bloom
: Logs bloom from the parent block (deferred execution output)parent_requests_hash
: Hash of requests from the parent block (deferred execution output)parent_execution_reverted
: Indicates whether the parent block's execution was revertedA 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.
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.
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:
pre_state_root
MUST match the current state root to ensure proper state transition.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.
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:
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.
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))
Transactions are executed sequentially until executing a transaction would cause the block to exceed the gas limit.
After execution, clients MUST verify that the total gas used matches the declared gas_used
in the block header.
If the actual gas used doesn't match the declared value, clients MUST:
execution_reverted
to True
Return zero gas used
The block itself remains valid, but execution outputs are not applied to the state.
General purpose requests as defined in EIP-7685 are skipped when execution is reverted.
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
...
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 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.
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:
When a block's execution is reverted due to gas mismatch:
last_execution_reverted
flag is set to True
in chain stateparent_execution_reverted
This EIP requires a hard fork, as it alters the block validation and execution process.
The protocol ensures execution correctness through these primary mechanisms:
parent_execution_reverted
flag ensures that blocks acknowledge when parent execution has been reverted, maintaining chain integrity.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.
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:
The economic costs of forgoing block rewards significantly outweigh any potential benefits, making such attacks financially impractical under normal network conditions.
Copyright and related rights waived via CC0.