Machine state¶
The user-space half of the machine: per-frame registers, operand stack,
gas counter, and the saved-frame stack for nested calls. World state lives
in the host kernel and is reached only via k_* calls; world rollback on
revert is a kernel snapshot, not saved here.
The state-passing convention¶
Hot machine state flows through handler signatures instead of registers, mirroring the Yellow Paper's state-passing transition μ′ = Ξ(μ): the remaining gas (μ_g), the program counter (μ_pc), the operand-stack cursor (μ_s), and the frame-memory height (μ_i) are carried by value from the interpreter loop into each opcode handler and returned updated. No handler reads or writes these registers: the canonical loop supplies each step's arguments from the registers and assigns the returned state back, while frame-boundary code — frame save/suspend (suspend_frame), resume (resume_frame), and the transaction wrapper — synchronizes them explicitly. The optimized interpreter carries the same values in locals and touches the registers only at those frame boundaries.
The frame registers¶
One Message per active frame (YP "message call"
I = (I_a, I_o, I_s, …)); sub-calls save and restore these registers in
the interpreter.
function validated_refund_add¶
function validated_refund_add(left, right) = {
let total = left + right;
if (-sizeof(gas_refund_bound) <= total) & (total <= sizeof(gas_refund_bound)) then {
total
} else {
fatal_error(ExecutionInvalid)
}
}function fatal_error(_reason) = exit(())function validated_refund_add(left, right) = {
let total = left + right;
if (-sizeof(gas_refund_bound) <= total) & (total <= sizeof(gas_refund_bound)) then {
total
} else {
fatal_error(ExecutionInvalid)
}
}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,
}Absolute lifecycle bound for the signed refund accumulator.
type gas_refund_bound : Int = 199 * (2 ^ 64 - 1)function record_refund¶
function record_refund(refund, delta) =
validated_refund_add(refund, delta)function record_refund(refund, delta) =
validated_refund_add(refund, delta)function validated_refund_add(left, right) = {
let total = left + right;
if (-sizeof(gas_refund_bound) <= total) & (total <= sizeof(gas_refund_bound)) then {
total
} else {
fatal_error(ExecutionInvalid)
}
}function frame_code_len¶
The frame code length in bytes (CODESIZE).
function frame_code_len(frame_code : Code) -> code_length = {
let code = frame_code;
let length = code.len;
length
}Existential executable-code value whose concrete byte address and length
remain correlated inside CodeFields.
type Code = {
'off 'len,
code_region_valid_range('off, 'len) & code_valid_length('len).
CodeFields('off, 'len)
}A contract-code length.
type code_length = range(0, code_region_bound)function frame_jumpdest_valid¶
function frame_jumpdest_valid(frame_code, dest) = {
let code = frame_code;
let length = code.len;
jumpdest_ref_contains(code.jumpdests, length, dest)
}function frame_jumpdest_valid(frame_code, dest) = {
let code = frame_code;
let length = code.len;
jumpdest_ref_contains(code.jumpdests, length, dest)
}Whether the referenced bitmap marks the given program counter as a
valid JUMPDEST.
val jumpdest_ref_contains = impure { c: "jumpdest_ref_contains" } : (jump_table_index, code_length, code_pointer) -> boolThe operand stack¶
let STACK_LIMIT¶
The 1024-element operand-stack limit (YP §9.1).
let STACK_LIMIT : operand_stack_height = 1024The number of words on an operand stack.
type operand_stack_height = range(0, 1024)function conserved_gas_add¶
function conserved_gas_add(available, credit) =
if credit <= (2 ^ 64 - 1) - available then {
available + credit
} else {
fatal_error(ExecutionInvalid)
}function conserved_gas_add(available, credit) =
if credit <= (2 ^ 64 - 1) - available then {
available + credit
} else {
fatal_error(ExecutionInvalid)
}function fatal_error(_reason) = exit(())We have special support for raising values to the power of two. Any Sail expression 2 ^ x will be compiled to this builtin.
val pow2 = pure {lean: "_lean_pow2i", _: "pow2"}: forall ('n : Int). int('n) -> int(2 ^ 'n)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,
}function frame_state_gas_used¶
Computes the signed state gas consumed by the current frame.
function frame_state_gas_used(
state_gas_reservoir : state_gas,
state_gas_remaining : state_gas,
state_gas_spilled : state_gas_spill,
) -> (
frame_state_gas_delta
) = {
let entry = state_gas_reservoir;
let remaining = state_gas_remaining;
let spilled = state_gas_spilled;
entry - remaining + spilled
}Net state gas consumed by one execution frame. A credit can make this
negative until transaction settlement clamps the block-level value at
zero. The bounds follow from subtracting two live uint64 counters and
adding at most one EIP-7825 regular-pool spill.
type frame_state_gas_delta = range(
-(2 ^ 64 - 1),
(2 ^ 64 - 1) + transaction_execution_gas_limit_value,
)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)Execution gas temporarily consumed by Amsterdam state charges. EIP-8037 draws spill only from the regular-gas pool, which is capped by EIP-7825.
type state_gas_spill = range(0, transaction_execution_gas_limit_value)function exceptional_state¶
function exceptional_state(state_gas_remaining, state_gas_spilled, state_gas_reservoir, k) = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
if profile.fork >= Amsterdam then {
struct {
state_gas_remaining = state_gas_reservoir,
state_gas_spilled = STATE_GAS_SPILL_ZERO,
status = Exceptional(k),
}
} else {
struct {
state_gas_remaining = state_gas_remaining,
state_gas_spilled = state_gas_spilled,
status = Exceptional(k),
}
}
}function exceptional_state(state_gas_remaining, state_gas_spilled, state_gas_reservoir, k) = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
if profile.fork >= Amsterdam then {
struct {
state_gas_remaining = state_gas_reservoir,
state_gas_spilled = STATE_GAS_SPILL_ZERO,
status = Exceptional(k),
}
} else {
struct {
state_gas_remaining = state_gas_remaining,
state_gas_spilled = state_gas_spilled,
status = Exceptional(k),
}
}
}EIP-7954 code/initcode size bump (65536/131072).
let Amsterdam : int(amsterdam_fork_value) = sizeof(amsterdam_fork_value)let STATE_GAS_SPILL_ZERO : int(0) = 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_PROFILEfunction stack_height¶
The stack height below a carried cursor.
function stack_height(top : StackPointer) -> operand_stack_height = stack_top_height(top)function stack_top_height(top : StackPointer) -> operand_stack_height =
top.heightThe operand-stack cursor for the active frame, threaded by value through
the interpreter in the state-passing convention and held in the
stack_top frame register at frame boundaries. storage is an opaque
host coordinate while height is the semantic stack height. Keeping the
height in the cursor makes stack validation independent of the host stack
representation. Optimized C refines storage to a native u256 *.
struct StackPointer = {
storage : bits(64),
height : operand_stack_height,
}The number of words on an operand stack.
type operand_stack_height = range(0, 1024)type StackValidation¶
Checks the Yellow Paper stack precondition for one instruction before it
charges gas or performs side effects. inputs is the instruction's
required stack height (delta) and outputs is the height it contributes
after consuming those inputs (alpha). This is the single stack-bounds
guard: handler bodies consume and produce operands unchecked behind it.
enum StackValidation = { StackValid, StackUnderflowFailure, StackOverflowFailure }function validate_stack¶
Classifies the carried cursor against one instruction's input and output stack requirements.
function validate_stack(top, inputs, outputs) = {
let height = stack_height(top);
if height < inputs then {
StackUnderflowFailure
} else if STACK_LIMIT < height - inputs + outputs then {
StackOverflowFailure
} else {
StackValid
}
}The stack height below a carried cursor.
function stack_height(top : StackPointer) -> operand_stack_height = stack_top_height(top)Classifies the carried cursor against one instruction's input and output stack requirements.
function validate_stack(top, inputs, outputs) = {
let height = stack_height(top);
if height < inputs then {
StackUnderflowFailure
} else if STACK_LIMIT < height - inputs + outputs then {
StackOverflowFailure
} else {
StackValid
}
}The 1024-element operand-stack limit (YP §9.1).
let STACK_LIMIT : operand_stack_height = 1024Checks the Yellow Paper stack precondition for one instruction before it
charges gas or performs side effects. inputs is the instruction's
required stack height (delta) and outputs is the height it contributes
after consuming those inputs (alpha). This is the single stack-bounds
guard: handler bodies consume and produce operands unchecked behind it.
enum StackValidation = { StackValid, StackUnderflowFailure, StackOverflowFailure }function read_stack_word¶
Reads the n=0 slot under a validated cursor.
function read_stack_word(sp : StackPointer) -> word = stack_slot_read(sp, 0)function stack_slot_read(top : StackPointer, index : stack_index) -> word =
stack_slot_read_host(top.storage, index)The operand-stack cursor for the active frame, threaded by value through
the interpreter in the state-passing convention and held in the
stack_top frame register at frame boundaries. storage is an opaque
host coordinate while height is the semantic stack height. Keeping the
height in the cursor makes stack validation independent of the host stack
representation. Optimized C refines storage to a native u256 *.
struct StackPointer = {
storage : bits(64),
height : operand_stack_height,
}The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function write_stack_word¶
Writes the n=0 slot under a validated cursor.
function write_stack_word(sp : StackPointer, value : word) -> unit =
stack_slot_write(sp, 0, value)function stack_slot_write(top : StackPointer, index : stack_index, value : word) -> unit =
stack_slot_write_host(top.storage, index, value)The operand-stack cursor for the active frame, threaded by value through
the interpreter in the state-passing convention and held in the
stack_top frame register at frame boundaries. storage is an opaque
host coordinate while height is the semantic stack height. Keeping the
height in the cursor makes stack validation independent of the host stack
representation. Optimized C refines storage to a native u256 *.
struct StackPointer = {
storage : bits(64),
height : operand_stack_height,
}The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function stack_set¶
Overwrites the n-th-from-top operand (SWAP); the cursor is
unchanged.
function stack_set(top : StackPointer, n : stack_index, w : word) -> unit =
stack_slot_write(top, n, w)function stack_slot_write(top : StackPointer, index : stack_index, value : word) -> unit =
stack_slot_write_host(top.storage, index, value)The operand-stack cursor for the active frame, threaded by value through
the interpreter in the state-passing convention and held in the
stack_top frame register at frame boundaries. storage is an opaque
host coordinate while height is the semantic stack height. Keeping the
height in the cursor makes stack validation independent of the host stack
representation. Optimized C refines storage to a native u256 *.
struct StackPointer = {
storage : bits(64),
height : operand_stack_height,
}A zero-based index from the top of the operand stack.
type stack_index = range(0, 1023)The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function is_running¶
Whether the frame is still running.
function is_running(frame_status : FrameStatus) -> bool = match frame_status {
Running() => true,
_ => false,
}Per-frame execution status: running, halted normally, or exceptionally halted.
union FrameStatus = {
/* mid-execution */
Running : unit,
/* halted normally (YP §9.4.4) */
Halted : HaltKind,
/* halted exceptionally: all frame gas consumed, effects void */
Exceptional : ExceptionKind
}function calldata_install¶
Installs the frame's calldata reference.
function calldata_install(data : CalldataSlice) -> CalldataSlice = dataCalldata is either the immutable top-level transaction input or a frozen range of the suspended caller's memory. The variants state the only two protocol-valid provenances instead of exposing the host's region enum.
union CalldataSlice = {
/* the immutable top-level transaction input */
InputCalldata : StatelessInputSlice,
/* a frozen range of the suspended caller's memory */
MemoryCalldata : EvmMemorySlice,
}function returndata_clear¶
Clears the returndata buffer (a new sub-call begins).
function returndata_clear() -> OutputSlice = EMPTY_OUTPUT_SLICElet EMPTY_OUTPUT_SLICE : OutputSliceFields(0, 0) = output_slice(0, 0)A frame-output range with its coordinate and length packed existentially.
type OutputSlice = {
'off 'len,
output_region_valid_range('off, 'len).
OutputSliceFields('off, 'len)
}function returndata_size¶
RETURNDATASIZE.
function returndata_size(returndata : OutputSlice) -> source_pointer = {
let data = returndata;
data.len
}A frame-output range with its coordinate and length packed existentially.
type OutputSlice = {
'off 'len,
output_region_valid_range('off, 'len).
OutputSliceFields('off, 'len)
}An absolute byte position in a named source region.
type source_pointer = range(0, default_host_region_bound)function returndata_copy¶
function returndata_copy(returndata, dst, off, len) =
slice_copy(returndata, dst, off, len)function returndata_copy(returndata, dst, off, len) =
slice_copy(returndata, dst, off, len)function returndata_copy_prefix¶
Copies min(want, size) returndata bytes — the CALL-family output
write-back.
function returndata_copy_prefix(returndata : OutputSlice, dst : memory_base, want : memory_length) -> unit = {
let wanted = want;
let available = returndata_size(returndata);
let copy_length : memory_length =
if wanted < available then wanted else available;
slice_copy(returndata, dst, 0, copy_length)
}RETURNDATASIZE.
function returndata_size(returndata : OutputSlice) -> source_pointer = {
let data = returndata;
data.len
}A frame-output range with its coordinate and length packed existentially.
type OutputSlice = {
'off 'len,
output_region_valid_range('off, 'len).
OutputSliceFields('off, 'len)
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)A materialized length or allocation size in the EVM-memory arena.
type memory_length = range(0, memory_region_bound)The frame memory¶
The EVM's view over frame memory, a region of the host interface. The active
frame's exact byte high-water mark flows through memory-family handlers as the
scalar memory_height, beside Sail's absolute arena
memory_base. The host owns the arena storage and
returns pointer-bearing EvmMemorySlice values only when
Sail requests a derived view. Every access is guarded
by gas charging against the carried cursor: an unaffordable expansion sets
the halt status before expand_memory is reached, so a
post-out-of-gas write to a huge offset (for example, MSTORE at 2^32)
cannot grow the backing buffer. Successful expansion raises the scalar
high-water mark that MSIZE and memory-expansion gas read.
function returndata_remaining¶
function returndata_remaining(available, offset) = available - offsetfunction returndata_remaining(available, offset) = available - offsetfunction memory_high_water¶
Returns the carried frame's exact byte high-water mark.
function memory_high_water(height : memory_height) -> memory_length = heightThe active EVM frame's exact relative byte high-water mark. It is carried
beside the frame's absolute memory_base; the host retains no hidden
frame coordinate or lifecycle state.
type memory_height = memory_lengthA materialized length or allocation size in the EVM-memory arena.
type memory_length = range(0, memory_region_bound)let MEMORY_HEIGHT_ZERO¶
The empty EVM-memory high-water mark.
let MEMORY_HEIGHT_ZERO : memory_height = 0The active EVM frame's exact relative byte high-water mark. It is carried
beside the frame's absolute memory_base; the host retains no hidden
frame coordinate or lifecycle state.
type memory_height = memory_lengthlet MEMORY_BASE_ZERO¶
The top-level frame begins at the shared arena's semantic offset zero.
let MEMORY_BASE_ZERO : memory_base = 0An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)function memory_absolute¶
Converts a frame-relative coordinate to an absolute arena coordinate.
function memory_absolute(base : memory_base, relative : memory_length) -> memory_base =
if relative <= sizeof(memory_region_bound) - base then {
base + relative
} else {
fatal_error(ExecutionInvalid)
}function fatal_error(_reason) = exit(())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,
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)A materialized length or allocation size in the EVM-memory arena.
type memory_length = range(0, memory_region_bound)Shared per-frame EVM-memory arena capacity.
type memory_region_bound : Int = default_host_region_boundfunction memory_parent_base¶
Restores the parent arena cursor from the current child cursor and the parent's frame-scoped memory height.
function memory_parent_base(child_base : memory_base, parent_height : memory_height) -> memory_base =
if parent_height <= child_base then {
child_base - parent_height
} else {
fatal_error(ExecutionInvalid)
}function fatal_error(_reason) = exit(())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,
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)The active EVM frame's exact relative byte high-water mark. It is carried
beside the frame's absolute memory_base; the host retains no hidden
frame coordinate or lifecycle state.
type memory_height = memory_lengthfunction expand_memory¶
Materializes an already-charged memory high-water mark and returns its updated scalar height.
function expand_memory(base : memory_base, height : memory_height, requested_height : memory_length) -> memory_height = {
if requested_height <= sizeof(memory_region_bound) - base then {
if height < requested_height then {
mem_expand(base, height, requested_height);
requested_height
} else {
height
}
} else {
fatal_error(ExecutionInvalid)
}
}function fatal_error(_reason) = exit(())Materializes a strictly larger EVM-memory extent whose protocol gas has
already been charged. The host zeroes exactly
[base + established, base + required). Sail checks the real aggregate
arena bound before crossing this interface.
val mem_expand = impure { c: "mem_expand" } : forall 'base 'established 'required,
memory_region_valid_range('base, 'required)
& 0
<= 'established
& 'established
< 'required. (int('base), int('established), int('required)) -> unitThe 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,
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)The active EVM frame's exact relative byte high-water mark. It is carried
beside the frame's absolute memory_base; the host retains no hidden
frame coordinate or lifecycle state.
type memory_height = memory_lengthA materialized length or allocation size in the EVM-memory arena.
type memory_length = range(0, memory_region_bound)Shared per-frame EVM-memory arena capacity.
type memory_region_bound : Int = default_host_region_boundfunction active_memory_slice¶
function active_memory_slice(base, mem, off, len) =
if len == 0 then {
EMPTY_EVM_MEMORY_SLICE
} else if mem <= sizeof(memory_region_bound) - base & off + len <= mem then {
let window = mem_view(base, mem, off + len);
sub_slice(window, off, len)
} else {
fatal_error(ExecutionInvalid)
}function active_memory_slice(base, mem, off, len) =
if len == 0 then {
EMPTY_EVM_MEMORY_SLICE
} else if mem <= sizeof(memory_region_bound) - base & off + len <= mem then {
let window = mem_view(base, mem, off + len);
sub_slice(window, off, len)
} else {
fatal_error(ExecutionInvalid)
}function fatal_error(_reason) = exit(())Borrows an already-materialized prefix of one frame. The established extent is carried explicitly so both backends dynamically reject a view outside the frame even if a malformed caller reaches the FFI boundary.
val mem_view = impure { c: "mem_view" } : forall 'base 'established 'required,
memory_region_valid_range('base, 'established)
& memory_region_valid_range('base, 'required)
& 0
<= 'required
& 'required
<= 'established. (int('base), int('established), int('required)) -> EvmMemorySliceLength(
'required,
)let EMPTY_EVM_MEMORY_SLICE : EvmMemorySliceFields(0, 0) = evm_memory_slice(0, 0)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,
}Shared per-frame EVM-memory arena capacity.
type memory_region_bound : Int = default_host_region_boundfunction memory_code_slice¶
function memory_code_slice(base, mem, off, len) =
if len == 0 then {
EMPTY_CODE_SLICE
} else if mem <= sizeof(memory_region_bound) - base & off + len <= mem then {
let window = mem_view(base, mem, off + len);
let initcode = sub_slice(window, off, len);
code_db_intern_memory(initcode)
} else {
fatal_error(ExecutionInvalid)
}Normalizes memory-backed initcode into the code arena before analysis.
function code_db_intern_memory(bytes : EvmMemorySlice) -> CodeSlice = {
let region = code_region_from_memory(bytes);
validated_code_slice(region)
}function fatal_error(_reason) = exit(())Borrows an already-materialized prefix of one frame. The established extent is carried explicitly so both backends dynamically reject a view outside the frame even if a malformed caller reaches the FFI boundary.
val mem_view = impure { c: "mem_view" } : forall 'base 'established 'required,
memory_region_valid_range('base, 'established)
& memory_region_valid_range('base, 'required)
& 0
<= 'required
& 'required
<= 'established. (int('base), int('established), int('required)) -> EvmMemorySliceLength(
'required,
)function memory_code_slice(base, mem, off, len) =
if len == 0 then {
EMPTY_CODE_SLICE
} else if mem <= sizeof(memory_region_bound) - base & off + len <= mem then {
let window = mem_view(base, mem, off + len);
let initcode = sub_slice(window, off, len);
code_db_intern_memory(initcode)
} else {
fatal_error(ExecutionInvalid)
}Canonical empty executable code.
let EMPTY_CODE_SLICE : CodeSlice = code_slice(EMPTY_CODE_REGION_SLICE)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,
}Shared per-frame EVM-memory arena capacity.
type memory_region_bound : Int = default_host_region_boundfunction suspend_frame¶
Captures parent execution state and enters child stack and memory frames.
The caller has published its carried machine state to the frame
registers: this checkpoint reads them at the one authoritative
boundary. The host operand-frame cursor mirrors call_depth: push the
empty child operand stack before installing the child's semantic depth,
and pop it before restoring the parent's semantic depth.
function suspend_frame(
pc : code_pointer,
gas_remaining : gas,
stack_top : StackPointer,
memory_base : memory_base,
memory_height : memory_height,
state_gas_remaining : state_gas,
state_gas_spilled : state_gas_spill,
frame_refund : gas_refund,
frame_status : FrameStatus,
message : Message,
frame_code : Code,
calldata : CalldataSlice,
) -> (
(FrameCheckpoint, StackPointer, memory_base, memory_height)
) = {
k_journal_checkpoint();
let child_stack = operand_stack_push_empty_frame();
let child_memory_base = memory_absolute(memory_base, memory_height);
let child_memory_height = MEMORY_HEIGHT_ZERO;
let checkpoint : FrameCheckpoint = struct {
pc = pc,
gas_remaining = gas_remaining,
stack_top = stack_top,
state_gas_remaining = state_gas_remaining,
state_gas_spilled = state_gas_spilled,
refund = frame_refund,
status = frame_status,
message = message,
code = frame_code,
calldata = calldata,
memory_height = memory_height,
};
(checkpoint, child_stack, child_memory_base, child_memory_height)
}Appends a frame marker to the state journal. The suspended frame stores its refund counter separately.
function k_journal_checkpoint() -> unit = state_journal_checkpoint()Converts a frame-relative coordinate to an absolute arena coordinate.
function memory_absolute(base : memory_base, relative : memory_length) -> memory_base =
if relative <= sizeof(memory_region_bound) - base then {
base + relative
} else {
fatal_error(ExecutionInvalid)
}function operand_stack_push_empty_frame() -> StackPointer =
struct { storage = operand_stack_push_empty_frame_host(), height = 0 }The empty EVM-memory high-water mark.
let MEMORY_HEIGHT_ZERO : memory_height = 0Calldata is either the immutable top-level transaction input or a frozen range of the suspended caller's memory. The variants state the only two protocol-valid provenances instead of exposing the host's region enum.
union CalldataSlice = {
/* the immutable top-level transaction input */
InputCalldata : StatelessInputSlice,
/* a frozen range of the suspended caller's memory */
MemoryCalldata : EvmMemorySlice,
}Existential executable-code value whose concrete byte address and length
remain correlated inside CodeFields.
type Code = {
'off 'len,
code_region_valid_range('off, 'len) & code_valid_length('len).
CodeFields('off, 'len)
}The suspended parent-frame state restored after nested execution.
struct FrameCheckpoint = {
pc : code_pointer,
gas_remaining : gas,
stack_top : StackPointer,
state_gas_remaining : state_gas,
state_gas_spilled : state_gas_spill,
refund : gas_refund,
status : FrameStatus,
message : Message,
code : Code,
calldata : CalldataSlice,
memory_height : memory_height,
}Per-frame execution status: running, halted normally, or exceptionally halted.
union FrameStatus = {
/* mid-execution */
Running : unit,
/* halted normally (YP §9.4.4) */
Halted : HaltKind,
/* halted exceptionally: all frame gas consumed, effects void */
Exceptional : ExceptionKind
}The per-frame call message (YP §8, the I tuple): caller, executing address, code owner, value, calldata length, static flag, and call depth.
struct Message = {
/* CALLER (I_s) */
caller : address,
/* account whose code runs (differs under DELEGATECALL/CALLCODE and
EIP-7702 delegation) */
code_address : address,
/* ADDRESS / storage owner = self (I_a) */
address : address,
/* CALLVALUE (I_v) */
value : word,
/* State-gas reservoir available when the frame was entered. */
state_gas_reservoir : state_gas,
/* inside a STATICCALL frame (EIP-214) */
is_static : bool,
/* call depth (YP I_e), bounded by call_depth_limit */
depth : frame_depth,
}The operand-stack cursor for the active frame, threaded by value through
the interpreter in the state-passing convention and held in the
stack_top frame register at frame boundaries. storage is an opaque
host coordinate while height is the semantic stack height. Keeping the
height in the cursor makes stack validation independent of the host stack
representation. Optimized C refines storage to a native u256 *.
struct StackPointer = {
storage : bits(64),
height : operand_stack_height,
}An absolute byte position in the code arena.
type code_pointer = range(0, code_region_bound)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)The signed transaction refund accumulator before capping.
type gas_refund = range(
-gas_refund_bound,
gas_refund_bound,
)An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)The active EVM frame's exact relative byte high-water mark. It is carried
beside the frame's absolute memory_base; the host retains no hidden
frame coordinate or lifecycle state.
type memory_height = memory_lengthAmsterdam'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)Execution gas temporarily consumed by Amsterdam state charges. EIP-8037 draws spill only from the regular-gas pool, which is capped by EIP-7825.
type state_gas_spill = range(0, transaction_execution_gas_limit_value)function mem_set_byte¶
Writes one memory byte and raises the high-water mark.
function mem_set_byte(base : memory_base, off : memory_base, v : byte) -> unit = {
let absolute_offset = memory_absolute(base, off);
mem_write_byte(absolute_offset, v)
}Writes one byte at absolute arena position off (MSTORE8).
val mem_write_byte = impure { c: "mem_write_byte" } : (memory_base, byte) -> unitConverts a frame-relative coordinate to an absolute arena coordinate.
function memory_absolute(base : memory_base, relative : memory_length) -> memory_base =
if relative <= sizeof(memory_region_bound) - base then {
base + relative
} else {
fatal_error(ExecutionInvalid)
}An 8-bit byte.
type byte = bits(8)An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)function mem_load¶
MLOAD: the big-endian word at off.
function mem_load(base : memory_base, off : memory_base) -> word = {
let absolute_offset = memory_absolute(base, off);
mem_load_word(absolute_offset)
}Reads the 32-byte big-endian word at absolute off in one interface crossing
(MLOAD).
val mem_load_word = impure { c: "mem_load_word" } : memory_base -> wordConverts a frame-relative coordinate to an absolute arena coordinate.
function memory_absolute(base : memory_base, relative : memory_length) -> memory_base =
if relative <= sizeof(memory_region_bound) - base then {
base + relative
} else {
fatal_error(ExecutionInvalid)
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function mem_store¶
MSTORE: writes the big-endian word at off and raises the
high-water mark.
function mem_store(base : memory_base, off : memory_base, w : word) -> unit = {
let absolute_offset = memory_absolute(base, off);
mem_store_word(absolute_offset, w)
}Writes the 32-byte big-endian word at absolute off in one interface
crossing (MSTORE).
val mem_store_word = impure { c: "mem_store_word" } : (memory_base, word) -> unitConverts a frame-relative coordinate to an absolute arena coordinate.
function memory_absolute(base : memory_base, relative : memory_length) -> memory_base =
if relative <= sizeof(memory_region_bound) - base then {
base + relative
} else {
fatal_error(ExecutionInvalid)
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function mem_store_byte¶
MSTORE8: writes the low byte of w.
function mem_store_byte(base : memory_base, off : memory_base, w : word) -> unit = {
let value = word_low_byte(w);
mem_set_byte(base, off, value)
}Writes one memory byte and raises the high-water mark.
function mem_set_byte(base : memory_base, off : memory_base, v : byte) -> unit = {
let absolute_offset = memory_absolute(base, off);
mem_write_byte(absolute_offset, v)
}function word_low_byte(value) = get_slice_int(8, value, 0)An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function mem_mcopy¶
MCOPY (EIP-5656): overlapping-safe memory-to-memory copy.
function mem_mcopy(base : memory_base, dst : memory_base, src : memory_base, len : memory_length) -> unit =
if len != 0 then {
let absolute_dst = memory_absolute(base, dst);
let absolute_src = memory_absolute(base, src);
mem_move(absolute_dst, absolute_src, len)
}Copies an absolute byte range within the shared arena; overlapping
ranges behave as one atomic move (MCOPY and the destination side of
the *COPY opcodes).
val mem_move = impure { c: "mem_move" } : (memory_base, memory_base, memory_length) -> unitConverts a frame-relative coordinate to an absolute arena coordinate.
function memory_absolute(base : memory_base, relative : memory_length) -> memory_base =
if relative <= sizeof(memory_region_bound) - base then {
base + relative
} else {
fatal_error(ExecutionInvalid)
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)A materialized length or allocation size in the EVM-memory arena.
type memory_length = range(0, memory_region_bound)function mem_keccak¶
KECCAK256 over the already-expanded memory range [off, off+len).
function mem_keccak(base : memory_base, mem : memory_height, range : MemoryRange) -> word = {
let bytes = active_memory_slice(base, mem, range.off, range.len);
let digest = keccak256(bytes);
hash_to_word(digest)
}function active_memory_slice(base, mem, off, len) =
if len == 0 then {
EMPTY_EVM_MEMORY_SLICE
} else if mem <= sizeof(memory_region_bound) - base & off + len <= mem then {
let window = mem_view(base, mem, off + len);
sub_slice(window, off, len)
} else {
fatal_error(ExecutionInvalid)
}Interprets a digest as the corresponding big-endian EVM word.
function hash_to_word(bytes : hash) -> word =
unsigned(
bytes[0]
@ bytes[1]
@ bytes[2]
@ bytes[3]
@ bytes[4]
@ bytes[5]
@ bytes[6]
@ bytes[7]
@ bytes[8]
@ bytes[9]
@ bytes[10]
@ bytes[11]
@ bytes[12]
@ bytes[13]
@ bytes[14]
@ bytes[15]
@ bytes[16]
@ bytes[17]
@ bytes[18]
@ bytes[19]
@ bytes[20]
@ bytes[21]
@ bytes[22]
@ bytes[23]
@ bytes[24]
@ bytes[25]
@ bytes[26]
@ bytes[27]
@ bytes[28]
@ bytes[29]
@ bytes[30]
@ bytes[31],
)A memory range retaining its offset, length, and containment proof.
type MemoryRange = {
'off 'len,
memory_valid_range('off, 'len). MemoryRangeFields('off, 'len)
}An absolute byte position in the shared EVM-memory arena.
type memory_base = range(0, memory_region_bound)The active EVM frame's exact relative byte high-water mark. It is carried
beside the frame's absolute memory_base; the host retains no hidden
frame coordinate or lifecycle state.
type memory_height = memory_lengthThe EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)