The block driver¶
Block-start system calls, the transaction loop with block-gas accounting, withdrawals, and block-end request collection.
type BlockExecutionResult¶
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,
}A consecutive transaction-local view into the block-lifetime host log store. Reverted frame records are removed before this view is captured.
struct LogSeriesRef = {
start : log_store_index,
count : log_store_index,
}A 20-byte account address (YP ยง4.1), in canonical protocol byte order.
type address = vector(20, inc, byte)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)
}Gas consumed by a block header. Provenance: the execution-payload SSZ
schema declares gas_used: uint64. The separate payload-validity rule
gas_used <= gas_limit is checked when the header is admitted.
type block_gas = range(0, block_gas_limit_bound)The common digest type used by trie, code, and block hashes.
type hash = b256type block_gas_usage_relation¶
The two independent block gas dimensions introduced by Amsterdam. Earlier
forks use only execution; state remains zero. Indexing the accumulator
by the concrete header limit makes an over-limit intermediate
unrepresentable after transaction admission.
type block_gas_usage_relation(
'limit : Int,
'execution : Int,
'state : Int,
'receipts : Int,
) -> Bool =
0 <= 'limit
& 'limit <= block_gas_limit_bound
& 0 <= 'execution
& 'execution <= 'limit
& 0 <= 'state
& 'state <= 'limit
& 0 <= 'receipts
& 'receipts <= 'execution + 'stateThe maximum block gas limit admitted by the execution-payload SSZ
uint64 field. Provenance: consensus ExecutionPayload.gas_limit and
Amsterdam SszExecutionPayload.gas_limit.
type block_gas_limit_bound : Int = ssz_uint_boundtype BlockGasUsageFields¶
The exact execution, state, and receipt gas accumulated under one block header gas limit.
struct BlockGasUsageFields(
'limit : Int,
'execution : Int,
'state : Int,
'receipts : Int,
), block_gas_usage_relation('limit, 'execution, 'state, 'receipts) = {
execution : int('execution),
state : int('state),
receipts : int('receipts),
}The two independent block gas dimensions introduced by Amsterdam. Earlier
forks use only execution; state remains zero. Indexing the accumulator
by the concrete header limit makes an over-limit intermediate
unrepresentable after transaction admission.
type block_gas_usage_relation(
'limit : Int,
'execution : Int,
'state : Int,
'receipts : Int,
) -> Bool =
0 <= 'limit
& 'limit <= block_gas_limit_bound
& 0 <= 'execution
& 'execution <= 'limit
& 0 <= 'state
& 'state <= 'limit
& 0 <= 'receipts
& 'receipts <= 'execution + 'statetype BlockGasUsageFor¶
A block gas accumulator existentially hiding its current totals while retaining their relationship to the concrete header limit.
type BlockGasUsageFor('limit : Int) = {
'execution 'state 'receipts,
block_gas_usage_relation('limit, 'execution, 'state, 'receipts).
BlockGasUsageFields('limit, 'execution, 'state, 'receipts)
}The exact execution, state, and receipt gas accumulated under one block header gas limit.
struct BlockGasUsageFields(
'limit : Int,
'execution : Int,
'state : Int,
'receipts : Int,
), block_gas_usage_relation('limit, 'execution, 'state, 'receipts) = {
execution : int('execution),
state : int('state),
receipts : int('receipts),
}The two independent block gas dimensions introduced by Amsterdam. Earlier
forks use only execution; state remains zero. Indexing the accumulator
by the concrete header limit makes an over-limit intermediate
unrepresentable after transaction admission.
type block_gas_usage_relation(
'limit : Int,
'execution : Int,
'state : Int,
'receipts : Int,
) -> Bool =
0 <= 'limit
& 'limit <= block_gas_limit_bound
& 0 <= 'execution
& 'execution <= 'limit
& 0 <= 'state
& 'state <= 'limit
& 0 <= 'receipts
& 'receipts <= 'execution + 'statefunction block_gas_usage_empty¶
function block_gas_usage_empty(_limit) =
struct { execution = 0, state = 0, receipts = 0 }function block_gas_usage_empty(_limit) =
struct { execution = 0, state = 0, receipts = 0 }function block_gas_usage_add¶
function block_gas_usage_add(usage, add_execution, add_state, add_receipt) =
struct {
execution = usage.execution + add_execution,
state = usage.state + add_state,
receipts = usage.receipts + add_receipt,
}function block_gas_usage_add(usage, add_execution, add_state, add_receipt) =
struct {
execution = usage.execution + add_execution,
state = usage.state + add_state,
receipts = usage.receipts + add_receipt,
}let PRE_MERGE_BLOCK_REWARD¶
let PRE_MERGE_BLOCK_REWARD = unsigned(0x1bc16d674ec80000)converts a bit vector of length $n$ to an integer in the range $0$ to $2^n - 1$.
val unsigned = pure {ocaml: "uint", lem: "uint", interpreter: "uint", coq: "uint", lean: "BitVec.toNatInt", _: "sail_unsigned"}: forall ('n : Int).
bits('n) -> range(0, 2 ^ 'n - 1)function run_block_start_system_calls¶
The block-start writes: beacon root (Cancun+, EIP-4788) and parent hash history (Prague+, EIP-2935).
function run_block_start_system_calls() -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
if profile.fork >= Cancun then {
system_call(BEACON_ROOTS_ADDR, k_header.parent_beacon_block_root)
};
if profile.fork >= Prague then {
system_call(HISTORY_STORAGE_ADDR, k_header.parent_hash)
}
}Issues one unchecked block-start system call: a 30M-gas frame from
SYSTEM_ADDRESS with a 32-byte input; skipped when the target has no
code, and its output is discarded.
function system_call(tgt : address, input : hash) -> unit = {
let code_hash = k_code_key(tgt);
if code_hash == KECCAK_EMPTY then {
return () /* system contract absent -> skip (EIP-4788 / EIP-2935) */
};
let code = code_db_resolve(code_hash);
/* Freeze the word below the system-call frame, so the child's memory
* writes cannot mutate its calldata. */
let initial_memory_base = MEMORY_BASE_ZERO;
let initial_memory_height = MEMORY_HEIGHT_ZERO;
let input_range : MemoryRange = memory_range(0, SYSTEM_CALL_INPUT_LENGTH);
let expanded_memory = expand_memory(initial_memory_base, initial_memory_height, input_range.len);
let input_word = hash_to_word(input);
mem_store(initial_memory_base, input_range.off, input_word);
let input_slice = active_memory_slice(initial_memory_base, expanded_memory, input_range.off, input_range.len);
let child_memory_base = memory_absolute(initial_memory_base, expanded_memory);
let memory_input = evm_memory_slice(input_slice.bytes, input_slice.len);
let frame_input = MemoryCalldata(memory_input);
let (_, _, _, _, status, _) = run_system_call_frame(tgt, code, frame_input, child_memory_base);
let succeeded = frame_succeeded(status);
let failed = not_bool(succeeded);
if failed then {
k_journal_revert()
} else {
k_journal_commit()
};
k_tx_merge() /* system call is a top-level boundary: merge its storage */
}let BEACON_ROOTS_ADDR : address = address_from_bits(0x000000000000000000000000000f3df6d732807ef1319fb7b8bb8522d0beac02)EIP-1153/4844; precompiles 0x01-0x0a.
let Cancun : int(first_blob_fork_value) = sizeof(first_blob_fork_value)let HISTORY_STORAGE_ADDR : address = address_from_bits(
0x0000000000000000000000000000f90827f1c53a10cb7a02335b175320002935,
)EIP-7623 calldata floor; BLS precompiles 0x0b-0x11.
let Prague : int(prague_fork_value) = sizeof(prague_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_PROFILEThe executing payload's header.
register k_header : BlockHeader =
struct {
number = 0,
timestamp = 0,
extra_data = EMPTY_STATELESS_INPUT_SLICE,
gas_limit = 0,
gas_used = 0,
prev_randao = ZERO_WORD,
base_fee = ZERO_WORD,
blob_gas_used = 0,
excess_blob_gas = 0,
state_root = ZERO_HASH,
receipts_root = ZERO_HASH,
logs_bloom = stateless_input_slice(0, 256),
fee_recipient = ZERO_ADDRESS,
parent_hash = ZERO_HASH,
parent_beacon_block_root = ZERO_HASH,
slot_number = 0,
}function execute_block_transactions¶
Executes the block's transactions in order, enforcing per-tx applicability and block gas/blob-gas availability (EIP-7778 block-gas accounting), accumulating receipts.
function execute_block_transactions(
transactions : TransactionListRef,
public_keys : StatelessInputSlice,
expected_deposits : StatelessInputSlice,
) -> (
BlockExecutionResult
) = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let gas_limits = execution_profile.gas;
let public_keys_length = public_keys.len;
let public_key_length = PUBLIC_KEY_LENGTH;
let public_key_count_value = public_keys_length / public_key_length;
if (public_key_count_value != transactions.count) |
(public_keys_length != public_key_count_value * public_key_length) then {
fatal_error(WitnessDeficient)
};
let (gas_limit as 'gas_limit) = gas_limits.block_limit;
var gas_usage : BlockGasUsageFor('gas_limit) = block_gas_usage_empty(gas_limit);
var blob_gas_acc : blob_gas_used = 0;
var tx0_to : address = ZERO_ADDRESS;
let records_start = receipt_store_begin();
let transaction_logs_start = logs_tx_start();
let transaction_logs_count = logs_tx_count();
let logs_start = log_store_index_add(transaction_logs_start, transaction_logs_count);
var remaining_deposits : StatelessInputSlice = expected_deposits;
var cursor = ssz_list_cursor(transactions);
var keys : StatelessInputSlice = public_keys;
let initial_cursor_empty = ssz_list_cursor_empty(cursor);
var cursor_has_item : bool = not_bool(initial_cursor_empty);
while cursor_has_item termination_measure(cursor.items.count - cursor.index) do {
let i = cursor.index;
let (transaction, next) = ssz_list_pop(cursor);
cursor = next;
let keys_fields = keys;
let keys_fields : StatelessInputSliceAtLeast(65) =
if public_key_length <= keys_fields.len then keys_fields else fatal_error(WitnessDeficient);
let public_key = sub_slice(keys_fields, 0, PUBLIC_KEY_LENGTH);
keys = slice_suffix(keys_fields, public_key_length);
let tx = decode_transaction(transaction, public_key);
k_current_transaction_epoch = i + 1;
if i == 0 then {
tx0_to = tx.recipient
};
/* A local binding opens the existential accumulator, retaining its
concrete execution/state indices for this admission step. */
let usage = gas_usage;
let available_execution_gas = gas_limit - usage.execution;
let available_state_gas = gas_limit - usage.state;
let allowance = transaction_gas_allowance(
tx.gas_limit,
gas_limits.transaction_total_limit,
gas_limits.transaction_regular_limit,
);
if profile.fork >= Amsterdam then {
if (available_execution_gas < allowance.regular) | (available_state_gas < allowance.total) then {
fatal_error(GasUsedExceedsLimit)
} else {
let tx_blob_gas : transaction_blob_gas = sizeof(gas_per_blob_value) * tx.blob_hashes.count;
let next_blob_gas : blob_gas_used = block_blob_gas_add(
profile.blob_schedule.max,
blob_gas_acc,
tx_blob_gas,
);
let receipt = process_transaction(tx, allowance);
let next_usage = block_gas_usage_add(usage, receipt.execution_gas, receipt.state_gas, receipt.gas_used);
gas_usage = next_usage;
receipt_store_append(receipt, next_usage.receipts, i);
if profile.fork >= Prague then {
remaining_deposits = authenticate_deposit_logs(receipt.logs, remaining_deposits)
};
blob_gas_acc = next_blob_gas
}
} else if available_execution_gas < allowance.total then {
fatal_error(GasUsedExceedsLimit)
} else {
let tx_blob_gas : transaction_blob_gas = sizeof(gas_per_blob_value) * tx.blob_hashes.count;
let next_blob_gas : blob_gas_used =
if profile.fork < Cancun
then blob_gas_acc
else block_blob_gas_add(profile.blob_schedule.max, blob_gas_acc, tx_blob_gas);
let receipt = process_transaction(tx, allowance);
let next_usage = block_gas_usage_add(usage, receipt.gas_used, 0, receipt.gas_used);
gas_usage = next_usage;
receipt_store_append(receipt, next_usage.receipts, i);
if profile.fork >= Prague then {
remaining_deposits = authenticate_deposit_logs(receipt.logs, remaining_deposits)
};
blob_gas_acc = next_blob_gas
};
let cursor_empty = ssz_list_cursor_empty(cursor);
cursor_has_item = not_bool(cursor_empty)
};
let remaining_deposits_length = region_slice_length(remaining_deposits);
if (profile.fork >= Prague) & (remaining_deposits_length != 0) then {
fatal_error(InvalidExecutionRequests)
};
let final_usage = gas_usage;
let header_gas_used =
if (profile.fork >= Amsterdam) & (final_usage.execution < final_usage.state)
then final_usage.state
else final_usage.execution;
let receipts_root = receipt_store_root(records_start, transactions.count);
let retained_logs_start = logs_tx_start();
let retained_logs_count = logs_tx_count();
let retained = log_store_index_add(retained_logs_start, retained_logs_count);
let logs_count : log_store_index =
if logs_start <= retained then retained - logs_start else 0;
struct {
header_gas_used = header_gas_used,
execution_gas_used = final_usage.execution,
state_gas_used = final_usage.state,
blob_gas_used = blob_gas_acc,
first_tx_recipient = tx0_to,
receipts_root = receipts_root,
logs = struct { start = logs_start, count = logs_count },
}
}Authenticates matching deposit logs in emission order and returns the unconsumed suffix of the expected request bytes.
function authenticate_deposit_logs(logs : LogSeriesRef, expected : StatelessInputSlice) -> StatelessInputSlice = {
var remaining : StatelessInputSlice = expected;
var offset : log_store_index = 0;
while offset < logs.count termination_measure(logs.count - offset) do {
let index = log_store_index_add(logs.start, offset);
let matches = deposit_log_matches(index);
if matches then {
let data = read_log_data(index);
remaining = authenticate_deposit_request(data, remaining)
};
offset = log_store_index_increment(offset)
};
remaining
}function blob_schedule(target, maximum, denominator) =
struct { target = target, max = maximum, base_fee_update_fraction = denominator }function block_blob_gas_add(maximum_count, accumulated, transaction) = {
let maximum = sizeof(gas_per_blob_value) * maximum_count;
if (accumulated <= maximum) & (transaction <= maximum - accumulated) then {
accumulated + transaction
} else {
fatal_error(BlobGasLimitExceeded)
}
}function block_gas_usage_add(usage, add_execution, add_state, add_receipt) =
struct {
execution = usage.execution + add_execution,
state = usage.state + add_state,
receipts = usage.receipts + add_receipt,
}function block_gas_usage_empty(_limit) =
struct { execution = 0, state = 0, receipts = 0 }function decode_transaction(transaction, public_key) = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
if transaction.len <= sizeof(transaction_length_bound) then {
rlp_decode_tx(transaction, public_key, profile.transaction_blob_limit)
} else {
fatal_error(InvalidConfig)
}
}function fatal_error(_reason) = exit(())Adds a relative log offset to its series start without wrapping.
function log_store_index_add(left : log_store_index, right : log_store_index) -> log_store_index =
if right <= sizeof(log_store_index_bound) - left then {
left + right
} else {
assert(false, "log store index overflow");
0
}Number of retained logs emitted by the current transaction.
val logs_tx_count = impure { c: "logs_tx_count" } : unit -> log_store_indexStart and length of the current transaction's retained log range.
val logs_tx_start = impure { c: "logs_tx_start" } : unit -> log_store_indexval not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))function process_transaction(tx, allowance) = {
k_tx_reset();
let validity = check_transaction_validity(tx, allowance);
let authorizations = prepare_authorizations(tx.authorizations);
let environment = tx_env(tx.sender, validity.gas_price, tx.blob_hashes);
k_set_tx(environment);
let upfront = apply_transaction_upfront_effects(tx, validity, authorizations);
let frame_result = run_transaction_frame(tx, validity, upfront, authorizations);
let receipt = settle_transaction(tx, validity, upfront.authorization_refund, frame_result);
receipt
}function receipt_store_append(receipt, cumulative_gas_used, _index) =
receipt_record_append(receipt, cumulative_gas_used)Opens the execution-ordered receipt record region for one block.
function receipt_store_begin() -> source_pointer =
scratch_begin()Computes the canonical receipts root over the retained records after the last transaction has executed, then releases the retained region.
function receipt_store_root(records_start : source_pointer, count : transaction_count) -> hash = {
let records = scratch_finish(records_start);
let root = indexed_receipt_trie_root(struct { bytes = records, count = count });
scratch_rewind(records_start);
root
}A cursor at the first element of a variable-element SSZ list.
function ssz_list_cursor forall ('maximum : Int), source_valid_length('maximum). (items : BoundedSszListRef('maximum)) -> (
BoundedSszListCursor('maximum)
) = {
let bytes = items.bytes;
let current =
if items.count != 0 then {
let first_offset = ssz_u32(bytes, 0);
ssz_offset_to_source_pointer(first_offset)
} else {
bytes.len
};
struct { items = items, index = 0, current = current }
}Whether the cursor has consumed all elements.
function ssz_list_cursor_empty forall ('maximum : Int), source_valid_length('maximum). (cursor :
BoundedSszListCursor('maximum)) -> (
bool
) =
cursor.items.count <= cursor.indexThe next element's span, and the advanced cursor.
function ssz_list_pop forall ('maximum : Int), source_valid_length('maximum) & 'maximum <= 2 ^ 30 - 1. (cursor :
BoundedSszListCursor('maximum)) -> (
(StatelessInputSlice, BoundedSszListCursor('maximum))
) = {
let count = cursor.items.count;
let index = cursor.index;
let next_index : range(0, 'maximum) =
if index < count then index + 1 else fatal_error(InvalidConfig);
let items = cursor.items;
let bytes = items.bytes;
let nat = bytes.len;
let next =
if next_index < items.count then {
let table_position = ssz_offset_table_position(next_index);
let next_offset = ssz_u32_in_slice(bytes, table_position);
ssz_offset_to_source_pointer(next_offset)
} else {
nat
};
let current_value = cursor.current;
let next_value = next;
if (current_value <= next_value) & (next_value <= nat) then {
let item_length = next_value - current_value;
if (items.max_item_length != 0) & (items.max_item_length < item_length) then {
fatal_error(InvalidConfig)
};
let item = sub_slice(bytes, current_value, item_length);
(item, struct { items = items, index = next_index, current = next })
} else {
fatal_error(InvalidConfig)
}
}The byte length of a stateless-input slice.
function stateless_input_slice_length(s : StatelessInputSlice) -> source_length =
s.lenfunction transaction_gas_allowance(value, total_limit, regular_limit) =
if total_limit < value then {
fatal_error(ExecutionInvalid)
} else {
transaction_gas_allowance_fields(value, total_limit, regular_limit)
}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)let PUBLIC_KEY_LENGTH : int(65) = 65EIP-7623 calldata floor; BLS precompiles 0x0b-0x11.
let Prague : int(prague_fork_value) = sizeof(prague_fork_value)let ZERO_ADDRESS : address = address_from_bits(0x0000000000000000000000000000000000000000000000000000000000000000)The current execution epoch: zero for pre-execution effects, transaction index plus one during transaction execution, and transaction count plus one for post-execution effects. EIP-2929 warmth and EIP-7928 BAL changes share this transaction-scoped identity.
register k_current_transaction_epoch : block_access_index = 0The 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_PROFILEEverything 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,
}A block gas accumulator existentially hiding its current totals while retaining their relationship to the concrete header limit.
type BlockGasUsageFor('limit : Int) = {
'execution 'state 'receipts,
block_gas_usage_relation('limit, 'execution, 'state, 'receipts).
BlockGasUsageFields('limit, 'execution, 'state, 'receipts)
}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,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}A stateless-input range of at least 'minimum bytes.
type StatelessInputSliceAtLeast('minimum : Int) = {
'off 'len,
stateless_input_valid_range('off, 'len) & 0 <= 'minimum & 'minimum <= 'len.
StatelessInputSliceFields('off, 'len)
}A schema-bounded source reference to encoded transaction envelopes.
type TransactionListRef = BoundedSszListRef(transaction_count_bound)A 20-byte account address (YP ยง4.1), in canonical protocol byte order.
type address = vector(20, inc, byte)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)Blob gas charged per blob, 2^17 (EIP-4844).
type gas_per_blob_value : Int = 2 ^ 17A position in the block-lifetime host log store.
type log_store_index = range(0, log_store_index_bound)Amsterdam's per-frame state-gas reservoir. The transaction's total gas
allowance remains in the execution payload's uint64 domain; only the
regular-gas portion and state-gas spill into that portion are capped by
EIP-7825.
type state_gas = range(0, 2 ^ 64 - 1)Blob gas contributed by one transaction. Profile-indexed blob-hash decoding establishes the selected limit before this derived quantity is widened to the common transaction-cost domain.
type transaction_blob_gas = {
'count,
0 <= 'count
& 'count <= prague_blob_max_count.
int(gas_per_blob_value * 'count)
}function apply_withdrawals¶
Credits every withdrawal's recipient with its amount in gwei (EIP-4895); withdrawals cannot fail and charge no gas.
function apply_withdrawals(withdrawals : WithdrawalListRef) -> unit = {
var rest = withdrawals;
while rest.count != 0 termination_measure(rest.count) do {
let (withdrawal_ref, tail) = ssz_fixed_list_pop(rest, WD_SIZE);
rest = tail;
let withdrawal = decode_withdrawal(withdrawal_ref);
let amount_in_wei = withdrawal.amount * 1000000000;
k_add_balance(withdrawal.address, amount_in_wei)
}
}Decodes one fixed-layout SSZ withdrawal element.
function decode_withdrawal(withdrawal : StatelessInputSlice) -> Withdrawal = {
struct {
index = decode_ssz_uint(withdrawal, WD_INDEX),
validator_index = decode_ssz_uint(withdrawal, WD_VALIDATOR_INDEX),
address = ssz_addr(withdrawal, WD_ADDRESS),
amount = decode_ssz_uint(withdrawal, WD_AMOUNT),
}
}function k_add_balance(a, v) = {
let cur = k_aload(a);
let value_is_zero = word_is_zero(v);
let value_is_nonzero = not_bool(value_is_zero);
if value_is_nonzero then {
let balance = alu_add(cur.info.balance, v);
store_account_info(a, cur, { cur.info with balance = balance })
}
}function ssz_fixed_list_pop(items, item_size) = {
let bytes = items.bytes;
let items_length = bytes.len;
let width = item_size;
let count = items.count;
if (0 < count) & (width <= items_length) then {
let item = sub_slice(bytes, 0, item_size);
let rest : BoundedSszListRef('maximum) = struct {
bytes = slice_suffix(bytes, width),
count = count - 1,
max_item_length = items.max_item_length,
};
(item, rest)
} else {
fatal_error(InvalidConfig)
}
}let WD_SIZE : int(44) = 44A schema-bounded source reference to withdrawals.
type WithdrawalListRef = BoundedSszListRef(withdrawal_count_bound)A 20-byte account address (YP ยง4.1), in canonical protocol byte order.
type address = vector(20, inc, byte)function apply_block_end_state¶
The block-end state effects: withdrawals (Shanghai+, EIP-4895), the pre-merge static block reward before Paris, and the final merge into the block layer.
function apply_block_end_state(body : BlockBody) -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
if profile.fork >= Shanghai then {
apply_withdrawals(body.withdrawals)
};
if profile.fork < Paris then {
let coinbase = k_coinbase();
k_add_balance(coinbase, PRE_MERGE_BLOCK_REWARD)
};
k_tx_merge()
}Credits every withdrawal's recipient with its amount in gwei (EIP-4895); withdrawals cannot fail and charge no gas.
function apply_withdrawals(withdrawals : WithdrawalListRef) -> unit = {
var rest = withdrawals;
while rest.count != 0 termination_measure(rest.count) do {
let (withdrawal_ref, tail) = ssz_fixed_list_pop(rest, WD_SIZE);
rest = tail;
let withdrawal = decode_withdrawal(withdrawal_ref);
let amount_in_wei = withdrawal.amount * 1000000000;
k_add_balance(withdrawal.address, amount_in_wei)
}
}function k_add_balance(a, v) = {
let cur = k_aload(a);
let value_is_zero = word_is_zero(v);
let value_is_nonzero = not_bool(value_is_zero);
if value_is_nonzero then {
let balance = alu_add(cur.info.balance, v);
store_account_info(a, cur, { cur.info with balance = balance })
}
}The block's fee recipient (COINBASE).
function k_coinbase() -> address = k_header.fee_recipientThe transaction-end merge: drains the transaction overlays into the block layer, applying the fork-specific selfdestruct clearing rule, storage-clear generations, and recording nonce/balance/code/storage changes for the EIP-7928 block access list. Lifecycle flags reset as rows merge.
function k_tx_merge() -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let semantics = transaction_merge_semantics(profile.fork);
var more : bool = true;
/* Each pop advances a cursor over a protocol-sized host table. */
while more termination_measure(2 ^ 64) do {
let popped_account = acct_tx_pop();
match popped_account {
AcctTxPopRow(e) => {
var curr : Account = e.value.curr;
let deleted = account_deleted_at_tx_end(semantics, curr);
if deleted then {
let cleared_account =
if semantics.preserve_selfdestruct_balance
then account_clear_preserving_balance(curr)
else account_delete(curr);
curr = cleared_account;
/* The account's active transaction generation belongs to
the deleted incarnation. Retire it before the storage
drain so its writes cannot be reinserted after the
block-layer clear. */
storage_tx_clear(e.addr)
};
let original_storage_retained = not_bool(e.value.orig.storage_cleared);
if deleted | (curr.storage_cleared & original_storage_retained) then {
storage_block_clear(e.addr)
};
if curr.info.nonce != e.value.orig.info.nonce then {
bal_nonce_change(k_current_transaction_epoch, e.addr, curr.info.nonce)
};
if curr.info.balance != e.value.orig.info.balance then {
bal_balance_change(k_current_transaction_epoch, e.addr, curr.info.balance)
};
if curr.info.code_hash != e.value.orig.info.code_hash then {
bal_code_change(k_current_transaction_epoch, e.addr, curr.info.code_hash)
};
curr = { curr with created = false, selfdestructed = false };
let changed = account_changed(curr, e.value.orig);
if changed then {
acct_block_write(struct { addr = e.addr, value = struct { curr = curr, orig = e.value.orig } })
}
},
AcctTxPopExhausted(_) => more = false,
}
};
more = true;
/* Each pop advances a cursor over a protocol-sized host table. */
while more termination_measure(2 ^ 64) do {
let popped_storage = storage_tx_pop();
match popped_storage {
StorageTxPopRow(e) => {
let account = acct_block_get(e.key.addr);
if account.found then {
let acc = account.account;
if acc.present & e.value.curr != e.value.orig then {
bal_storage_change(k_current_transaction_epoch, e.key.addr, e.key.slot, e.value.curr);
storage_block_put(e)
}
}
},
StorageTxPopExhausted(_) => more = false,
}
};
storage_tx_reset();
acct_tx_reset()
}let PRE_MERGE_BLOCK_REWARD = unsigned(0x1bc16d674ec80000)EIP-4399 PREVRANDAO replaces DIFFICULTY.
let Paris : int(paris_fork_value) = sizeof(paris_fork_value)EIP-3651 warm coinbase, EIP-3855 PUSH0, EIP-3860 initcode.
let Shanghai : int(shanghai_fork_value) = sizeof(shanghai_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_PROFILEThe block body. The semantic structure is explicit while its potentially large fields stay source-backed until individual elements are needed.
struct BlockBody = {
transactions : TransactionListRef,
withdrawals : WithdrawalListRef,
block_access_list : StatelessInputSliceAtMost(block_access_list_length_bound),
}function execute_block_body¶
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
}The block-end state effects: withdrawals (Shanghai+, EIP-4895), the pre-merge static block reward before Paris, and the final merge into the block layer.
function apply_block_end_state(body : BlockBody) -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
if profile.fork >= Shanghai then {
apply_withdrawals(body.withdrawals)
};
if profile.fork < Paris then {
let coinbase = k_coinbase();
k_add_balance(coinbase, PRE_MERGE_BLOCK_REWARD)
};
k_tx_merge()
}Clears the block-access-list recorder (start of block execution).
val bal_reset = impure { c: "bal_reset" } : unit -> unitExecutes the block's transactions in order, enforcing per-tx applicability and block gas/blob-gas availability (EIP-7778 block-gas accounting), accumulating receipts.
function execute_block_transactions(
transactions : TransactionListRef,
public_keys : StatelessInputSlice,
expected_deposits : StatelessInputSlice,
) -> (
BlockExecutionResult
) = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let gas_limits = execution_profile.gas;
let public_keys_length = public_keys.len;
let public_key_length = PUBLIC_KEY_LENGTH;
let public_key_count_value = public_keys_length / public_key_length;
if (public_key_count_value != transactions.count) |
(public_keys_length != public_key_count_value * public_key_length) then {
fatal_error(WitnessDeficient)
};
let (gas_limit as 'gas_limit) = gas_limits.block_limit;
var gas_usage : BlockGasUsageFor('gas_limit) = block_gas_usage_empty(gas_limit);
var blob_gas_acc : blob_gas_used = 0;
var tx0_to : address = ZERO_ADDRESS;
let records_start = receipt_store_begin();
let transaction_logs_start = logs_tx_start();
let transaction_logs_count = logs_tx_count();
let logs_start = log_store_index_add(transaction_logs_start, transaction_logs_count);
var remaining_deposits : StatelessInputSlice = expected_deposits;
var cursor = ssz_list_cursor(transactions);
var keys : StatelessInputSlice = public_keys;
let initial_cursor_empty = ssz_list_cursor_empty(cursor);
var cursor_has_item : bool = not_bool(initial_cursor_empty);
while cursor_has_item termination_measure(cursor.items.count - cursor.index) do {
let i = cursor.index;
let (transaction, next) = ssz_list_pop(cursor);
cursor = next;
let keys_fields = keys;
let keys_fields : StatelessInputSliceAtLeast(65) =
if public_key_length <= keys_fields.len then keys_fields else fatal_error(WitnessDeficient);
let public_key = sub_slice(keys_fields, 0, PUBLIC_KEY_LENGTH);
keys = slice_suffix(keys_fields, public_key_length);
let tx = decode_transaction(transaction, public_key);
k_current_transaction_epoch = i + 1;
if i == 0 then {
tx0_to = tx.recipient
};
/* A local binding opens the existential accumulator, retaining its
concrete execution/state indices for this admission step. */
let usage = gas_usage;
let available_execution_gas = gas_limit - usage.execution;
let available_state_gas = gas_limit - usage.state;
let allowance = transaction_gas_allowance(
tx.gas_limit,
gas_limits.transaction_total_limit,
gas_limits.transaction_regular_limit,
);
if profile.fork >= Amsterdam then {
if (available_execution_gas < allowance.regular) | (available_state_gas < allowance.total) then {
fatal_error(GasUsedExceedsLimit)
} else {
let tx_blob_gas : transaction_blob_gas = sizeof(gas_per_blob_value) * tx.blob_hashes.count;
let next_blob_gas : blob_gas_used = block_blob_gas_add(
profile.blob_schedule.max,
blob_gas_acc,
tx_blob_gas,
);
let receipt = process_transaction(tx, allowance);
let next_usage = block_gas_usage_add(usage, receipt.execution_gas, receipt.state_gas, receipt.gas_used);
gas_usage = next_usage;
receipt_store_append(receipt, next_usage.receipts, i);
if profile.fork >= Prague then {
remaining_deposits = authenticate_deposit_logs(receipt.logs, remaining_deposits)
};
blob_gas_acc = next_blob_gas
}
} else if available_execution_gas < allowance.total then {
fatal_error(GasUsedExceedsLimit)
} else {
let tx_blob_gas : transaction_blob_gas = sizeof(gas_per_blob_value) * tx.blob_hashes.count;
let next_blob_gas : blob_gas_used =
if profile.fork < Cancun
then blob_gas_acc
else block_blob_gas_add(profile.blob_schedule.max, blob_gas_acc, tx_blob_gas);
let receipt = process_transaction(tx, allowance);
let next_usage = block_gas_usage_add(usage, receipt.gas_used, 0, receipt.gas_used);
gas_usage = next_usage;
receipt_store_append(receipt, next_usage.receipts, i);
if profile.fork >= Prague then {
remaining_deposits = authenticate_deposit_logs(receipt.logs, remaining_deposits)
};
blob_gas_acc = next_blob_gas
};
let cursor_empty = ssz_list_cursor_empty(cursor);
cursor_has_item = not_bool(cursor_empty)
};
let remaining_deposits_length = region_slice_length(remaining_deposits);
if (profile.fork >= Prague) & (remaining_deposits_length != 0) then {
fatal_error(InvalidExecutionRequests)
};
let final_usage = gas_usage;
let header_gas_used =
if (profile.fork >= Amsterdam) & (final_usage.execution < final_usage.state)
then final_usage.state
else final_usage.execution;
let receipts_root = receipt_store_root(records_start, transactions.count);
let retained_logs_start = logs_tx_start();
let retained_logs_count = logs_tx_count();
let retained = log_store_index_add(retained_logs_start, retained_logs_count);
let logs_count : log_store_index =
if logs_start <= retained then retained - logs_start else 0;
struct {
header_gas_used = header_gas_used,
execution_gas_used = final_usage.execution,
state_gas_used = final_usage.state,
blob_gas_used = blob_gas_acc,
first_tx_recipient = tx0_to,
receipts_root = receipts_root,
logs = struct { start = logs_start, count = logs_count },
}
}The block-start writes: beacon root (Cancun+, EIP-4788) and parent hash history (Prague+, EIP-2935).
function run_block_start_system_calls() -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
if profile.fork >= Cancun then {
system_call(BEACON_ROOTS_ADDR, k_header.parent_beacon_block_root)
};
if profile.fork >= Prague then {
system_call(HISTORY_STORAGE_ADDR, k_header.parent_hash)
}
}Validates the EIP-7685 execution requests at block end in request-type order against the input's committed request bytes: withdrawal (EIP-7002), consolidation (EIP-7251), and, from Amsterdam, builder deposit and builder exit (EIP-8282). Before Amsterdam the input must commit to empty builder request streams. Deposits (EIP-6110) are authenticated inline against the transaction receipt logs.
function validate_execution_requests(input_ref : StatelessInputRef) -> unit = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
validate_request_stream(WITHDRAWAL_REQUEST_ADDR, input_ref.withdrawal_requests);
validate_request_stream(CONSOLIDATION_REQUEST_ADDR, input_ref.consolidation_requests);
if profile.fork >= Amsterdam then {
validate_request_stream(BUILDER_DEPOSIT_REQUEST_ADDR, input_ref.builder_deposit_requests);
validate_request_stream(BUILDER_EXIT_REQUEST_ADDR, input_ref.builder_exit_requests)
} else {
let builder_deposit_length = region_slice_length(input_ref.builder_deposit_requests);
let builder_exit_length = region_slice_length(input_ref.builder_exit_requests);
if (builder_deposit_length != 0) | (builder_exit_length != 0) then {
fatal_error(InvalidExecutionRequests)
}
}
}Clears both warm sets (per-transaction reset; EIP-2929 warmth is transaction-scoped).
val warm_reset = impure { c: "warm_reset" } : block_access_index -> unitEIP-7623 calldata floor; BLS precompiles 0x0b-0x11.
let Prague : int(prague_fork_value) = sizeof(prague_fork_value)The current execution epoch: zero for pre-execution effects, transaction index plus one during transaction execution, and transaction count plus one for post-execution effects. EIP-2929 warmth and EIP-7928 BAL changes share this transaction-scoped identity.
register k_current_transaction_epoch : block_access_index = 0The 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_PROFILEThe block body. The semantic structure is explicit while its potentially large fields stay source-backed until individual elements are needed.
struct BlockBody = {
transactions : TransactionListRef,
withdrawals : WithdrawalListRef,
block_access_list : StatelessInputSliceAtMost(block_access_list_length_bound),
}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,
}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,
}