Payload commitments¶
Recursive transaction, withdrawal, and receipt tries plus the header-level commitment checks over their recomputed roots.
The indexed tries¶
Transactions, withdrawals, and receipts all use rlp(index) keys. Their
values come from different source-backed regions, but their trie shape is the
same: recursively partition the canonical key-order cursor by nibble and use
the shared MPT child combiner. No Sail list, explicit frame stack, or
collection-sized vector is involved.
type IndexedTrieSource¶
The closed source algebra for Ethereum's three index-keyed payload tries. indexed_trie_begin and indexed_trie_pop interpret it as count and value operations, allowing one recursive trie builder without storing function values or duplicating the traversal.
union IndexedTrieSource = {
/* the block body's transaction list */
IndexedTransactions : TransactionListRef,
/* the block body's withdrawal list */
IndexedWithdrawals : WithdrawalListRef,
/* the executed block's encoded receipt records */
IndexedReceipts : ReceiptRecordsRef,
}One execution-ordered sequence of length-prefixed encoded receipts in the scratch arena. The records contain no Sail list or aggregate receipt values: each push writes an eight-byte little-endian length followed by the canonical trie value.
struct ReceiptRecordsRef = {
bytes : ScratchSlice,
count : transaction_count,
}A schema-bounded source reference to encoded transaction envelopes.
type TransactionListRef = BoundedSszListRef(transaction_count_bound)A schema-bounded source reference to withdrawals.
type WithdrawalListRef = BoundedSszListRef(withdrawal_count_bound)type IndexedTrieCursor¶
Canonical key traversal plus the receipt-only record state. receipt_zero
bridges the sole difference between numeric receipt storage order and
lexical rlp(index) order; receipt_remaining otherwise advances once.
struct IndexedTrieCursor = {
keys : RlpIndexCursor(transaction_count_bound),
receipt_zero : ScratchSlice,
receipt_remaining : ScratchSlice,
}The item count, next canonical-key position, and its cached lookup
descriptor. Each rlp(index) key is therefore constructed exactly once.
current is meaningful iff position < count; the pair already carries
the exhaustion state, so no separate presence wrapper exists.
struct RlpIndexCursor('maximum : Int), rlp_index_valid_maximum('maximum) = {
count : range(0, 'maximum),
position : range(0, 'maximum),
current : RlpIndexItem('maximum),
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}Maximum transactions in the execution-payload SSZ list. Provenance:
Bellatrix MAX_TRANSACTIONS_PER_PAYLOAD and Amsterdam
SszExecutionPayload.transactions.
type transaction_count_bound : Int = 2 ^ 20function indexed_receipt_parts¶
Opens the receipt record span while giving non-receipt sources empty placeholders. The explicit result type keeps both existential slices independent.
function indexed_receipt_parts(source : IndexedTrieSource) -> (ScratchSlice, ScratchSlice) =
match source {
IndexedReceipts(receipts) => if receipts.count == 0 then {
(EMPTY_SCRATCH_SLICE, receipts.bytes)
} else {
receipt_record_pop(receipts.bytes)
},
_ => (EMPTY_SCRATCH_SLICE, EMPTY_SCRATCH_SLICE),
}Splits the first retained record from an execution-ordered record span.
function receipt_record_pop(records : ScratchSlice) -> (ScratchSlice, ScratchSlice) = {
let records : ScratchSliceAtLeast(8) =
if EIGHT_BYTE_LENGTH <= records.len then records else fatal_error(WitnessDeficient);
let value_length = decode_scratch_uint(records, 0);
let payload = slice_suffix(records, EIGHT_BYTE_LENGTH);
if value_length <= payload.len then {
(sub_slice(payload, 0, value_length), slice_suffix(payload, value_length))
} else {
fatal_error(WitnessDeficient)
}
}let EMPTY_SCRATCH_SLICE : ScratchSliceFields(0, 0) = scratch_slice(0, 0)The closed source algebra for Ethereum's three index-keyed payload tries. indexed_trie_begin and indexed_trie_pop interpret it as count and value operations, allowing one recursive trie builder without storing function values or duplicating the traversal.
union IndexedTrieSource = {
/* the block body's transaction list */
IndexedTransactions : TransactionListRef,
/* the block body's withdrawal list */
IndexedWithdrawals : WithdrawalListRef,
/* the executed block's encoded receipt records */
IndexedReceipts : ReceiptRecordsRef,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}function indexed_trie_begin¶
Opens one source at its first canonical trie key.
function indexed_trie_begin(source : IndexedTrieSource) -> IndexedTrieCursor = {
let count : transaction_count = match source {
IndexedTransactions(txs) => txs.count,
IndexedWithdrawals(wds) => wds.count,
IndexedReceipts(receipts) => receipts.count,
};
let (zero, remaining) = indexed_receipt_parts(source);
struct { keys = rlp_index_cursor(count), receipt_zero = zero, receipt_remaining = remaining }
}Opens the receipt record span while giving non-receipt sources empty placeholders. The explicit result type keeps both existential slices independent.
function indexed_receipt_parts(source : IndexedTrieSource) -> (ScratchSlice, ScratchSlice) =
match source {
IndexedReceipts(receipts) => if receipts.count == 0 then {
(EMPTY_SCRATCH_SLICE, receipts.bytes)
} else {
receipt_record_pop(receipts.bytes)
},
_ => (EMPTY_SCRATCH_SLICE, EMPTY_SCRATCH_SLICE),
}Starts canonical RLP-index traversal for a bounded collection.
function rlp_index_cursor forall ('maximum : Int), rlp_index_valid_maximum('maximum). (count : range(0, 'maximum)) -> (
RlpIndexCursor('maximum)
) = {
var cursor : RlpIndexCursor('maximum) = struct {
count = count,
position = 0,
current = struct { index = 0, key = path_empty() },
};
if count != 0 then {
let index = rlp_index_at_position(cursor);
cursor.current = struct { index = index, key = trie_index_key(index) }
};
cursor
}Canonical key traversal plus the receipt-only record state. receipt_zero
bridges the sole difference between numeric receipt storage order and
lexical rlp(index) order; receipt_remaining otherwise advances once.
struct IndexedTrieCursor = {
keys : RlpIndexCursor(transaction_count_bound),
receipt_zero : ScratchSlice,
receipt_remaining : ScratchSlice,
}The closed source algebra for Ethereum's three index-keyed payload tries. indexed_trie_begin and indexed_trie_pop interpret it as count and value operations, allowing one recursive trie builder without storing function values or duplicating the traversal.
union IndexedTrieSource = {
/* the block body's transaction list */
IndexedTransactions : TransactionListRef,
/* the block body's withdrawal list */
IndexedWithdrawals : WithdrawalListRef,
/* the executed block's encoded receipt records */
IndexedReceipts : ReceiptRecordsRef,
}Number of transactions in a schema-valid execution payload.
type transaction_count = range(0, transaction_count_bound)function indexed_trie_cursor_empty¶
Whether the source has no canonical keys left.
function indexed_trie_cursor_empty(cursor : IndexedTrieCursor) -> bool =
rlp_index_cursor_empty(cursor.keys)Whether canonical RLP-index traversal has consumed every index.
function rlp_index_cursor_empty forall ('maximum : Int), rlp_index_valid_maximum('maximum). (cursor :
RlpIndexCursor('maximum)) -> (
bool
) =
cursor.position == cursor.countCanonical key traversal plus the receipt-only record state. receipt_zero
bridges the sole difference between numeric receipt storage order and
lexical rlp(index) order; receipt_remaining otherwise advances once.
struct IndexedTrieCursor = {
keys : RlpIndexCursor(transaction_count_bound),
receipt_zero : ScratchSlice,
receipt_remaining : ScratchSlice,
}function indexed_trie_cursor_key¶
The next canonical key without consuming its value.
function indexed_trie_cursor_key(cursor : IndexedTrieCursor) -> TriePath =
rlp_index_cursor_peek(cursor.keys).keyReturns the cached numeric index and trie key without advancing.
function rlp_index_cursor_peek forall ('maximum : Int), rlp_index_valid_maximum('maximum). (cursor :
RlpIndexCursor('maximum)) -> (
RlpIndexItem('maximum)
) =
if cursor.position < cursor.count then {
cursor.current
} else {
fatal_error(WitnessDeficient)
}Canonical key traversal plus the receipt-only record state. receipt_zero
bridges the sole difference between numeric receipt storage order and
lexical rlp(index) order; receipt_remaining otherwise advances once.
struct IndexedTrieCursor = {
keys : RlpIndexCursor(transaction_count_bound),
receipt_zero : ScratchSlice,
receipt_remaining : ScratchSlice,
}A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function indexed_trie_next_under¶
Whether the next indexed key lies beneath prefix.
function indexed_trie_next_under(cursor : IndexedTrieCursor, prefix : TriePath) -> bool =
let cursor_empty = indexed_trie_cursor_empty(cursor) in
if cursor_empty then {
false
} else {
let key = indexed_trie_cursor_key(cursor);
path_prefix_of(prefix, key)
}Whether the source has no canonical keys left.
function indexed_trie_cursor_empty(cursor : IndexedTrieCursor) -> bool =
rlp_index_cursor_empty(cursor.keys)The next canonical key without consuming its value.
function indexed_trie_cursor_key(cursor : IndexedTrieCursor) -> TriePath =
rlp_index_cursor_peek(cursor.keys).keyWhether prefix is a prefix of path.
function path_prefix_of(prefix : TriePath, path : TriePath) -> bool =
path_matches(path, 0, prefix)Canonical key traversal plus the receipt-only record state. receipt_zero
bridges the sole difference between numeric receipt storage order and
lexical rlp(index) order; receipt_remaining otherwise advances once.
struct IndexedTrieCursor = {
keys : RlpIndexCursor(transaction_count_bound),
receipt_zero : ScratchSlice,
receipt_remaining : ScratchSlice,
}A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function indexed_trie_pop¶
Consumes one canonical indexed key and resolves its source-backed value.
function indexed_trie_pop(source : IndexedTrieSource, cursor : IndexedTrieCursor) -> (TrieItem, IndexedTrieCursor) = {
let (index_item, next_keys) = rlp_index_cursor_pop(cursor.keys);
var remaining = cursor.receipt_remaining;
let value : TrieLeafValue = match source {
IndexedTransactions(txs) => {
let transaction = ssz_list_at(txs, index_item.index);
InputTrieLeaf(transaction)
},
IndexedWithdrawals(wds) => {
let withdrawal = ssz_fixed_list_at(wds, index_item.index, WD_SIZE);
let encoded_withdrawal = withdrawal_rlp(withdrawal);
ScratchTrieLeaf(encoded_withdrawal)
},
IndexedReceipts(_) => if index_item.index == 0 then {
ScratchTrieLeaf(cursor.receipt_zero)
} else {
let (receipt, rest) = receipt_record_pop(remaining);
remaining = rest;
ScratchTrieLeaf(receipt)
},
};
(
trie_leaf(index_item.key, value),
struct { keys = next_keys, receipt_zero = cursor.receipt_zero, receipt_remaining = remaining },
)
}Splits the first retained record from an execution-ordered record span.
function receipt_record_pop(records : ScratchSlice) -> (ScratchSlice, ScratchSlice) = {
let records : ScratchSliceAtLeast(8) =
if EIGHT_BYTE_LENGTH <= records.len then records else fatal_error(WitnessDeficient);
let value_length = decode_scratch_uint(records, 0);
let payload = slice_suffix(records, EIGHT_BYTE_LENGTH);
if value_length <= payload.len then {
(sub_slice(payload, 0, value_length), slice_suffix(payload, value_length))
} else {
fatal_error(WitnessDeficient)
}
}Removes the cached indexed item and advances the cursor.
function rlp_index_cursor_pop forall ('maximum : Int), rlp_index_valid_maximum('maximum). (cursor :
RlpIndexCursor('maximum)) -> (
(RlpIndexItem('maximum), RlpIndexCursor('maximum))
) = {
let item = rlp_index_cursor_peek(cursor);
(item, rlp_index_cursor_advance(cursor))
}function ssz_fixed_list_at(items, index, item_size) = {
let bytes = items.bytes;
let width_value = item_size;
let offset_value = index * width_value;
let items_length = bytes.len;
if (index < items.count) & (offset_value + width_value <= items_length) then {
sub_slice(bytes, offset_value, width_value)
} else {
fatal_error(InvalidConfig)
}
}Returns a variable-width list item by resolving its adjacent offsets.
function ssz_list_at forall ('maximum : Int) ('index : Int),
source_valid_length('maximum) & 'maximum <= 2 ^ 30 - 1 & 0 <= 'index. (
items : BoundedSszListRef('maximum),
index : int('index),
) -> (
StatelessInputSlice
) = {
let bytes = items.bytes;
let count = items.count;
let item_index : range(0, 'maximum - 1) =
if index < count then index else fatal_error(InvalidConfig);
let next_index : range(0, 'maximum) = item_index + 1;
let start_position = ssz_offset_table_position(item_index);
let start_offset = ssz_u32_in_slice(bytes, start_position);
let start = ssz_offset_to_source_pointer(start_offset);
let stop : source_pointer =
if next_index < items.count then {
let stop_position = ssz_offset_table_position(next_index);
let stop_offset = ssz_u32_in_slice(bytes, stop_position);
ssz_offset_to_source_pointer(stop_offset)
} else {
bytes.len
};
let start_value = start;
let stop_value = stop;
let items_length = bytes.len;
if (start_value <= stop_value) & (stop_value <= items_length) then {
let item_length = stop_value - start_value;
if (items.max_item_length != 0) & (items.max_item_length < item_length) then {
fatal_error(InvalidConfig)
};
sub_slice(bytes, start, item_length)
} else {
fatal_error(InvalidConfig)
}
}function trie_leaf(path : TriePath, value : TrieLeafValue) -> TrieItem =
struct { path = path, value = LeafItem(value) }The RLP of one withdrawal (EIP-4895), assembled in the scratch arena.
function withdrawal_rlp(withdrawal : StatelessInputSliceLength(44)) -> ScratchSlice = {
let index = decode_ssz_uint(withdrawal, WD_INDEX);
let validator_index = decode_ssz_uint(withdrawal, WD_VALIDATOR_INDEX);
let address = sub_slice(withdrawal, WD_ADDRESS, ADDRESS_BYTE_LENGTH);
let amount = decode_ssz_uint(withdrawal, WD_AMOUNT);
let index_length = rlp_uint_size(index);
let validator_index_length = rlp_uint_size(validator_index);
let address_length = rlp_slice_size(address);
let amount_length = rlp_uint_size(amount);
let content_length = index_length + validator_index_length + address_length + amount_length;
if 48 < content_length then {
fatal_error(RlpDecode)
};
let bounded_content_length : range(0, 48) = tmod_nat(content_length, 49);
let content_len = bounded_content_length;
let encoded_length = rlp_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_length);
rlp_write_list_prefix(content_len);
rlp_write_uint(index);
rlp_write_uint(validator_index);
rlp_write_slice(address);
rlp_write_uint(amount);
rlp_encoder_finish(encoder)
}let WD_SIZE : int(44) = 44Canonical key traversal plus the receipt-only record state. receipt_zero
bridges the sole difference between numeric receipt storage order and
lexical rlp(index) order; receipt_remaining otherwise advances once.
struct IndexedTrieCursor = {
keys : RlpIndexCursor(transaction_count_bound),
receipt_zero : ScratchSlice,
receipt_remaining : ScratchSlice,
}The closed source algebra for Ethereum's three index-keyed payload tries. indexed_trie_begin and indexed_trie_pop interpret it as count and value operations, allowing one recursive trie builder without storing function values or duplicating the traversal.
union IndexedTrieSource = {
/* the block body's transaction list */
IndexedTransactions : TransactionListRef,
/* the block body's withdrawal list */
IndexedWithdrawals : WithdrawalListRef,
/* the executed block's encoded receipt records */
IndexedReceipts : ReceiptRecordsRef,
}A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A leaf value retained by trie assembly. Authenticated witness and transaction leaves borrow immutable input bytes; newly encoded state, receipt, and withdrawal leaves borrow the scratch arena.
union TrieLeafValue = {
/* a leaf borrowing immutable stateless input bytes */
InputTrieLeaf : StatelessInputSlice,
/* a leaf borrowing freshly encoded scratch bytes */
ScratchTrieLeaf : ScratchSlice,
}function indexed_trie_subtree¶
Recursively assembles the indexed leaves beneath prefix.
function indexed_trie_subtree(
source : IndexedTrieSource,
cursor : IndexedTrieCursor,
prefix : TriePath,
) -> (
(TrieItem, IndexedTrieCursor)
) = {
let cursor_under_prefix = indexed_trie_next_under(cursor, prefix);
let outside_prefix = not_bool(cursor_under_prefix);
if outside_prefix then {
(trie_empty_subtree(), cursor)
} else {
let key = indexed_trie_cursor_key(cursor);
let key_at_prefix = path_eq(key, prefix);
if key_at_prefix then {
indexed_trie_pop(source, cursor)
} else {
let depth = path_len(prefix);
if 64 <= depth then {
fatal_error(WitnessDeficient)
};
var children = trie_children_empty();
var remaining = cursor;
var remaining_under_prefix : bool = indexed_trie_next_under(remaining, prefix);
while remaining_under_prefix termination_measure(remaining.keys.count - remaining.keys.position) do {
let next_key = indexed_trie_cursor_key(remaining);
let nib = path_nibble(next_key, depth);
let nibble_path = path_single(nib);
let child_prefix = path_concat(prefix, nibble_path);
let (child, next) = indexed_trie_subtree(source, remaining, child_prefix);
children = trie_children_add(children, prefix, nib, child);
remaining = next;
remaining_under_prefix = indexed_trie_next_under(remaining, prefix)
};
(trie_children_finish(prefix, children), remaining)
}
}
}function fatal_error(_reason) = exit(())The next canonical key without consuming its value.
function indexed_trie_cursor_key(cursor : IndexedTrieCursor) -> TriePath =
rlp_index_cursor_peek(cursor.keys).keyWhether the next indexed key lies beneath prefix.
function indexed_trie_next_under(cursor : IndexedTrieCursor, prefix : TriePath) -> bool =
let cursor_empty = indexed_trie_cursor_empty(cursor) in
if cursor_empty then {
false
} else {
let key = indexed_trie_cursor_key(cursor);
path_prefix_of(prefix, key)
}Consumes one canonical indexed key and resolves its source-backed value.
function indexed_trie_pop(source : IndexedTrieSource, cursor : IndexedTrieCursor) -> (TrieItem, IndexedTrieCursor) = {
let (index_item, next_keys) = rlp_index_cursor_pop(cursor.keys);
var remaining = cursor.receipt_remaining;
let value : TrieLeafValue = match source {
IndexedTransactions(txs) => {
let transaction = ssz_list_at(txs, index_item.index);
InputTrieLeaf(transaction)
},
IndexedWithdrawals(wds) => {
let withdrawal = ssz_fixed_list_at(wds, index_item.index, WD_SIZE);
let encoded_withdrawal = withdrawal_rlp(withdrawal);
ScratchTrieLeaf(encoded_withdrawal)
},
IndexedReceipts(_) => if index_item.index == 0 then {
ScratchTrieLeaf(cursor.receipt_zero)
} else {
let (receipt, rest) = receipt_record_pop(remaining);
remaining = rest;
ScratchTrieLeaf(receipt)
},
};
(
trie_leaf(index_item.key, value),
struct { keys = next_keys, receipt_zero = cursor.receipt_zero, receipt_remaining = remaining },
)
}Recursively assembles the indexed leaves beneath prefix.
function indexed_trie_subtree(
source : IndexedTrieSource,
cursor : IndexedTrieCursor,
prefix : TriePath,
) -> (
(TrieItem, IndexedTrieCursor)
) = {
let cursor_under_prefix = indexed_trie_next_under(cursor, prefix);
let outside_prefix = not_bool(cursor_under_prefix);
if outside_prefix then {
(trie_empty_subtree(), cursor)
} else {
let key = indexed_trie_cursor_key(cursor);
let key_at_prefix = path_eq(key, prefix);
if key_at_prefix then {
indexed_trie_pop(source, cursor)
} else {
let depth = path_len(prefix);
if 64 <= depth then {
fatal_error(WitnessDeficient)
};
var children = trie_children_empty();
var remaining = cursor;
var remaining_under_prefix : bool = indexed_trie_next_under(remaining, prefix);
while remaining_under_prefix termination_measure(remaining.keys.count - remaining.keys.position) do {
let next_key = indexed_trie_cursor_key(remaining);
let nib = path_nibble(next_key, depth);
let nibble_path = path_single(nib);
let child_prefix = path_concat(prefix, nibble_path);
let (child, next) = indexed_trie_subtree(source, remaining, child_prefix);
children = trie_children_add(children, prefix, nib, child);
remaining = next;
remaining_under_prefix = indexed_trie_next_under(remaining, prefix)
};
(trie_children_finish(prefix, children), remaining)
}
}
}val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))Path concatenation; over 64 nibbles is a witness fault.
function path_concat(a : TriePath, b : TriePath) -> TriePath = {
let alen = path_len(a);
let blen = path_len(b);
let combined = alen + blen;
if combined <= 64 then {
var result = a;
var index : trie_path_len = 0;
while index < blen termination_measure(blen - index) do {
let nibble = path_nibble(b, index);
result = path_append_nibble(result, nibble);
let current_index = index;
index =
if current_index < 64 then {
current_index + 1
} else {
fatal_error(WitnessDeficient)
}
};
result
} else {
fatal_error(WitnessDeficient)
}
}Path equality.
function path_eq(a : TriePath, b : TriePath) -> bool =
(a.len == b.len) & (a.data == b.data)The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenThe i-th nibble, most significant first; out of range yields 0.
function path_nibble(path : TriePath, i : trie_path_cursor) -> nibble =
let length = path_len(path) in
if length <= i then {
0x0
} else {
let bytes = path.data;
let byte_index = path_byte_index(i);
let parity = tmod_int(i, 2);
if parity == 0 then {
bytes[byte_index][7 .. 4]
} else {
bytes[byte_index][3 .. 0]
}
}A one-nibble path.
function path_single(n : nibble) -> TriePath = {
let empty_path = path_empty();
path_append_nibble(empty_path, n)
}Adds one recursively built child at index; an empty subtree
contributes nothing.
function trie_children_add(
children : TrieChildren,
prefix : TriePath,
index : nibble,
child : TrieItem,
) -> (
TrieChildren
) =
match child.value {
EmptySubtree() => children,
_ => {
let depth = path_len(prefix);
let child_count = children.count;
let child_depth : range(1, 64) =
if depth < 64 then depth + 1 else fatal_error(WitnessDeficient);
let next_child_count : range(1, 16) =
if child_count < 16 then child_count + 1 else fatal_error(WitnessDeficient);
let child_segment = path_single(index);
let child_prefix = path_concat(prefix, child_segment);
let path_below_child = path_prefix_of(child_prefix, child.path);
let path_outside_child = not_bool(path_below_child);
if path_outside_child then {
fatal_error(WitnessDeficient)
};
let child_already_present = branch_mask_has(children.mask, index);
if child_already_present then {
fatal_error(WitnessDeficient)
};
var updated = children;
updated.mask = branch_mask_set(updated.mask, index);
updated.children[unsigned(index)] = trie_child_ref(child, child_depth);
updated.only = child;
updated.count = next_child_count;
updated
},
}Constructs an empty child accumulator. only is meaningful iff exactly
one child has been added.
function trie_children_empty() -> TrieChildren = {
let empty_ref = EmptyRef();
let children = vector_init(16, empty_ref);
let empty_subtree = trie_empty_subtree();
struct { mask = 0x0000, children = children, only = empty_subtree, count = 0 }
}Finishes a recursive branch: zero children disappear, one child bubbles upward structurally, and multiple children form a canonical branch item.
function trie_children_finish(prefix : TriePath, children : TrieChildren) -> TrieItem =
if children.count == 0 then {
struct { path = prefix, value = EmptySubtree() }
} else if children.count == 1 then {
children.only
} else {
let branch_ref = branch_child_ref(children.mask, children.children);
trie_branch(prefix, branch_ref)
}The absent subtree (YP n(I,i) = ()). Its path carries no meaning.
function trie_empty_subtree() -> TrieItem =
struct { path = path_empty(), value = EmptySubtree() }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,
}Canonical key traversal plus the receipt-only record state. receipt_zero
bridges the sole difference between numeric receipt storage order and
lexical rlp(index) order; receipt_remaining otherwise advances once.
struct IndexedTrieCursor = {
keys : RlpIndexCursor(transaction_count_bound),
receipt_zero : ScratchSlice,
receipt_remaining : ScratchSlice,
}The closed source algebra for Ethereum's three index-keyed payload tries. indexed_trie_begin and indexed_trie_pop interpret it as count and value operations, allowing one recursive trie builder without storing function values or duplicating the traversal.
union IndexedTrieSource = {
/* the block body's transaction list */
IndexedTransactions : TransactionListRef,
/* the block body's withdrawal list */
IndexedWithdrawals : WithdrawalListRef,
/* the executed block's encoded receipt records */
IndexedReceipts : ReceiptRecordsRef,
}A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function indexed_trie_root¶
Computes one complete index-keyed trie. Temporary withdrawal encodings and node encodings share one scratch suffix, released after the root has absorbed every leaf. Receipt records beneath the mark remain live until their accumulator releases them.
function indexed_trie_root(source : IndexedTrieSource) -> hash = {
let mark = scratch_begin();
let initial = indexed_trie_begin(source);
let root_path = path_empty();
let (subtree, remaining) = indexed_trie_subtree(source, initial, root_path);
let retained = remaining.receipt_remaining;
let cursor_empty = indexed_trie_cursor_empty(remaining);
let cursor_not_empty = not_bool(cursor_empty);
if cursor_not_empty | retained.len != 0 then {
fatal_error(WitnessDeficient)
};
let root = trie_subtree_root(subtree);
scratch_rewind(mark);
root
}function fatal_error(_reason) = exit(())Opens one source at its first canonical trie key.
function indexed_trie_begin(source : IndexedTrieSource) -> IndexedTrieCursor = {
let count : transaction_count = match source {
IndexedTransactions(txs) => txs.count,
IndexedWithdrawals(wds) => wds.count,
IndexedReceipts(receipts) => receipts.count,
};
let (zero, remaining) = indexed_receipt_parts(source);
struct { keys = rlp_index_cursor(count), receipt_zero = zero, receipt_remaining = remaining }
}Whether the source has no canonical keys left.
function indexed_trie_cursor_empty(cursor : IndexedTrieCursor) -> bool =
rlp_index_cursor_empty(cursor.keys)Recursively assembles the indexed leaves beneath prefix.
function indexed_trie_subtree(
source : IndexedTrieSource,
cursor : IndexedTrieCursor,
prefix : TriePath,
) -> (
(TrieItem, IndexedTrieCursor)
) = {
let cursor_under_prefix = indexed_trie_next_under(cursor, prefix);
let outside_prefix = not_bool(cursor_under_prefix);
if outside_prefix then {
(trie_empty_subtree(), cursor)
} else {
let key = indexed_trie_cursor_key(cursor);
let key_at_prefix = path_eq(key, prefix);
if key_at_prefix then {
indexed_trie_pop(source, cursor)
} else {
let depth = path_len(prefix);
if 64 <= depth then {
fatal_error(WitnessDeficient)
};
var children = trie_children_empty();
var remaining = cursor;
var remaining_under_prefix : bool = indexed_trie_next_under(remaining, prefix);
while remaining_under_prefix termination_measure(remaining.keys.count - remaining.keys.position) do {
let next_key = indexed_trie_cursor_key(remaining);
let nib = path_nibble(next_key, depth);
let nibble_path = path_single(nib);
let child_prefix = path_concat(prefix, nibble_path);
let (child, next) = indexed_trie_subtree(source, remaining, child_prefix);
children = trie_children_add(children, prefix, nib, child);
remaining = next;
remaining_under_prefix = indexed_trie_next_under(remaining, prefix)
};
(trie_children_finish(prefix, children), remaining)
}
}
}val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))The empty path.
function path_empty() -> TriePath =
path_new(ZERO_HASH, 0)Marks the start of a scratch construction.
function scratch_begin() -> source_pointer = {
let arena = scratch_arena;
arena.len
}Discards everything pushed since mark.
function scratch_rewind(mark : source_pointer) -> unit =
let mark_offset = mark in
let arena = scratch_arena in
let cursor_offset = arena.len in
if mark_offset <= cursor_offset then {
scratch_arena = sub_slice(arena, 0, mark);
host_scratch_truncate(mark)
} else {
assert(false, "scratch rewind mark")
}Converts a recursively assembled top-level item to the committed root.
function trie_subtree_root(subtree : TrieItem) -> hash =
match subtree.value {
EmptySubtree() => EMPTY_TRIE_ROOT,
_ => {
let root_ref = trie_child_ref(subtree, 0);
trie_ref_to_root(root_ref)
},
}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,
}The closed source algebra for Ethereum's three index-keyed payload tries. indexed_trie_begin and indexed_trie_pop interpret it as count and value operations, allowing one recursive trie builder without storing function values or duplicating the traversal.
union IndexedTrieSource = {
/* the block body's transaction list */
IndexedTransactions : TransactionListRef,
/* the block body's withdrawal list */
IndexedWithdrawals : WithdrawalListRef,
/* the executed block's encoded receipt records */
IndexedReceipts : ReceiptRecordsRef,
}The common digest type used by trie, code, and block hashes.
type hash = b256function transaction_trie_root¶
The transactions-trie root (YP §4.4.2): leaf i holds the raw
EIP-2718 envelope of transaction i, keyed by rlp(i).
function transaction_trie_root(txs : TransactionListRef) -> hash = {
let source = IndexedTransactions(txs);
indexed_trie_root(source)
}Computes one complete index-keyed trie. Temporary withdrawal encodings and node encodings share one scratch suffix, released after the root has absorbed every leaf. Receipt records beneath the mark remain live until their accumulator releases them.
function indexed_trie_root(source : IndexedTrieSource) -> hash = {
let mark = scratch_begin();
let initial = indexed_trie_begin(source);
let root_path = path_empty();
let (subtree, remaining) = indexed_trie_subtree(source, initial, root_path);
let retained = remaining.receipt_remaining;
let cursor_empty = indexed_trie_cursor_empty(remaining);
let cursor_not_empty = not_bool(cursor_empty);
if cursor_not_empty | retained.len != 0 then {
fatal_error(WitnessDeficient)
};
let root = trie_subtree_root(subtree);
scratch_rewind(mark);
root
}A schema-bounded source reference to encoded transaction envelopes.
type TransactionListRef = BoundedSszListRef(transaction_count_bound)The common digest type used by trie, code, and block hashes.
type hash = b256function withdrawals_trie_root¶
The withdrawals-trie root (EIP-4895): leaf i holds
rlp(withdrawal_i), keyed by rlp(i).
function withdrawals_trie_root(wds : WithdrawalListRef) -> hash = {
let source = IndexedWithdrawals(wds);
indexed_trie_root(source)
}Computes one complete index-keyed trie. Temporary withdrawal encodings and node encodings share one scratch suffix, released after the root has absorbed every leaf. Receipt records beneath the mark remain live until their accumulator releases them.
function indexed_trie_root(source : IndexedTrieSource) -> hash = {
let mark = scratch_begin();
let initial = indexed_trie_begin(source);
let root_path = path_empty();
let (subtree, remaining) = indexed_trie_subtree(source, initial, root_path);
let retained = remaining.receipt_remaining;
let cursor_empty = indexed_trie_cursor_empty(remaining);
let cursor_not_empty = not_bool(cursor_empty);
if cursor_not_empty | retained.len != 0 then {
fatal_error(WitnessDeficient)
};
let root = trie_subtree_root(subtree);
scratch_rewind(mark);
root
}A schema-bounded source reference to withdrawals.
type WithdrawalListRef = BoundedSszListRef(withdrawal_count_bound)The common digest type used by trie, code, and block hashes.
type hash = b256function indexed_receipt_trie_root¶
The receipts-trie root over execution-ordered retained records.
function indexed_receipt_trie_root(receipts : ReceiptRecordsRef) -> hash = {
let source = IndexedReceipts(receipts);
indexed_trie_root(source)
}The receipts-trie root over execution-ordered retained records.
function indexed_receipt_trie_root(receipts : ReceiptRecordsRef) -> hash = {
let source = IndexedReceipts(receipts);
indexed_trie_root(source)
}Computes one complete index-keyed trie. Temporary withdrawal encodings and node encodings share one scratch suffix, released after the root has absorbed every leaf. Receipt records beneath the mark remain live until their accumulator releases them.
function indexed_trie_root(source : IndexedTrieSource) -> hash = {
let mark = scratch_begin();
let initial = indexed_trie_begin(source);
let root_path = path_empty();
let (subtree, remaining) = indexed_trie_subtree(source, initial, root_path);
let retained = remaining.receipt_remaining;
let cursor_empty = indexed_trie_cursor_empty(remaining);
let cursor_not_empty = not_bool(cursor_empty);
if cursor_not_empty | retained.len != 0 then {
fatal_error(WitnessDeficient)
};
let root = trie_subtree_root(subtree);
scratch_rewind(mark);
root
}One execution-ordered sequence of length-prefixed encoded receipts in the scratch arena. The records contain no Sail list or aggregate receipt values: each push writes an eight-byte little-endian length followed by the canonical trie value.
struct ReceiptRecordsRef = {
bytes : ScratchSlice,
count : transaction_count,
}The common digest type used by trie, code, and block hashes.
type hash = b256function expected_payload_excess_blob_gas¶
The excess_blob_gas the header must carry, derived from the
authenticated parent (EIP-4844).
function expected_payload_excess_blob_gas(witness : WitnessContext) -> excess_blob_gas = {
let execution_profile = k_execution_profile;
next_excess_blob_gas(
execution_profile.protocol,
witness.parent_excess_blob_gas,
witness.parent_blob_gas_used,
witness.parent_base_fee_per_gas,
)
}The header excess_blob_gas rule: decreases toward zero when the
parent underused blobs, otherwise accumulates; from Osaka, EIP-7918
substitutes the reserve-price form when the execution base fee
dominates.
function next_excess_blob_gas(
profile : ProtocolProfile,
parent_excess_blob_gas : excess_blob_gas,
parent_blob_gas_used : blob_gas_used,
parent_base_fee_per_gas : word,
) -> (
excess_blob_gas
) = {
let parent_blob_gas = parent_excess_blob_gas + parent_blob_gas_used;
let target_blob_gas = sizeof(gas_per_blob_value) * profile.blob_schedule.target;
if parent_blob_gas < target_blob_gas then {
0
} else {
/* EIP-7918 compares BLOB_BASE_COST*base_fee with
GAS_PER_BLOB*blob_fee. Their ratio is exactly 16, so this avoids
forming either potentially 257+-bit product. */
let parent_blob_base_fee = blob_base_fee(
profile.fork,
profile.blob_schedule,
profile.excess_blob_gas_limit,
parent_excess_blob_gas,
);
if (profile.fork >= Osaka) & (16 * parent_blob_base_fee < parent_base_fee_per_gas) then {
let maximum = profile.blob_schedule.max;
if maximum == 0 then {
fatal_error(InvalidConfig)
};
let share = (parent_blob_gas_used * (maximum - profile.blob_schedule.target)) / maximum;
let next = parent_excess_blob_gas + share;
let limit = profile.excess_blob_gas_limit;
if next <= limit then {
next
} else {
fatal_error(InvalidConfig)
}
} else {
let next = parent_blob_gas - target_blob_gas;
let limit = profile.excess_blob_gas_limit;
if next <= limit then {
next
} else {
fatal_error(InvalidConfig)
}
}
}
}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_PROFILEAuthenticated facts about the parent block that validation rules compare the payload header against.
struct WitnessContext = {
parent_hash : hash,
parent_state_root : hash,
parent_base_fee_per_gas : word,
parent_blob_gas_used : blob_gas_used,
parent_excess_blob_gas : excess_blob_gas,
}The accumulated excess blob gas carried between headers (EIP-4844).
type excess_blob_gas = range(0, excess_blob_gas_bound)function execution_requests_hash¶
The EIP-7685 requests hash: sha256 over the present request-type
digests in request-type order; the request bodies remain
region-backed through the hash calls.
function execution_requests_hash(input_ref : StatelessInputRef) -> hash = {
let deposits = input_ref.deposits;
let withdrawal_requests = input_ref.withdrawal_requests;
let consolidation_requests = input_ref.consolidation_requests;
let builder_deposit_requests = input_ref.builder_deposit_requests;
let builder_exit_requests = input_ref.builder_exit_requests;
let d0 : hash =
if deposits.len != 0 then sha256_request_digest(0x00, deposits) else ZERO_HASH;
let d1 : hash =
if withdrawal_requests.len != 0 then sha256_request_digest(0x01, withdrawal_requests) else ZERO_HASH;
let d2 : hash =
if consolidation_requests.len != 0 then sha256_request_digest(0x02, consolidation_requests) else ZERO_HASH;
let d3 : hash =
if builder_deposit_requests.len != 0 then sha256_request_digest(0x03, builder_deposit_requests) else ZERO_HASH;
let d4 : hash =
if builder_exit_requests.len != 0 then sha256_request_digest(0x04, builder_exit_requests) else ZERO_HASH;
let mark = scratch_reserve(5 * WORD_BYTE_LENGTH);
if deposits.len != 0 then {
scratch_push_b256(d0, WORD_BYTE_LENGTH)
};
if withdrawal_requests.len != 0 then {
scratch_push_b256(d1, WORD_BYTE_LENGTH)
};
if consolidation_requests.len != 0 then {
scratch_push_b256(d2, WORD_BYTE_LENGTH)
};
if builder_deposit_requests.len != 0 then {
scratch_push_b256(d3, WORD_BYTE_LENGTH)
};
if builder_exit_requests.len != 0 then {
scratch_push_b256(d4, WORD_BYTE_LENGTH)
};
let request_bytes = scratch_finish(mark);
let digest = sha256(request_bytes);
scratch_rewind(mark);
digest
}The slice covering everything pushed since start.
function scratch_finish(start : source_pointer) -> ScratchSlice =
let start_offset = start in
let arena = scratch_arena in
let stop_offset = arena.len in
if start_offset <= stop_offset then {
sub_slice(arena, start, stop_offset - start_offset)
} else {
assert(false, "scratch finish mark");
EMPTY_SCRATCH_SLICE
}Appends a prefix of a fixed 32-byte value at the cursor.
function scratch_push_b256(data : b256, len : range(0, 32)) -> unit = {
if len != 0 then {
let arena = scratch_arena;
scratch_arena = host_scratch_store_b256(arena.len, data, len)
}
}function scratch_reserve(len) = {
let arena = scratch_arena;
let reserved = host_scratch_reserve(arena.len, len);
assert(reserved, "scratch reserve");
arena.len
}Discards everything pushed since mark.
function scratch_rewind(mark : source_pointer) -> unit =
let mark_offset = mark in
let arena = scratch_arena in
let cursor_offset = arena.len in
if mark_offset <= cursor_offset then {
scratch_arena = sub_slice(arena, 0, mark);
host_scratch_truncate(mark)
} else {
assert(false, "scratch rewind mark")
}The EIP-7685 per-type request digest:
sha256(request_type ‖ request_data).
function sha256_request_digest(request_type : byte, s : StatelessInputSlice) -> hash = {
let digest_length = scratch_length_add(1, s.len);
let mark = scratch_reserve(digest_length);
scratch_push_byte(request_type);
scratch_push_slice(s);
let preimage = scratch_finish(mark);
let digest = sha256(preimage);
scratch_rewind(mark);
digest
}let WORD_BYTE_LENGTH : int(32) = 32let ZERO_HASH : hash = hash_from_bits(0x0000000000000000000000000000000000000000000000000000000000000000)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,
}The common digest type used by trie, code, and block hashes.
type hash = b256function validate_execution_payload¶
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)
}
}
}The block header hash: keccak256(rlp(header)) with the recomputed
body roots spliced in (YP §4.4; post-merge constants for ommers,
difficulty, and nonce).
function block_header_hash(
header : BlockHeader,
transactions_root : hash,
withdrawals_root : hash,
requests_hash : hash,
block_access_list_hash : hash,
) -> (
hash
) = {
let execution_profile = k_execution_profile;
let profile = execution_profile.protocol;
let word_length = rlp_word_size();
let address_length = rlp_addr_size();
let bloom_length = 3 + LOGS_BLOOM_BYTE_LENGTH;
let difficulty_length = rlp_uint_size(0);
let number_length = rlp_uint_size(header.number);
let gas_limit_length = rlp_uint_size(header.gas_limit);
let gas_used_length = rlp_uint_size(header.gas_used);
let timestamp_length = rlp_uint_size(header.timestamp);
let extra_data_length = rlp_scratch_slice_size(header.extra_data);
let nonce_length = 1 + EIGHT_BYTE_LENGTH;
var content_length : rlp_scratch_length = rlp_scratch_length_add(6 * word_length, address_length);
content_length = rlp_scratch_length_add(content_length, bloom_length);
content_length = rlp_scratch_length_add(content_length, difficulty_length);
content_length = rlp_scratch_length_add(content_length, number_length);
content_length = rlp_scratch_length_add(content_length, gas_limit_length);
content_length = rlp_scratch_length_add(content_length, gas_used_length);
content_length = rlp_scratch_length_add(content_length, timestamp_length);
content_length = rlp_scratch_length_add(content_length, extra_data_length);
content_length = rlp_scratch_length_add(content_length, nonce_length);
if profile.fork >= London then {
let field_length = rlp_uint_word_size(header.base_fee);
content_length = rlp_scratch_length_add(content_length, field_length)
};
if profile.fork >= Shanghai then {
content_length = rlp_scratch_length_add(content_length, word_length)
};
if profile.fork >= Cancun then {
let blob_gas_used_length = rlp_uint_size(header.blob_gas_used);
let excess_blob_gas_length = rlp_uint_size(header.excess_blob_gas);
content_length = rlp_scratch_length_add(content_length, blob_gas_used_length);
content_length = rlp_scratch_length_add(content_length, excess_blob_gas_length);
content_length = rlp_scratch_length_add(content_length, word_length)
};
if profile.fork >= Prague then {
content_length = rlp_scratch_length_add(content_length, word_length)
};
if profile.fork >= Amsterdam then {
let slot_number_length = rlp_uint_size(header.slot_number);
content_length = rlp_scratch_length_add(content_length, word_length);
content_length = rlp_scratch_length_add(content_length, slot_number_length)
};
/* Six fixed words plus the protocol-bounded variable fields give an
* Amsterdam header-content maximum of 749 bytes. Keep that semantic
* bound instead of reconstructing a generic backend byte length. */
if 749 < content_length then {
fatal_error(RlpDecode)
};
let bounded_content_length : range(0, 749) = tmod_nat(content_length, 750);
let content_len = bounded_content_length;
let encoded_length = rlp_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_length);
rlp_write_list_prefix(content_len);
let parent_hash = hash_to_word(header.parent_hash);
rlp_write_word(parent_hash);
let ommer_hash = hash_to_word(EMPTY_OMMER_HASH);
rlp_write_word(ommer_hash);
rlp_write_addr(header.fee_recipient);
let state_root = hash_to_word(header.state_root);
rlp_write_word(state_root);
let transactions_root_word = hash_to_word(transactions_root);
rlp_write_word(transactions_root_word);
let receipts_root = hash_to_word(header.receipts_root);
rlp_write_word(receipts_root);
let logs_bloom = logs_bloom_from_ref(header.logs_bloom);
rlp_write_logs_bloom(logs_bloom);
rlp_write_uint(0);
rlp_write_uint(header.number);
rlp_write_uint(header.gas_limit);
rlp_write_uint(header.gas_used);
rlp_write_uint(header.timestamp);
rlp_write_slice(header.extra_data);
rlp_write_word(header.prev_randao);
rlp_write_string_prefix(EIGHT_BYTE_LENGTH, 0x00);
scratch_push_word_be(ZERO_WORD, EIGHT_BYTE_LENGTH);
if profile.fork >= London then {
rlp_write_uint_word(header.base_fee)
};
if profile.fork >= Shanghai then {
let withdrawals_root_word = hash_to_word(withdrawals_root);
rlp_write_word(withdrawals_root_word)
};
if profile.fork >= Cancun then {
rlp_write_uint(header.blob_gas_used);
rlp_write_uint(header.excess_blob_gas);
let parent_beacon_block_root = hash_to_word(header.parent_beacon_block_root);
rlp_write_word(parent_beacon_block_root)
};
if profile.fork >= Prague then {
let requests_hash_word = hash_to_word(requests_hash);
rlp_write_word(requests_hash_word)
};
if profile.fork >= Amsterdam then {
let block_access_list_hash_word = hash_to_word(block_access_list_hash);
rlp_write_word(block_access_list_hash_word);
rlp_write_uint(header.slot_number)
};
let encoded = rlp_encoder_finish(encoder);
let block_hash = keccak256(encoded);
rlp_encoder_rewind(encoder);
block_hash
}The EIP-7685 requests hash: sha256 over the present request-type
digests in request-type order; the request bodies remain
region-backed through the hash calls.
function execution_requests_hash(input_ref : StatelessInputRef) -> hash = {
let deposits = input_ref.deposits;
let withdrawal_requests = input_ref.withdrawal_requests;
let consolidation_requests = input_ref.consolidation_requests;
let builder_deposit_requests = input_ref.builder_deposit_requests;
let builder_exit_requests = input_ref.builder_exit_requests;
let d0 : hash =
if deposits.len != 0 then sha256_request_digest(0x00, deposits) else ZERO_HASH;
let d1 : hash =
if withdrawal_requests.len != 0 then sha256_request_digest(0x01, withdrawal_requests) else ZERO_HASH;
let d2 : hash =
if consolidation_requests.len != 0 then sha256_request_digest(0x02, consolidation_requests) else ZERO_HASH;
let d3 : hash =
if builder_deposit_requests.len != 0 then sha256_request_digest(0x03, builder_deposit_requests) else ZERO_HASH;
let d4 : hash =
if builder_exit_requests.len != 0 then sha256_request_digest(0x04, builder_exit_requests) else ZERO_HASH;
let mark = scratch_reserve(5 * WORD_BYTE_LENGTH);
if deposits.len != 0 then {
scratch_push_b256(d0, WORD_BYTE_LENGTH)
};
if withdrawal_requests.len != 0 then {
scratch_push_b256(d1, WORD_BYTE_LENGTH)
};
if consolidation_requests.len != 0 then {
scratch_push_b256(d2, WORD_BYTE_LENGTH)
};
if builder_deposit_requests.len != 0 then {
scratch_push_b256(d3, WORD_BYTE_LENGTH)
};
if builder_exit_requests.len != 0 then {
scratch_push_b256(d4, WORD_BYTE_LENGTH)
};
let request_bytes = scratch_finish(mark);
let digest = sha256(request_bytes);
scratch_rewind(mark);
digest
}The excess_blob_gas the header must carry, derived from the
authenticated parent (EIP-4844).
function expected_payload_excess_blob_gas(witness : WitnessContext) -> excess_blob_gas = {
let execution_profile = k_execution_profile;
next_excess_blob_gas(
execution_profile.protocol,
witness.parent_excess_blob_gas,
witness.parent_blob_gas_used,
witness.parent_base_fee_per_gas,
)
}function fatal_error(_reason) = exit(())The transactions-trie root (YP §4.4.2): leaf i holds the raw
EIP-2718 envelope of transaction i, keyed by rlp(i).
function transaction_trie_root(txs : TransactionListRef) -> hash = {
let source = IndexedTransactions(txs);
indexed_trie_root(source)
}The withdrawals-trie root (EIP-4895): leaf i holds
rlp(withdrawal_i), keyed by rlp(i).
function withdrawals_trie_root(wds : WithdrawalListRef) -> hash = {
let source = IndexedWithdrawals(wds);
indexed_trie_root(source)
}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)keccak256(rlp("")) — the root of an empty Merkle-Patricia trie: the
storage root of every account with no storage (EMPTY_ACCOUNT, freshly
created).
let EMPTY_TRIE_ROOT : hash = hash_from_bits(0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421)EIP-4399 PREVRANDAO replaces DIFFICULTY.
let Paris : int(paris_fork_value) = sizeof(paris_fork_value)EIP-7623 calldata floor; BLS precompiles 0x0b-0x11.
let Prague : int(prague_fork_value) = sizeof(prague_fork_value)EIP-3651 warm coinbase, EIP-3855 PUSH0, EIP-3860 initcode.
let Shanghai : int(shanghai_fork_value) = sizeof(shanghai_fork_value)let ZERO_HASH : hash = hash_from_bits(0x0000000000000000000000000000000000000000000000000000000000000000)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 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,
}The guest's decoded input: the execution payload and the chain configuration it executes under.
struct StatelessInput = {
payload : ExecutionPayload,
chain_config : ChainConfig,
}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,
}Authenticated facts about the parent block that validation rules compare the payload header against.
struct WitnessContext = {
parent_hash : hash,
parent_state_root : hash,
parent_base_fee_per_gas : word,
parent_blob_gas_used : blob_gas_used,
parent_excess_blob_gas : excess_blob_gas,
}The accumulated excess blob gas carried between headers (EIP-4844).
type excess_blob_gas = range(0, excess_blob_gas_bound)