Stateless block validation¶
Validation of the commitments produced by executing a block body: gas and blob-gas accounting, the post-state root, the receipts root and logs bloom, the EIP-7685 execution requests, and the EIP-7928 block access list.
function validate_executed_block¶
Checks every executed-block commitment against the header and payload,
throwing the specific InvalidBlock reason on the first failure:
gas/blob-gas totals, post-state root, receipts root, logs bloom, and
block-access-list bytes and size (Amsterdam+). Execution-request bytes
(Prague+) are validated where they are collected.
function validate_executed_block(block : Block, result : BlockExecutionResult) -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let header = block.header;
validation_debug_capture_block_gas(
result.header_gas_used,
header.gas_used,
result.execution_gas_used,
result.state_gas_used,
);
if result.header_gas_used != header.gas_used then {
fatal_error(InvalidGasUsed)
};
if (profile.fork >= Cancun) & (result.blob_gas_used != header.blob_gas_used) then {
fatal_error(InvalidBlobGasUsed)
};
let poststate = compute_state_root();
if poststate != header.state_root then {
fatal_error(InvalidStateRoot)
};
if result.receipts_root != header.receipts_root then {
fatal_error(InvalidReceiptsRoot)
};
let logs_bloom_matches = block_logs_bloom_matches(result.logs, header.logs_bloom);
let logs_bloom_mismatch = not_bool(logs_bloom_matches);
if logs_bloom_mismatch then {
fatal_error(InvalidLogsBloom)
};
if profile.fork >= Amsterdam then {
validate_block_access_list(block.body.block_access_list, execution_profile.gas.block_limit)
}
}Compares the block logs bloom of one consecutive retained log range with the payload-header commitment (YP §4.4.1). The range covers exactly the logs retained by the block's transaction receipts.
function block_logs_bloom_matches(logs : LogSeriesRef, reference : LogsBloomRef) -> bool = {
let logs_bloom = bloom_add_logs(EMPTY_LOGS_BLOOM, logs);
logs_bloom_matches_ref(logs_bloom, reference)
}The post-state root: traverses every changed account in the kernel's block-level overlay, recomputes each touched account's storage root from its changed slots (zero-valued slots delete), re-encodes the account leaf (empty accounts delete, per EIP-161), and streams the ordered updates into the parent state root via trie_root.
function compute_state_root() -> hash = {
acct_block_iter_begin();
let updates = ChangedAccountTrieUpdates();
trie_root(k_parent_state_root, updates).root
}function fatal_error(_reason) = exit(())val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))Validates the canonical EIP-7928 BAL directly against the host recorder.
function validate_block_access_list(
bytes : StatelessInputSliceAtMost(block_access_list_length_bound),
block_gas_limit : block_gas_limit,
) -> (
unit
) = {
bal_prepare_iter();
let root = rlp_single_ref(bytes);
let accounts_cursor = bal_ref_cursor(root);
let bal_items = bal_validate_accounts(accounts_cursor);
let remaining_event = bal_iter_next();
match remaining_event {
BalEmpty(_) => (),
_ => fatal_error(InvalidBlockAccessList),
};
if BLOCK_ACCESS_LIST_ITEM_GAS * bal_items > block_gas_limit then {
fatal_error(BlockAccessListTooLarge)
}
}function validation_debug_capture_block_gas(_actual, _expected, _execution, _state) -> unit =
()EIP-7954 code/initcode size bump (65536/131072).
let Amsterdam : int(amsterdam_fork_value) = sizeof(amsterdam_fork_value)EIP-1153/4844; precompiles 0x01-0x0a.
let Cancun : int(first_blob_fork_value) = sizeof(first_blob_fork_value)The active protocol policy and all gas limits derived from the executing header, selected together while decoding the stateless input.
register k_execution_profile : ExecutionProfile = DEFAULT_EXECUTION_PROFILEA block: header plus body.
struct Block = {
header : BlockHeader,
body : BlockBody,
}Everything block validation needs from a successfully executed body: gas and blob-gas totals, the post-execution receipts root, and the block's retained receipt-log range. EIP-7685 requests are validated where they are collected rather than carried in the result.
struct BlockExecutionResult = {
header_gas_used : block_gas,
execution_gas_used : block_gas,
state_gas_used : block_gas,
blob_gas_used : blob_gas_used,
first_tx_recipient : address,
receipts_root : hash,
logs : LogSeriesRef,
}The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}Blob gas used by one supported block. The existential count retains that
every value is exactly a multiple of GAS_PER_BLOB; profile-indexed
decoding applies the selected schedule's tighter range before values enter
this heterogeneous header domain.
type blob_gas_used = {
'count,
0 <= 'count
& 'count <= bpo2_blob_max_count.
int(gas_per_blob_value * 'count)
}Available gas in a running EVM frame. Every admitted transaction gas
limit originates in the execution payload's SSZ uint64 gas-limit
domain, and child frames can only receive gas from their parent.
type gas = range(0, 2 ^ 64 - 1)Constants¶
Stable stage identifiers for optional native validation diagnostics. These are validation metadata, not performance instrumentation.
let VALIDATION_STAGE_DECODE_INPUT¶
let VALIDATION_STAGE_DECODE_INPUT : validation_stage = 1Stable identifier for the validation stage that raised a block error. This diagnostic metadata is a bounded integer, not a protocol bitvector.
type validation_stage = range(0, 255)let VALIDATION_STAGE_INDEX_WITNESS¶
let VALIDATION_STAGE_INDEX_WITNESS : validation_stage = 2Stable identifier for the validation stage that raised a block error. This diagnostic metadata is a bounded integer, not a protocol bitvector.
type validation_stage = range(0, 255)let VALIDATION_STAGE_VALIDATE_PAYLOAD¶
let VALIDATION_STAGE_VALIDATE_PAYLOAD : validation_stage = 3Stable identifier for the validation stage that raised a block error. This diagnostic metadata is a bounded integer, not a protocol bitvector.
type validation_stage = range(0, 255)let VALIDATION_STAGE_EXECUTE_BLOCK¶
let VALIDATION_STAGE_EXECUTE_BLOCK : validation_stage = 4Stable identifier for the validation stage that raised a block error. This diagnostic metadata is a bounded integer, not a protocol bitvector.
type validation_stage = range(0, 255)let VALIDATION_STAGE_VALIDATE_RESULT¶
let VALIDATION_STAGE_VALIDATE_RESULT : validation_stage = 5Stable identifier for the validation stage that raised a block error. This diagnostic metadata is a bounded integer, not a protocol bitvector.
type validation_stage = range(0, 255)The verification pipeline¶
The single entry called for every stateless input.
function verify_stateless_payload¶
The stateless verification pipeline: decode the semantic envelope,
index the witness, validate the payload commitments, execute the
block body one transaction at a time, and validate the execution results.
Any failure terminates through fatal_error; normal return means valid.
function verify_stateless_payload(input_ref : StatelessInputRef) -> unit = {
scratch_reset();
let input = decode_stateless_input(input_ref);
let witness = index_execution_witness(input_ref);
validate_execution_payload(input, input_ref, witness);
let block = input.payload.block;
let result = execute_block_body(block.body, input_ref);
validate_executed_block(block, result)
}Decodes the semantic payload structure — header, chain config, body references — without touching an encoded transaction or withdrawal element, and installs the header and chain parameters in the kernel.
function decode_stateless_input(input_ref : StatelessInputRef) -> StatelessInput = {
let payload = input_ref.execution_payload;
let header = decode_block_header_ssz(input_ref);
let chain_config = decode_chain_config(input_ref.chain_config, header.number, header.timestamp);
k_set_header(header);
k_chain_id = chain_config.chain_id;
k_execution_profile = execution_profile_for(input_ref.protocol, header.gas_limit);
struct {
payload =
struct {
expected_block_hash = ssz_bytes32(input_ref.execution_payload, PL_BLOCK_HASH),
block =
struct {
header = header,
body =
struct {
transactions = input_ref.transactions,
withdrawals = input_ref.withdrawals,
block_access_list = input_ref.block_access_list,
},
},
},
chain_config = chain_config,
}
}Executes a block body end to end: block-start system calls, the transaction loop, block-end state effects, and request validation; invalid execution throws immediately, while successful execution returns the accumulated BlockExecutionResult.
function execute_block_body(body : BlockBody, input_ref : StatelessInputRef) -> BlockExecutionResult = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
bal_reset();
k_current_transaction_epoch = 0;
warm_reset(k_current_transaction_epoch);
run_block_start_system_calls();
let result = execute_block_transactions(body.transactions, input_ref.public_keys, input_ref.deposits);
let post_tx_index = body.transactions.count + 1;
k_current_transaction_epoch = post_tx_index;
warm_reset(k_current_transaction_epoch);
apply_block_end_state(body);
if profile.fork >= Prague then {
validate_execution_requests(input_ref)
};
result
}Indexes the source-backed witness (nodes, codes, headers) and installs its authenticated parent-state anchor; no witness list is materialized.
function index_execution_witness(input_ref : StatelessInputRef) -> WitnessContext = {
nodedb_reset();
index_witness_nodes(input_ref.witness_state);
index_witness_codes(input_ref.witness_codes);
let witness = index_witness_headers(input_ref.witness_headers);
k_parent_state_root = witness.parent_state_root;
witness
}Empties the arena (per-block lifetime).
function scratch_reset() -> unit = {
scratch_arena = EMPTY_SCRATCH_SLICE;
host_scratch_truncate(0)
}Checks every executed-block commitment against the header and payload,
throwing the specific InvalidBlock reason on the first failure:
gas/blob-gas totals, post-state root, receipts root, logs bloom, and
block-access-list bytes and size (Amsterdam+). Execution-request bytes
(Prague+) are validated where they are collected.
function validate_executed_block(block : Block, result : BlockExecutionResult) -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let header = block.header;
validation_debug_capture_block_gas(
result.header_gas_used,
header.gas_used,
result.execution_gas_used,
result.state_gas_used,
);
if result.header_gas_used != header.gas_used then {
fatal_error(InvalidGasUsed)
};
if (profile.fork >= Cancun) & (result.blob_gas_used != header.blob_gas_used) then {
fatal_error(InvalidBlobGasUsed)
};
let poststate = compute_state_root();
if poststate != header.state_root then {
fatal_error(InvalidStateRoot)
};
if result.receipts_root != header.receipts_root then {
fatal_error(InvalidReceiptsRoot)
};
let logs_bloom_matches = block_logs_bloom_matches(result.logs, header.logs_bloom);
let logs_bloom_mismatch = not_bool(logs_bloom_matches);
if logs_bloom_mismatch then {
fatal_error(InvalidLogsBloom)
};
if profile.fork >= Amsterdam then {
validate_block_access_list(block.body.block_access_list, execution_profile.gas.block_limit)
}
}Validates every commitment checkable before transaction decoding: parent linkage, gas and blob-gas header rules, the transactions and withdrawals roots, the requests hash, and the block hash. The supplied block access list is hashed once for the header; post-execution validation compares its bytes against the canonical reconstruction.
function validate_execution_payload(
input : StatelessInput,
input_ref : StatelessInputRef,
witness : WitnessContext,
) -> (
unit
) = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let payload = input.payload;
let block = payload.block;
let header = block.header;
let body = block.body;
if header.gas_limit < header.gas_used then {
fatal_error(InvalidGasUsed)
};
if witness.parent_hash != header.parent_hash then {
fatal_error(InvalidParentHash)
};
let expected_excess_blob_gas = expected_payload_excess_blob_gas(witness);
if (profile.fork >= Cancun) & (header.excess_blob_gas != expected_excess_blob_gas) then {
fatal_error(InvalidExcessBlobGas)
};
if profile.fork >= Paris then {
let transactions_root = transaction_trie_root(body.transactions);
let withdrawals_root =
if profile.fork >= Shanghai then withdrawals_trie_root(body.withdrawals) else EMPTY_TRIE_ROOT;
let requests_hash =
if profile.fork >= Prague then execution_requests_hash(input_ref) else ZERO_HASH;
let block_access_list_hash =
if profile.fork >= Amsterdam then keccak256(body.block_access_list) else ZERO_HASH;
let computed_block_hash = block_header_hash(
header,
transactions_root,
withdrawals_root,
requests_hash,
block_access_list_hash,
);
if computed_block_hash != payload.expected_block_hash then {
fatal_error(InvalidBlockHash)
}
}
}Every variable region of the input, resolved once before decoding. Consumers receive explicit source spans instead of re-reading nested SSZ offset tables.
struct StatelessInputRef = {
protocol : ProtocolProfile,
new_payload_request : StatelessInputSlice,
execution_payload : StatelessInputSliceAtLeast(540),
versioned_hashes : StatelessInputSlice,
deposits : StatelessInputSlice,
withdrawal_requests : StatelessInputSlice,
consolidation_requests : StatelessInputSlice,
builder_deposit_requests : StatelessInputSlice,
builder_exit_requests : StatelessInputSlice,
extra_data : StatelessInputSliceAtMost(extra_data_length_bound),
transactions : TransactionListRef,
withdrawals : WithdrawalListRef,
block_access_list : StatelessInputSliceAtMost(block_access_list_length_bound),
witness_state : WitnessNodeListRef,
witness_codes : WitnessCodeListRef,
witness_headers : WitnessHeaderListRef,
chain_config : StatelessInputSlice,
public_keys : StatelessInputSlice,
}