Merkle-Patricia trie node codec¶
Canonical hex-prefix and RLP encoding, decoding, and references for trie nodes (YP Appendix C/D).
function node_ref_size¶
Returns the RLP width of a child reference in its parent node.
function node_ref_size(r : NodeRef) -> range(0, 33) =
match r {
EmptyRef() => 1,
InputInlineRef(node) => node.len,
ScratchInlineRef(node) => node.len,
HashRef(_) => rlp_word_size(),
}function rlp_word_size() -> int(33) = RLP_ENCODED_WORD_LENGTHA reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}function rlp_write_node_ref¶
Appends a child reference in its canonical RLP representation.
function rlp_write_node_ref(r : NodeRef) -> unit =
match r {
EmptyRef() => scratch_push_byte(0x80),
InputInlineRef(node) => scratch_push_slice(node),
ScratchInlineRef(node) => scratch_push_b256(node.data, node.len),
HashRef(h) => {
let hash_word = hash_to_word(h);
rlp_write_word(hash_word)
},
}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],
)Appends one full-width EVM word as an RLP byte string.
function rlp_write_word(w : word) -> unit = {
rlp_write_string_prefix(WORD_BYTE_LENGTH, 0x00);
scratch_push_word_be(w, WORD_BYTE_LENGTH)
}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)
}
}Appends one byte without constructing a Sail list.
function scratch_push_byte(data : byte) -> unit = {
let arena = scratch_arena;
scratch_arena = host_scratch_store_byte(arena.len, data)
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}function rlp_hex_prefix_size¶
Returns the canonical RLP width of a compact trie path.
function rlp_hex_prefix_size(path : TriePath, is_leaf : bool) -> range(1, 34) = {
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
if (encoded_length == 1) & (first[7] == bitzero) then {
1
} else {
encoded_length + 1
}
}The encoded byte length of the hex-prefix form of a trie path.
function hex_prefix_encoded_length(path : TriePath) -> range(1, 33) = {
let length : trie_path_len = path_len(path);
let packed_pair_count : range(0, 32) = tdiv_nat(length, 2);
1 + packed_pair_count
}The flag byte beginning the hex-prefix form of a trie path.
function hex_prefix_first_byte(path : TriePath, is_leaf : bool) -> byte = {
let length : trie_path_len = path_len(path);
let odd = tmod_nat(length, 2) != 0;
let flag : nibble =
if is_leaf then 0x2 else 0x0;
if odd then {
let first_nibble = path_nibble(path, 0);
append(flag | 0x1, first_nibble)
} else {
append(flag, 0x0)
}
}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 rlp_write_hex_prefix¶
Writes the hex-prefix path directly into scratch in wire order.
function rlp_write_hex_prefix(path : TriePath, is_leaf : bool) -> unit = {
let length = path_len(path);
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
rlp_write_string_prefix(encoded_length, first);
scratch_push_byte(first);
let odd_length = tmod_nat(length, 2) != 0;
var index : hex_prefix_cursor =
if odd_length then 1 else 0;
while index < length termination_measure(length - index) do {
let current : trie_path_cursor = tmod_nat(index, 65);
let next : trie_path_cursor = tmod_nat(current + 1, 65);
let high = path_nibble(path, current);
let low = path_nibble(path, next);
let path_byte = append(high, low);
scratch_push_byte(path_byte);
index = next + 1
}
}The encoded byte length of the hex-prefix form of a trie path.
function hex_prefix_encoded_length(path : TriePath) -> range(1, 33) = {
let length : trie_path_len = path_len(path);
let packed_pair_count : range(0, 32) = tdiv_nat(length, 2);
1 + packed_pair_count
}The flag byte beginning the hex-prefix form of a trie path.
function hex_prefix_first_byte(path : TriePath, is_leaf : bool) -> byte = {
let length : trie_path_len = path_len(path);
let odd = tmod_nat(length, 2) != 0;
let flag : nibble =
if is_leaf then 0x2 else 0x0;
if odd then {
let first_nibble = path_nibble(path, 0);
append(flag | 0x1, first_nibble)
} else {
append(flag, 0x0)
}
}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]
}
}function rlp_write_string_prefix(len, first) = {
if (len == 1) & (first[7] == bitzero) then {
return ()
};
if len <= RLP_SHORT_LENGTH_LIMIT then {
let length_byte = rlp_length_byte(len);
let prefix = add_bits(0x80, length_byte);
scratch_push_byte(prefix)
} else {
let length_word = rlp_length_word(len);
let length_len = rlp_minimal_word_len(length_word);
let length_byte = rlp_length_byte(length_len);
let prefix = add_bits(0xb7, length_byte);
scratch_push_byte(prefix);
scratch_push_word_be(length_word, length_len)
}
}Appends one byte without constructing a Sail list.
function scratch_push_byte(data : byte) -> unit = {
let arena = scratch_arena;
scratch_arena = host_scratch_store_byte(arena.len, data)
}Remainder specialized to a non-negative dividend and positive divisor. Singleton operands determine the exact natural-number result.
val tmod_nat = pure {smt: "mod", ocaml: "modulus", interpreter: "modulus", lem: "integerMod", c: "tmod_int", cpp: "tmod_int", systemverilog: "tmod_int", coq: "Z.rem", lean: "Nat.mod", _: "tmod_int"}: forall ('n : Int) ('m : Int), ('n >= 0 & 'm >= 1).
(int('n), int('m)) -> int(mod('n, 'm))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 }A cursor through the at-most-65 positions used by hex-prefix decoding.
type hex_prefix_cursor = range(0, 65)A cursor at or immediately after a position in a trie path.
type trie_path_cursor = range(0, 64)function child_ref¶
The canonical child reference for an encoded node: inline under 32 bytes, otherwise its hash (YP Appendix D, Eq. 207).
function child_ref(encoded : ScratchSlice) -> NodeRef =
if encoded.len < MPT_HASH_LENGTH then {
let inline_node = inline_node_from_slice(encoded);
ScratchInlineRef(inline_node)
} else {
let node_hash = keccak256(encoded);
HashRef(node_hash)
}let MPT_HASH_LENGTH : int(32) = WORD_BYTE_LENGTHA reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}type branch_mask¶
A compact presence bitset for the sixteen children of a branch.
type branch_mask = bits(16)function branch_mask_for¶
Returns the one-hot presence mask for a branch-child nibble.
function branch_mask_for(index : nibble) -> branch_mask = {
let shift = unsigned(index);
sail_shiftleft(0x0001, shift)
}val sail_shiftleft = pure {lean: "_lean_shiftl", _: "shiftl"}: forall ('n : Int) ('amount : Int).
(bitvector('n), int('amount)) -> bitvector('n)converts a bit vector of length $n$ to an integer in the range $0$ to $2^n - 1$.
val unsigned = pure {ocaml: "uint", lem: "uint", interpreter: "uint", coq: "uint", lean: "BitVec.toNatInt", _: "sail_unsigned"}: forall ('n : Int).
bits('n) -> range(0, 2 ^ 'n - 1)A compact presence bitset for the sixteen children of a branch.
type branch_mask = bits(16)A four-bit path element (YP Appendix D).
type nibble = bits(4)function branch_mask_has¶
Whether the mask records a child at the given nibble.
function branch_mask_has(mask : branch_mask, index : nibble) -> bool = {
let index_mask = branch_mask_for(index);
and_vec(mask, index_mask) != 0x0000
}val and_vec = pure {lem: "and_vec", coq: "and_vec", ocaml: "and_vec", interpreter: "and_vec", lean: "_lean_bvand", _: "and_bits"}: forall ('n : Int).
(bits('n), bits('n)) -> bits('n)Returns the one-hot presence mask for a branch-child nibble.
function branch_mask_for(index : nibble) -> branch_mask = {
let shift = unsigned(index);
sail_shiftleft(0x0001, shift)
}A compact presence bitset for the sixteen children of a branch.
type branch_mask = bits(16)A four-bit path element (YP Appendix D).
type nibble = bits(4)function branch_mask_set¶
Returns the mask with the child at the given nibble marked present.
function branch_mask_set(mask : branch_mask, index : nibble) -> branch_mask = {
let index_mask = branch_mask_for(index);
or_vec(mask, index_mask)
}Returns the one-hot presence mask for a branch-child nibble.
function branch_mask_for(index : nibble) -> branch_mask = {
let shift = unsigned(index);
sail_shiftleft(0x0001, shift)
}val or_vec = pure {lem: "or_vec", coq: "or_vec", ocaml: "or_vec", interpreter: "or_vec", lean: "_lean_bvor", _: "or_bits"}: forall ('n : Int).
(bits('n), bits('n)) -> bits('n)A compact presence bitset for the sixteen children of a branch.
type branch_mask = bits(16)A four-bit path element (YP Appendix D).
type nibble = bits(4)function input_leaf_child_ref¶
The child reference of a leaf, keeping the value in its native representation: long nodes hash the RLP framing and value as segments; only an inline node materializes a slice.
function input_leaf_child_ref(key : TriePath, value : StatelessInputSlice) -> NodeRef = {
let path_size = rlp_hex_prefix_size(key, true);
let value_size = rlp_scratch_slice_size(value);
let content_len = rlp_scratch_length_add(path_size, value_size);
let encoded_size = rlp_scratch_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, true);
rlp_write_slice(value);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}The canonical child reference for an encoded node: inline under 32 bytes, otherwise its hash (YP Appendix D, Eq. 207).
function child_ref(encoded : ScratchSlice) -> NodeRef =
if encoded.len < MPT_HASH_LENGTH then {
let inline_node = inline_node_from_slice(encoded);
ScratchInlineRef(inline_node)
} else {
let node_hash = keccak256(encoded);
HashRef(node_hash)
}function rlp_encoder_begin(expected_len) =
struct { start = scratch_reserve(expected_len), expected_len = expected_len }Finishes an exact-size construction and retains its encoded bytes.
function rlp_encoder_finish(encoder : RlpEncoder) -> ScratchSlice = {
let encoded = scratch_finish(encoder.start);
assert(encoded.len == encoder.expected_len, "RLP encoder length");
encoded
}Releases every byte appended by an encoder after its result is consumed.
function rlp_encoder_rewind(encoder : RlpEncoder) -> unit =
scratch_rewind(encoder.start)Returns the canonical RLP width of a compact trie path.
function rlp_hex_prefix_size(path : TriePath, is_leaf : bool) -> range(1, 34) = {
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
if (encoded_length == 1) & (first[7] == bitzero) then {
1
} else {
encoded_length + 1
}
}function rlp_scratch_length_add(left, right) =
if right <= sizeof(scratch_region_bound) - left then {
left + right
} else {
fatal_error(RlpDecode)
}Adds the canonical list prefix to a materializable RLP content length.
function rlp_scratch_list_size(content_len : rlp_scratch_length) -> rlp_scratch_length = {
let prefix_size = rlp_length_prefix_len(content_len);
let prefix_length = rlp_scratch_small_length(prefix_size);
rlp_scratch_length_add(content_len, prefix_length)
}Writes the hex-prefix path directly into scratch in wire order.
function rlp_write_hex_prefix(path : TriePath, is_leaf : bool) -> unit = {
let length = path_len(path);
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
rlp_write_string_prefix(encoded_length, first);
scratch_push_byte(first);
let odd_length = tmod_nat(length, 2) != 0;
var index : hex_prefix_cursor =
if odd_length then 1 else 0;
while index < length termination_measure(length - index) do {
let current : trie_path_cursor = tmod_nat(index, 65);
let next : trie_path_cursor = tmod_nat(current + 1, 65);
let high = path_nibble(path, current);
let low = path_nibble(path, next);
let path_byte = append(high, low);
scratch_push_byte(path_byte);
index = next + 1
}
}function rlp_write_list_prefix(content_len) = {
if content_len <= RLP_SHORT_LENGTH_LIMIT then {
let length_byte = rlp_length_byte(content_len);
let prefix = add_bits(0xc0, length_byte);
scratch_push_byte(prefix)
} else {
let length_word = rlp_length_word(content_len);
let length_len = rlp_minimal_word_len(length_word);
let length_byte = rlp_length_byte(length_len);
let prefix = add_bits(0xf7, length_byte);
scratch_push_byte(prefix);
scratch_push_word_be(length_word, length_len)
}
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}A 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 scratch_leaf_child_ref¶
input_leaf_child_ref for a leaf value encoded in the scratch arena (YP Appendix D, Eq. 208).
function scratch_leaf_child_ref(key : TriePath, value : ScratchSlice) -> NodeRef = {
let path_size = rlp_hex_prefix_size(key, true);
let value_size = rlp_scratch_slice_size(value);
let content_len = rlp_scratch_length_add(path_size, value_size);
let encoded_size = rlp_scratch_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, true);
rlp_write_slice(value);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}The canonical child reference for an encoded node: inline under 32 bytes, otherwise its hash (YP Appendix D, Eq. 207).
function child_ref(encoded : ScratchSlice) -> NodeRef =
if encoded.len < MPT_HASH_LENGTH then {
let inline_node = inline_node_from_slice(encoded);
ScratchInlineRef(inline_node)
} else {
let node_hash = keccak256(encoded);
HashRef(node_hash)
}function rlp_encoder_begin(expected_len) =
struct { start = scratch_reserve(expected_len), expected_len = expected_len }Finishes an exact-size construction and retains its encoded bytes.
function rlp_encoder_finish(encoder : RlpEncoder) -> ScratchSlice = {
let encoded = scratch_finish(encoder.start);
assert(encoded.len == encoder.expected_len, "RLP encoder length");
encoded
}Releases every byte appended by an encoder after its result is consumed.
function rlp_encoder_rewind(encoder : RlpEncoder) -> unit =
scratch_rewind(encoder.start)Returns the canonical RLP width of a compact trie path.
function rlp_hex_prefix_size(path : TriePath, is_leaf : bool) -> range(1, 34) = {
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
if (encoded_length == 1) & (first[7] == bitzero) then {
1
} else {
encoded_length + 1
}
}function rlp_scratch_length_add(left, right) =
if right <= sizeof(scratch_region_bound) - left then {
left + right
} else {
fatal_error(RlpDecode)
}Adds the canonical list prefix to a materializable RLP content length.
function rlp_scratch_list_size(content_len : rlp_scratch_length) -> rlp_scratch_length = {
let prefix_size = rlp_length_prefix_len(content_len);
let prefix_length = rlp_scratch_small_length(prefix_size);
rlp_scratch_length_add(content_len, prefix_length)
}Writes the hex-prefix path directly into scratch in wire order.
function rlp_write_hex_prefix(path : TriePath, is_leaf : bool) -> unit = {
let length = path_len(path);
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
rlp_write_string_prefix(encoded_length, first);
scratch_push_byte(first);
let odd_length = tmod_nat(length, 2) != 0;
var index : hex_prefix_cursor =
if odd_length then 1 else 0;
while index < length termination_measure(length - index) do {
let current : trie_path_cursor = tmod_nat(index, 65);
let next : trie_path_cursor = tmod_nat(current + 1, 65);
let high = path_nibble(path, current);
let low = path_nibble(path, next);
let path_byte = append(high, low);
scratch_push_byte(path_byte);
index = next + 1
}
}function rlp_write_list_prefix(content_len) = {
if content_len <= RLP_SHORT_LENGTH_LIMIT then {
let length_byte = rlp_length_byte(content_len);
let prefix = add_bits(0xc0, length_byte);
scratch_push_byte(prefix)
} else {
let length_word = rlp_length_word(content_len);
let length_len = rlp_minimal_word_len(length_word);
let length_byte = rlp_length_byte(length_len);
let prefix = add_bits(0xf7, length_byte);
scratch_push_byte(prefix);
scratch_push_word_be(length_word, length_len)
}
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}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 leaf_child_ref¶
The child reference of a leaf, selecting the encoder for the value's backing region.
function leaf_child_ref(key : TriePath, value : TrieLeafValue) -> NodeRef =
match value {
InputTrieLeaf(bytes) => input_leaf_child_ref(key, bytes),
ScratchTrieLeaf(bytes) => scratch_leaf_child_ref(key, bytes),
}The child reference of a leaf, keeping the value in its native representation: long nodes hash the RLP framing and value as segments; only an inline node materializes a slice.
function input_leaf_child_ref(key : TriePath, value : StatelessInputSlice) -> NodeRef = {
let path_size = rlp_hex_prefix_size(key, true);
let value_size = rlp_scratch_slice_size(value);
let content_len = rlp_scratch_length_add(path_size, value_size);
let encoded_size = rlp_scratch_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, true);
rlp_write_slice(value);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}input_leaf_child_ref for a leaf value encoded in the scratch arena (YP Appendix D, Eq. 208).
function scratch_leaf_child_ref(key : TriePath, value : ScratchSlice) -> NodeRef = {
let path_size = rlp_hex_prefix_size(key, true);
let value_size = rlp_scratch_slice_size(value);
let content_len = rlp_scratch_length_add(path_size, value_size);
let encoded_size = rlp_scratch_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, true);
rlp_write_slice(value);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}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,
}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 extension_child_ref¶
The child reference of an extension node.
function extension_child_ref(key : TriePath, childref : NodeRef) -> NodeRef = {
let path_length = rlp_hex_prefix_size(key, false);
let child_length = node_ref_size(childref);
let content_len = path_length + child_length;
let encoded_size = rlp_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, false);
rlp_write_node_ref(childref);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}The canonical child reference for an encoded node: inline under 32 bytes, otherwise its hash (YP Appendix D, Eq. 207).
function child_ref(encoded : ScratchSlice) -> NodeRef =
if encoded.len < MPT_HASH_LENGTH then {
let inline_node = inline_node_from_slice(encoded);
ScratchInlineRef(inline_node)
} else {
let node_hash = keccak256(encoded);
HashRef(node_hash)
}Returns the RLP width of a child reference in its parent node.
function node_ref_size(r : NodeRef) -> range(0, 33) =
match r {
EmptyRef() => 1,
InputInlineRef(node) => node.len,
ScratchInlineRef(node) => node.len,
HashRef(_) => rlp_word_size(),
}function rlp_encoder_begin(expected_len) =
struct { start = scratch_reserve(expected_len), expected_len = expected_len }Finishes an exact-size construction and retains its encoded bytes.
function rlp_encoder_finish(encoder : RlpEncoder) -> ScratchSlice = {
let encoded = scratch_finish(encoder.start);
assert(encoded.len == encoder.expected_len, "RLP encoder length");
encoded
}Releases every byte appended by an encoder after its result is consumed.
function rlp_encoder_rewind(encoder : RlpEncoder) -> unit =
scratch_rewind(encoder.start)Returns the canonical RLP width of a compact trie path.
function rlp_hex_prefix_size(path : TriePath, is_leaf : bool) -> range(1, 34) = {
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
if (encoded_length == 1) & (first[7] == bitzero) then {
1
} else {
encoded_length + 1
}
}function rlp_list_size(content_len) = {
rlp_length_prefix_len(content_len) + content_len
}Writes the hex-prefix path directly into scratch in wire order.
function rlp_write_hex_prefix(path : TriePath, is_leaf : bool) -> unit = {
let length = path_len(path);
let encoded_length = hex_prefix_encoded_length(path);
let first = hex_prefix_first_byte(path, is_leaf);
rlp_write_string_prefix(encoded_length, first);
scratch_push_byte(first);
let odd_length = tmod_nat(length, 2) != 0;
var index : hex_prefix_cursor =
if odd_length then 1 else 0;
while index < length termination_measure(length - index) do {
let current : trie_path_cursor = tmod_nat(index, 65);
let next : trie_path_cursor = tmod_nat(current + 1, 65);
let high = path_nibble(path, current);
let low = path_nibble(path, next);
let path_byte = append(high, low);
scratch_push_byte(path_byte);
index = next + 1
}
}function rlp_write_list_prefix(content_len) = {
if content_len <= RLP_SHORT_LENGTH_LIMIT then {
let length_byte = rlp_length_byte(content_len);
let prefix = add_bits(0xc0, length_byte);
scratch_push_byte(prefix)
} else {
let length_word = rlp_length_word(content_len);
let length_len = rlp_minimal_word_len(length_word);
let length_byte = rlp_length_byte(length_len);
let prefix = add_bits(0xf7, length_byte);
scratch_push_byte(prefix);
scratch_push_word_be(length_word, length_len)
}
}Appends a child reference in its canonical RLP representation.
function rlp_write_node_ref(r : NodeRef) -> unit =
match r {
EmptyRef() => scratch_push_byte(0x80),
InputInlineRef(node) => scratch_push_slice(node),
ScratchInlineRef(node) => scratch_push_b256(node.data, node.len),
HashRef(h) => {
let hash_word = hash_to_word(h);
rlp_write_word(hash_word)
},
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}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 branch_child_ref¶
The child reference of a branch node.
function branch_child_ref(mask : branch_mask, children : BranchRefs) -> NodeRef = {
var content_length : branch_content_length = 1;
var child_bit : branch_mask = 0x0001;
foreach (i from 0 to 15) {
let child_present = and_vec(mask, child_bit) != 0x0000;
if child_present then {
let childref = children[i];
let child_length = node_ref_size(childref);
content_length = branch_content_length_add(content_length, child_length)
} else {
content_length = branch_content_length_add(content_length, 1)
};
child_bit = sail_shiftleft(child_bit, 1)
};
let scratch_content_length = rlp_scratch_length_add(content_length, 0);
let encoded_size = rlp_scratch_list_size(scratch_content_length);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_length);
child_bit = 0x0001;
foreach (i from 0 to 15) {
let child_present = and_vec(mask, child_bit) != 0x0000;
if child_present then {
let childref = children[i];
rlp_write_node_ref(childref)
} else {
scratch_push_byte(0x80)
};
child_bit = sail_shiftleft(child_bit, 1)
};
scratch_push_byte(0x80);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}val and_vec = pure {lem: "and_vec", coq: "and_vec", ocaml: "and_vec", interpreter: "and_vec", lean: "_lean_bvand", _: "and_bits"}: forall ('n : Int).
(bits('n), bits('n)) -> bits('n)Advances the branch payload length while preserving its structural bound.
function branch_content_length_add(current : branch_content_length, addition : range(0, 33)) -> branch_content_length =
if addition <= 529 - current then {
current + addition
} else {
fatal_error(RlpDecode)
}The canonical child reference for an encoded node: inline under 32 bytes, otherwise its hash (YP Appendix D, Eq. 207).
function child_ref(encoded : ScratchSlice) -> NodeRef =
if encoded.len < MPT_HASH_LENGTH then {
let inline_node = inline_node_from_slice(encoded);
ScratchInlineRef(inline_node)
} else {
let node_hash = keccak256(encoded);
HashRef(node_hash)
}Returns the RLP width of a child reference in its parent node.
function node_ref_size(r : NodeRef) -> range(0, 33) =
match r {
EmptyRef() => 1,
InputInlineRef(node) => node.len,
ScratchInlineRef(node) => node.len,
HashRef(_) => rlp_word_size(),
}function rlp_encoder_begin(expected_len) =
struct { start = scratch_reserve(expected_len), expected_len = expected_len }Finishes an exact-size construction and retains its encoded bytes.
function rlp_encoder_finish(encoder : RlpEncoder) -> ScratchSlice = {
let encoded = scratch_finish(encoder.start);
assert(encoded.len == encoder.expected_len, "RLP encoder length");
encoded
}Releases every byte appended by an encoder after its result is consumed.
function rlp_encoder_rewind(encoder : RlpEncoder) -> unit =
scratch_rewind(encoder.start)function rlp_scratch_length_add(left, right) =
if right <= sizeof(scratch_region_bound) - left then {
left + right
} else {
fatal_error(RlpDecode)
}Adds the canonical list prefix to a materializable RLP content length.
function rlp_scratch_list_size(content_len : rlp_scratch_length) -> rlp_scratch_length = {
let prefix_size = rlp_length_prefix_len(content_len);
let prefix_length = rlp_scratch_small_length(prefix_size);
rlp_scratch_length_add(content_len, prefix_length)
}function rlp_write_list_prefix(content_len) = {
if content_len <= RLP_SHORT_LENGTH_LIMIT then {
let length_byte = rlp_length_byte(content_len);
let prefix = add_bits(0xc0, length_byte);
scratch_push_byte(prefix)
} else {
let length_word = rlp_length_word(content_len);
let length_len = rlp_minimal_word_len(length_word);
let length_byte = rlp_length_byte(length_len);
let prefix = add_bits(0xf7, length_byte);
scratch_push_byte(prefix);
scratch_push_word_be(length_word, length_len)
}
}Appends a child reference in its canonical RLP representation.
function rlp_write_node_ref(r : NodeRef) -> unit =
match r {
EmptyRef() => scratch_push_byte(0x80),
InputInlineRef(node) => scratch_push_slice(node),
ScratchInlineRef(node) => scratch_push_b256(node.data, node.len),
HashRef(h) => {
let hash_word = hash_to_word(h);
rlp_write_word(hash_word)
},
}val sail_shiftleft = pure {lean: "_lean_shiftl", _: "shiftl"}: forall ('n : Int) ('amount : Int).
(bitvector('n), int('amount)) -> bitvector('n)Appends one byte without constructing a Sail list.
function scratch_push_byte(data : byte) -> unit = {
let arena = scratch_arena;
scratch_arena = host_scratch_store_byte(arena.len, data)
}The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}The RLP payload of a branch contains sixteen child references of at most 33 bytes and one empty value byte.
type branch_content_length = range(0, 529)A compact presence bitset for the sixteen children of a branch.
type branch_mask = bits(16)function trie_ref_to_root¶
The root hash a node reference commits to; the empty reference is the empty-trie root.
function trie_ref_to_root(r : NodeRef) -> hash =
match r {
EmptyRef() => EMPTY_TRIE_ROOT,
InputInlineRef(node) => keccak256(node),
ScratchInlineRef(node) => inline_node_hash(node),
HashRef(h) => h,
}Hashes an inline node from its existing scratch representation.
function inline_node_hash(node : InlineNode) -> hash = {
let mark = scratch_begin();
let encoded = inline_node_slice(node);
let digest = keccak256(encoded);
scratch_rewind(mark);
digest
}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)A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}The common digest type used by trie, code, and block hashes.
type hash = b256function input_node_to_ref¶
The reference form of authenticated input node bytes.
function input_node_to_ref(node : StatelessInputSlice) -> NodeRef =
if node.len == 0 then {
EmptyRef()
} else if node.len < MPT_HASH_LENGTH then {
InputInlineRef(node)
} else {
let node_hash = keccak256(node);
HashRef(node_hash)
}let MPT_HASH_LENGTH : int(32) = WORD_BYTE_LENGTHA reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}function scratch_node_to_ref¶
The reference form of freshly encoded scratch node bytes.
function scratch_node_to_ref(node : ScratchSlice) -> NodeRef =
if node.len == 0 then {
EmptyRef()
} else if node.len < MPT_HASH_LENGTH then {
let inline_node = inline_node_from_slice(node);
ScratchInlineRef(inline_node)
} else {
let node_hash = keccak256(node);
HashRef(node_hash)
}let MPT_HASH_LENGTH : int(32) = WORD_BYTE_LENGTHA reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}function node_db_lookup¶
The witness node bytes whose KECCAK-256 digest is h, retained as a
slice into the stateless input; empty if unwitnessed.
function node_db_lookup(h : hash) -> StatelessInputSlice = {
nodedb_lookup(h)
}The complete stateless-input span recorded for a node hash; the empty slice when the node is unwitnessed. Returning the span as one value keeps its source and host-range constraint intact.
val nodedb_lookup = impure { c: "nodedb_lookup" } : hash -> StatelessInputSliceA stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}The common digest type used by trie, code, and block hashes.
type hash = b256function branch_refs_get¶
Selects a decoded branch child reference by nibble value.
function branch_refs_get(children : BranchRefs, index : nibble) -> NodeRef = match index {
0x0 => children[0],
0x1 => children[1],
0x2 => children[2],
0x3 => children[3],
0x4 => children[4],
0x5 => children[5],
0x6 => children[6],
0x7 => children[7],
0x8 => children[8],
0x9 => children[9],
0xa => children[10],
0xb => children[11],
0xc => children[12],
0xd => children[13],
0xe => children[14],
0xf => children[15],
}The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A four-bit path element (YP Appendix D).
type nibble = bits(4)type InputTrieNode¶
A decoded authenticated node. Every borrowed field remains a stateless input slice, including fields reached through an input-inline child.
union InputTrieNode = {
/* a two-field leaf: its path and value bytes */
InputLeafNode : (TriePath, StatelessInputSlice),
/* a two-field extension: its path and single child reference */
InputExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
InputBranchNode : (BranchRefs, StatelessInputSlice),
}The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}A 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 }type ScratchTrieNode¶
A decoded node freshly encoded in scratch during canonical rebuilding.
union ScratchTrieNode = {
/* a two-field leaf: its path and scratch value bytes */
ScratchLeafNode : (TriePath, ScratchSlice),
/* a two-field extension: its path and single child reference */
ScratchExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
ScratchBranchNode : (BranchRefs, ScratchSlice),
}The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}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 input_field_to_ref¶
The reference denoted by a child field: an inline list, a 32-byte hash, or empty.
function input_field_to_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
RlpFieldRef('source_off, 'source_len, 'content_len)) -> (
NodeRef
) =
if f.is_list then {
if f.source.len < MPT_HASH_LENGTH then {
InputInlineRef(f.source)
} else {
fatal_error(RlpDecode)
}
} else if f.content_len == MPT_HASH_LENGTH then {
let word = rlp_decode_word(f);
let hash = word_to_hash(word);
HashRef(hash)
} else {
EmptyRef()
}function fatal_error(_reason) = exit(())Decodes a string field of at most 32 bytes into a word.
function rlp_decode_word forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
RlpFieldRef('source_off, 'source_len, 'content_len)) -> (
word
) = {
let n = f.content_len;
if f.is_list | (RLP_WORD_LENGTH_LIMIT < n) then {
fatal_error(RlpDecode)
} else {
slice_load_n(f.source, f.source.len - n, n)
}
}Serializes an EVM word as a 32-byte big-endian digest.
function word_to_hash(value : word) -> hash = {
let zero_bytes = vector_init(32, 0x00);
var result : hash = B256(zero_bytes);
result[0] = get_slice_int(8, value, 248);
result[1] = get_slice_int(8, value, 240);
result[2] = get_slice_int(8, value, 232);
result[3] = get_slice_int(8, value, 224);
result[4] = get_slice_int(8, value, 216);
result[5] = get_slice_int(8, value, 208);
result[6] = get_slice_int(8, value, 200);
result[7] = get_slice_int(8, value, 192);
result[8] = get_slice_int(8, value, 184);
result[9] = get_slice_int(8, value, 176);
result[10] = get_slice_int(8, value, 168);
result[11] = get_slice_int(8, value, 160);
result[12] = get_slice_int(8, value, 152);
result[13] = get_slice_int(8, value, 144);
result[14] = get_slice_int(8, value, 136);
result[15] = get_slice_int(8, value, 128);
result[16] = get_slice_int(8, value, 120);
result[17] = get_slice_int(8, value, 112);
result[18] = get_slice_int(8, value, 104);
result[19] = get_slice_int(8, value, 96);
result[20] = get_slice_int(8, value, 88);
result[21] = get_slice_int(8, value, 80);
result[22] = get_slice_int(8, value, 72);
result[23] = get_slice_int(8, value, 64);
result[24] = get_slice_int(8, value, 56);
result[25] = get_slice_int(8, value, 48);
result[26] = get_slice_int(8, value, 40);
result[27] = get_slice_int(8, value, 32);
result[28] = get_slice_int(8, value, 24);
result[29] = get_slice_int(8, value, 16);
result[30] = get_slice_int(8, value, 8);
result[31] = get_slice_int(8, value, 0);
result
}let MPT_HASH_LENGTH : int(32) = WORD_BYTE_LENGTHThe reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}The witness-carrying fields of a decoded RLP reference. Both the complete encoding and its content are statically contained by the source slice.
struct RlpFieldRef(
'source_off : Int,
'source_len : Int,
'content_len : Int,
),
rlp_field_ref_valid(
'source_off,
'source_len,
'content_len,
) = {
source : StatelessInputSliceFields('source_off, 'source_len),
is_list : bool,
content_len : int('content_len),
}The common digest type used by trie, code, and block hashes.
type hash = b256The complete containment invariant for an RLP field reference. source
is normalized to the complete encoded item. RLP content is its suffix, so
the content offset is derived as source.len - content_len.
type rlp_field_ref_valid(
'source_off : Int,
'source_len : Int,
'content_len : Int,
) -> Bool =
source_valid_range('source_off, 'source_len)
& 0 <= 'content_len
& 'content_len <= 'source_lenThe 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 scratch_field_to_ref¶
input_field_to_ref for a scratch-backed child field; an inline list is copied into a self-contained inline node.
function scratch_field_to_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
ScratchRlpFieldRef('source_off, 'source_len, 'content_len)) -> (
NodeRef
) =
if f.is_list then {
let inline_node = inline_node_from_slice(f.source);
ScratchInlineRef(inline_node)
} else if f.content_len == MPT_HASH_LENGTH then {
let word = scratch_rlp_decode_word(f);
let hash = word_to_hash(word);
HashRef(hash)
} else {
EmptyRef()
}rlp_decode_word for a scratch-backed field.
function scratch_rlp_decode_word forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
ScratchRlpFieldRef('source_off, 'source_len, 'content_len)) -> (
word
) = {
let n = f.content_len;
if f.is_list | (RLP_WORD_LENGTH_LIMIT < n) then {
fatal_error(RlpDecode)
} else {
slice_load_n(f.source, f.source.len - n, n)
}
}Serializes an EVM word as a 32-byte big-endian digest.
function word_to_hash(value : word) -> hash = {
let zero_bytes = vector_init(32, 0x00);
var result : hash = B256(zero_bytes);
result[0] = get_slice_int(8, value, 248);
result[1] = get_slice_int(8, value, 240);
result[2] = get_slice_int(8, value, 232);
result[3] = get_slice_int(8, value, 224);
result[4] = get_slice_int(8, value, 216);
result[5] = get_slice_int(8, value, 208);
result[6] = get_slice_int(8, value, 200);
result[7] = get_slice_int(8, value, 192);
result[8] = get_slice_int(8, value, 184);
result[9] = get_slice_int(8, value, 176);
result[10] = get_slice_int(8, value, 168);
result[11] = get_slice_int(8, value, 160);
result[12] = get_slice_int(8, value, 152);
result[13] = get_slice_int(8, value, 144);
result[14] = get_slice_int(8, value, 136);
result[15] = get_slice_int(8, value, 128);
result[16] = get_slice_int(8, value, 120);
result[17] = get_slice_int(8, value, 112);
result[18] = get_slice_int(8, value, 104);
result[19] = get_slice_int(8, value, 96);
result[20] = get_slice_int(8, value, 88);
result[21] = get_slice_int(8, value, 80);
result[22] = get_slice_int(8, value, 72);
result[23] = get_slice_int(8, value, 64);
result[24] = get_slice_int(8, value, 56);
result[25] = get_slice_int(8, value, 48);
result[26] = get_slice_int(8, value, 40);
result[27] = get_slice_int(8, value, 32);
result[28] = get_slice_int(8, value, 24);
result[29] = get_slice_int(8, value, 16);
result[30] = get_slice_int(8, value, 8);
result[31] = get_slice_int(8, value, 0);
result
}let MPT_HASH_LENGTH : int(32) = WORD_BYTE_LENGTHA reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}The same RLP framing invariants over a node encoding held in the scratch arena. Keeping this nominally separate prevents decoded input fields from acquiring a runtime byte-source tag.
struct ScratchRlpFieldRef(
'source_off : Int,
'source_len : Int,
'content_len : Int,
),
rlp_field_ref_valid(
'source_off,
'source_len,
'content_len,
) = {
source : ScratchSliceFields('source_off, 'source_len),
is_list : bool,
content_len : int('content_len),
}The common digest type used by trie, code, and block hashes.
type hash = b256The complete containment invariant for an RLP field reference. source
is normalized to the complete encoded item. RLP content is its suffix, so
the content offset is derived as source.len - content_len.
type rlp_field_ref_valid(
'source_off : Int,
'source_len : Int,
'content_len : Int,
) -> Bool =
source_valid_range('source_off, 'source_len)
& 0 <= 'content_len
& 'content_len <= 'source_lenThe 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 decode_input_branch_node¶
Decodes branch children 2 through 15, followed by the branch value.
function decode_input_branch_node forall 'source_off 'source_len, source_valid_range('source_off, 'source_len). (
cursor : RlpCursor('source_off, 'source_len),
index : range(2, 16),
children : BranchRefs,
) -> (
InputTrieNode
) =
if index < 16 then {
let child = rlp_decode_item(cursor);
let next = rlp_cursor_advance(cursor, child.source.len);
let decoded_child = input_field_to_ref(child);
var updated = children;
updated[index] = decoded_child;
decode_input_branch_node(next, index + 1, updated)
} else {
let value = rlp_decode_item(cursor);
let next = rlp_cursor_advance(cursor, value.source.len);
rlp_cursor_expect_end(next);
let content = rlp_item_content(value);
InputBranchNode(children, content)
}Decodes branch children 2 through 15, followed by the branch value.
function decode_input_branch_node forall 'source_off 'source_len, source_valid_range('source_off, 'source_len). (
cursor : RlpCursor('source_off, 'source_len),
index : range(2, 16),
children : BranchRefs,
) -> (
InputTrieNode
) =
if index < 16 then {
let child = rlp_decode_item(cursor);
let next = rlp_cursor_advance(cursor, child.source.len);
let decoded_child = input_field_to_ref(child);
var updated = children;
updated[index] = decoded_child;
decode_input_branch_node(next, index + 1, updated)
} else {
let value = rlp_decode_item(cursor);
let next = rlp_cursor_advance(cursor, value.source.len);
rlp_cursor_expect_end(next);
let content = rlp_item_content(value);
InputBranchNode(children, content)
}The reference denoted by a child field: an inline list, a 32-byte hash, or empty.
function input_field_to_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
RlpFieldRef('source_off, 'source_len, 'content_len)) -> (
NodeRef
) =
if f.is_list then {
if f.source.len < MPT_HASH_LENGTH then {
InputInlineRef(f.source)
} else {
fatal_error(RlpDecode)
}
} else if f.content_len == MPT_HASH_LENGTH then {
let word = rlp_decode_word(f);
let hash = word_to_hash(word);
HashRef(hash)
} else {
EmptyRef()
}function rlp_cursor_advance(cursor, consumed) =
slice_suffix(cursor, consumed)function rlp_cursor_expect_end(cursor) = {
if cursor.len == 0 then {
return ()
};
fatal_error(RlpDecode)
}function rlp_decode_item(cursor) = {
if cursor.len == 0 then {
fatal_error(RlpDecode)
};
let (is_list, content_off, content_len_value) = rlp_ref_hdr(cursor);
let (content_len as 'content_len) = content_len_value;
if cursor.len < content_off then {
fatal_error(RlpDecode)
};
if cursor.len - content_off < content_len then {
fatal_error(RlpDecode)
};
let (full_len as 'full_len) = content_off + content_len;
if (0 < full_len) & (full_len <= cursor.len) then {
let field_source = sub_slice(cursor, 0, full_len);
let field : RlpFieldRef('source_off, 'full_len, 'content_len) = struct {
source = field_source,
is_list = is_list,
content_len = content_len,
};
field
} else {
fatal_error(RlpDecode)
}
}The content span of a field.
function rlp_item_content forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
RlpFieldRef('source_off, 'source_len, 'content_len)) -> (
StatelessInputSlice
) =
sub_slice(f.source, f.source.len - f.content_len, f.content_len)The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)A decoded authenticated node. Every borrowed field remains a stateless input slice, including fields reached through an input-inline child.
union InputTrieNode = {
/* a two-field leaf: its path and value bytes */
InputLeafNode : (TriePath, StatelessInputSlice),
/* a two-field extension: its path and single child reference */
InputExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
InputBranchNode : (BranchRefs, StatelessInputSlice),
}A one-pass RLP cursor is the unconsumed suffix of its source. Decoding yields a field whose length witnesses a valid advance; the caller owns the corresponding cursor transition.
type RlpCursor('source_off : Int, 'source_len : Int),
source_valid_range('source_off, 'source_len) =
StatelessInputSliceFields('source_off, 'source_len)Common bound for relative source coordinates used by generic cursor operations. Nominal slices retain their region-specific invariant.
type source_valid_range('off : Int, 'len : Int) -> Bool =
0 <= 'off & 0 <= 'len & 'off + 'len <= default_host_region_boundfunction decode_scratch_branch_node¶
decode_input_branch_node over the scratch cursor family.
function decode_scratch_branch_node forall 'source_off 'source_len, source_valid_range('source_off, 'source_len). (
cursor : ScratchRlpCursor('source_off, 'source_len),
index : range(2, 16),
children : BranchRefs,
) -> (
ScratchTrieNode
) =
if index < 16 then {
let child = scratch_rlp_decode_item(cursor);
let next = scratch_rlp_cursor_advance(cursor, child.source.len);
let decoded_child = scratch_field_to_ref(child);
var updated = children;
updated[index] = decoded_child;
decode_scratch_branch_node(next, index + 1, updated)
} else {
let value = scratch_rlp_decode_item(cursor);
let next = scratch_rlp_cursor_advance(cursor, value.source.len);
scratch_rlp_cursor_expect_end(next);
let content = scratch_rlp_item_content(value);
ScratchBranchNode(children, content)
}decode_input_branch_node over the scratch cursor family.
function decode_scratch_branch_node forall 'source_off 'source_len, source_valid_range('source_off, 'source_len). (
cursor : ScratchRlpCursor('source_off, 'source_len),
index : range(2, 16),
children : BranchRefs,
) -> (
ScratchTrieNode
) =
if index < 16 then {
let child = scratch_rlp_decode_item(cursor);
let next = scratch_rlp_cursor_advance(cursor, child.source.len);
let decoded_child = scratch_field_to_ref(child);
var updated = children;
updated[index] = decoded_child;
decode_scratch_branch_node(next, index + 1, updated)
} else {
let value = scratch_rlp_decode_item(cursor);
let next = scratch_rlp_cursor_advance(cursor, value.source.len);
scratch_rlp_cursor_expect_end(next);
let content = scratch_rlp_item_content(value);
ScratchBranchNode(children, content)
}input_field_to_ref for a scratch-backed child field; an inline list is copied into a self-contained inline node.
function scratch_field_to_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
ScratchRlpFieldRef('source_off, 'source_len, 'content_len)) -> (
NodeRef
) =
if f.is_list then {
let inline_node = inline_node_from_slice(f.source);
ScratchInlineRef(inline_node)
} else if f.content_len == MPT_HASH_LENGTH then {
let word = scratch_rlp_decode_word(f);
let hash = word_to_hash(word);
HashRef(hash)
} else {
EmptyRef()
}function scratch_rlp_cursor_advance(cursor, consumed) =
slice_suffix(cursor, consumed)function scratch_rlp_cursor_expect_end(cursor) = {
if cursor.len == 0 then {
return ()
};
fatal_error(RlpDecode)
}function scratch_rlp_decode_item(cursor) = {
if cursor.len == 0 then {
fatal_error(RlpDecode)
};
let (is_list, content_off, content_len_value) = scratch_rlp_ref_hdr(cursor);
let (content_len as 'content_len) = content_len_value;
if cursor.len < content_off then {
fatal_error(RlpDecode)
};
if cursor.len - content_off < content_len then {
fatal_error(RlpDecode)
};
let (full_len as 'full_len) = content_off + content_len;
if (0 < full_len) & (full_len <= cursor.len) then {
let field_source = sub_slice(cursor, 0, full_len);
let field : ScratchRlpFieldRef('source_off, 'full_len, 'content_len) = struct {
source = field_source,
is_list = is_list,
content_len = content_len,
};
field
} else {
fatal_error(RlpDecode)
}
}rlp_item_content for a scratch-backed field.
function scratch_rlp_item_content forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
ScratchRlpFieldRef('source_off, 'source_len, 'content_len)) -> (
ScratchSlice
) =
sub_slice(f.source, f.source.len - f.content_len, f.content_len)The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)A one-pass RLP cursor over a scratch-arena node encoding, nominally distinct from the stateless-input cursor.
type ScratchRlpCursor('source_off : Int, 'source_len : Int),
source_valid_range('source_off, 'source_len) =
ScratchSliceFields('source_off, 'source_len)A decoded node freshly encoded in scratch during canonical rebuilding.
union ScratchTrieNode = {
/* a two-field leaf: its path and scratch value bytes */
ScratchLeafNode : (TriePath, ScratchSlice),
/* a two-field extension: its path and single child reference */
ScratchExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
ScratchBranchNode : (BranchRefs, ScratchSlice),
}Common bound for relative source coordinates used by generic cursor operations. Nominal slices retain their region-specific invariant.
type source_valid_range('off : Int, 'len : Int) -> Bool =
0 <= 'off & 0 <= 'len & 'off + 'len <= default_host_region_boundfunction decode_input_trie_node¶
Decodes node bytes into leaf/extension/branch form by field count (2 = leaf or extension by the HP flag; 17 = branch).
function decode_input_trie_node(node : StatelessInputSlice) -> InputTrieNode = {
if node.len == 0 then {
fatal_error(RlpDecode)
};
let fields = rlp_node_cursor(node);
let first = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, first.source.len);
let second = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, second.source.len);
if fields.len == 0 then {
let (is_leaf, path) = hex_prefix_decode_ref(first);
if is_leaf then {
let value = rlp_item_content(second);
InputLeafNode(path, value)
} else {
let path_length = path_len(path);
if path_length == 0 then {
fatal_error(RlpDecode)
} else {
let child = input_field_to_ref(second);
InputExtensionNode(path, child)
}
}
} else {
let empty_child = EmptyRef();
let first_child = input_field_to_ref(first);
let second_child = input_field_to_ref(second);
var children : BranchRefs = vector_init(16, empty_child);
children[0] = first_child;
children[1] = second_child;
decode_input_branch_node(fields, 2, children)
}
}Decodes branch children 2 through 15, followed by the branch value.
function decode_input_branch_node forall 'source_off 'source_len, source_valid_range('source_off, 'source_len). (
cursor : RlpCursor('source_off, 'source_len),
index : range(2, 16),
children : BranchRefs,
) -> (
InputTrieNode
) =
if index < 16 then {
let child = rlp_decode_item(cursor);
let next = rlp_cursor_advance(cursor, child.source.len);
let decoded_child = input_field_to_ref(child);
var updated = children;
updated[index] = decoded_child;
decode_input_branch_node(next, index + 1, updated)
} else {
let value = rlp_decode_item(cursor);
let next = rlp_cursor_advance(cursor, value.source.len);
rlp_cursor_expect_end(next);
let content = rlp_item_content(value);
InputBranchNode(children, content)
}function fatal_error(_reason) = exit(())Decodes a compact path directly from its RLP source span, returning the leaf flag and the path.
function hex_prefix_decode_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
RlpFieldRef('source_off, 'source_len, 'content_len)) -> (
(bool, TriePath)
) = {
if f.is_list then {
fatal_error(RlpDecode)
};
let n = f.content_len;
if n == 0 then {
(false, path_empty())
} else {
let maximum_length = HEX_PREFIX_MAX_LENGTH;
if maximum_length < n then {
fatal_error(RlpDecode)
} else {
let content = sub_slice(f.source, f.source.len - n, n);
let fb = slice_byte(content, 0);
let flag : nibble = fb[7 .. 4];
let is_leaf : bool = flag[1] == bitone;
let odd : bool = flag[0] == bitone;
let tail_length : range(0, 32) = n - 1;
let tail = slice_suffix(content, 1);
let packed = slice_load(tail, 0);
let paired_nibbles : range(0, 64) = tail_length * 2;
if odd then {
if paired_nibbles < 64 then {
let shifted = word_shift_right(packed, 4);
var bytes = word_to_hash(shifted);
bytes[0] = append(fb[3 .. 0], bytes[0][3 .. 0]);
let path_data = B256(bytes);
let path = path_new(path_data, paired_nibbles + 1);
(is_leaf, path)
} else {
fatal_error(WitnessDeficient)
}
} else {
let path_data = word_to_hash(packed);
let path = path_new(path_data, paired_nibbles);
(is_leaf, path)
}
}
}
}The reference denoted by a child field: an inline list, a 32-byte hash, or empty.
function input_field_to_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
RlpFieldRef('source_off, 'source_len, 'content_len)) -> (
NodeRef
) =
if f.is_list then {
if f.source.len < MPT_HASH_LENGTH then {
InputInlineRef(f.source)
} else {
fatal_error(RlpDecode)
}
} else if f.content_len == MPT_HASH_LENGTH then {
let word = rlp_decode_word(f);
let hash = word_to_hash(word);
HashRef(hash)
} else {
EmptyRef()
}The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenfunction rlp_cursor_advance(cursor, consumed) =
slice_suffix(cursor, consumed)function rlp_decode_item(cursor) = {
if cursor.len == 0 then {
fatal_error(RlpDecode)
};
let (is_list, content_off, content_len_value) = rlp_ref_hdr(cursor);
let (content_len as 'content_len) = content_len_value;
if cursor.len < content_off then {
fatal_error(RlpDecode)
};
if cursor.len - content_off < content_len then {
fatal_error(RlpDecode)
};
let (full_len as 'full_len) = content_off + content_len;
if (0 < full_len) & (full_len <= cursor.len) then {
let field_source = sub_slice(cursor, 0, full_len);
let field : RlpFieldRef('source_off, 'full_len, 'content_len) = struct {
source = field_source,
is_list = is_list,
content_len = content_len,
};
field
} else {
fatal_error(RlpDecode)
}
}The content span of a field.
function rlp_item_content forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
RlpFieldRef('source_off, 'source_len, 'content_len)) -> (
StatelessInputSlice
) =
sub_slice(f.source, f.source.len - f.content_len, f.content_len)A cursor over the children of a byte sequence that must be exactly one RLP list (e.g. a trie node).
function rlp_node_cursor(node : StatelessInputSlice) -> (
{'source_off 'source_len,
source_valid_range('source_off, 'source_len).
RlpCursor('source_off, 'source_len)}
) = {
if node.len == 0 then {
fatal_error(RlpDecode)
} else {
let item = rlp_single_ref(node);
rlp_decode_list(item)
}
}val vector_init = pure {lean: "vectorInit", _: "vector_init"}: forall ('n : Int) ('a : Type), 'n >= 0.
(implicit('n), 'a) -> vector('n, 'a)The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}A decoded authenticated node. Every borrowed field remains a stateless input slice, including fields reached through an input-inline child.
union InputTrieNode = {
/* a two-field leaf: its path and value bytes */
InputLeafNode : (TriePath, StatelessInputSlice),
/* a two-field extension: its path and single child reference */
InputExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
InputBranchNode : (BranchRefs, StatelessInputSlice),
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}function decode_scratch_trie_node¶
decode_input_trie_node over freshly encoded scratch node bytes.
function decode_scratch_trie_node(node : ScratchSlice) -> ScratchTrieNode = {
let fields = scratch_rlp_node_cursor(node);
let first = scratch_rlp_decode_item(fields);
let fields = scratch_rlp_cursor_advance(fields, first.source.len);
let second = scratch_rlp_decode_item(fields);
let fields = scratch_rlp_cursor_advance(fields, second.source.len);
if fields.len == 0 then {
let (is_leaf, path) = scratch_hex_prefix_decode_ref(first);
if is_leaf then {
let value = scratch_rlp_item_content(second);
ScratchLeafNode(path, value)
} else {
let path_length = path_len(path);
if path_length == 0 then {
fatal_error(RlpDecode)
} else {
let child = scratch_field_to_ref(second);
ScratchExtensionNode(path, child)
}
}
} else {
let empty_child = EmptyRef();
let first_child = scratch_field_to_ref(first);
let second_child = scratch_field_to_ref(second);
var children : BranchRefs = vector_init(16, empty_child);
children[0] = first_child;
children[1] = second_child;
decode_scratch_branch_node(fields, 2, children)
}
}decode_input_branch_node over the scratch cursor family.
function decode_scratch_branch_node forall 'source_off 'source_len, source_valid_range('source_off, 'source_len). (
cursor : ScratchRlpCursor('source_off, 'source_len),
index : range(2, 16),
children : BranchRefs,
) -> (
ScratchTrieNode
) =
if index < 16 then {
let child = scratch_rlp_decode_item(cursor);
let next = scratch_rlp_cursor_advance(cursor, child.source.len);
let decoded_child = scratch_field_to_ref(child);
var updated = children;
updated[index] = decoded_child;
decode_scratch_branch_node(next, index + 1, updated)
} else {
let value = scratch_rlp_decode_item(cursor);
let next = scratch_rlp_cursor_advance(cursor, value.source.len);
scratch_rlp_cursor_expect_end(next);
let content = scratch_rlp_item_content(value);
ScratchBranchNode(children, content)
}function fatal_error(_reason) = exit(())The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.leninput_field_to_ref for a scratch-backed child field; an inline list is copied into a self-contained inline node.
function scratch_field_to_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
ScratchRlpFieldRef('source_off, 'source_len, 'content_len)) -> (
NodeRef
) =
if f.is_list then {
let inline_node = inline_node_from_slice(f.source);
ScratchInlineRef(inline_node)
} else if f.content_len == MPT_HASH_LENGTH then {
let word = scratch_rlp_decode_word(f);
let hash = word_to_hash(word);
HashRef(hash)
} else {
EmptyRef()
}Scratch-backed counterpart used only when canonicalization reopens an embedded node that it just encoded.
function scratch_hex_prefix_decode_ref forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
ScratchRlpFieldRef('source_off, 'source_len, 'content_len)) -> (
(bool, TriePath)
) = {
if f.is_list then {
fatal_error(RlpDecode)
};
let n = f.content_len;
if n == 0 then {
(false, path_empty())
} else {
let maximum_length = HEX_PREFIX_MAX_LENGTH;
if maximum_length < n then {
fatal_error(RlpDecode)
} else {
let content = sub_slice(f.source, f.source.len - n, n);
let fb = slice_byte(content, 0);
let flag : nibble = fb[7 .. 4];
let is_leaf : bool = flag[1] == bitone;
let odd : bool = flag[0] == bitone;
let tail_length : range(0, 32) = n - 1;
let tail = slice_suffix(content, 1);
let packed = slice_load(tail, 0);
let paired_nibbles : range(0, 64) = tail_length * 2;
if odd then {
if paired_nibbles < 64 then {
let shifted = word_shift_right(packed, 4);
var bytes = word_to_hash(shifted);
bytes[0] = append(fb[3 .. 0], bytes[0][3 .. 0]);
let path_data = B256(bytes);
let path = path_new(path_data, paired_nibbles + 1);
(is_leaf, path)
} else {
fatal_error(WitnessDeficient)
}
} else {
let path_data = word_to_hash(packed);
let path = path_new(path_data, paired_nibbles);
(is_leaf, path)
}
}
}
}function scratch_rlp_cursor_advance(cursor, consumed) =
slice_suffix(cursor, consumed)function scratch_rlp_decode_item(cursor) = {
if cursor.len == 0 then {
fatal_error(RlpDecode)
};
let (is_list, content_off, content_len_value) = scratch_rlp_ref_hdr(cursor);
let (content_len as 'content_len) = content_len_value;
if cursor.len < content_off then {
fatal_error(RlpDecode)
};
if cursor.len - content_off < content_len then {
fatal_error(RlpDecode)
};
let (full_len as 'full_len) = content_off + content_len;
if (0 < full_len) & (full_len <= cursor.len) then {
let field_source = sub_slice(cursor, 0, full_len);
let field : ScratchRlpFieldRef('source_off, 'full_len, 'content_len) = struct {
source = field_source,
is_list = is_list,
content_len = content_len,
};
field
} else {
fatal_error(RlpDecode)
}
}rlp_item_content for a scratch-backed field.
function scratch_rlp_item_content forall 'source_off 'source_len 'content_len,
rlp_field_ref_valid('source_off, 'source_len, 'content_len). (f :
ScratchRlpFieldRef('source_off, 'source_len, 'content_len)) -> (
ScratchSlice
) =
sub_slice(f.source, f.source.len - f.content_len, f.content_len)rlp_node_cursor over a freshly encoded scratch node.
function scratch_rlp_node_cursor(node : ScratchSlice) -> (
{'source_off 'source_len,
source_valid_range('source_off, 'source_len).
ScratchRlpCursor('source_off, 'source_len)}
) =
let item = scratch_rlp_single_ref(node) in
scratch_rlp_decode_list(item)val vector_init = pure {lean: "vectorInit", _: "vector_init"}: forall ('n : Int) ('a : Type), 'n >= 0.
(implicit('n), 'a) -> vector('n, 'a)The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}A decoded node freshly encoded in scratch during canonical rebuilding.
union ScratchTrieNode = {
/* a two-field leaf: its path and scratch value bytes */
ScratchLeafNode : (TriePath, ScratchSlice),
/* a two-field extension: its path and single child reference */
ScratchExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
ScratchBranchNode : (BranchRefs, ScratchSlice),
}function resolve_witness_ref¶
Resolves a reference to node bytes. Resolving a missing hash is a
deficient witness (fatal_error(WitnessDeficient)), never an empty
subtree.
function resolve_witness_ref(r : NodeRef) -> StatelessInputSlice =
match r {
EmptyRef() => EMPTY_STATELESS_INPUT_SLICE,
InputInlineRef(node) => node,
ScratchInlineRef(_) => fatal_error(WitnessDeficient),
HashRef(h) => {
let node = node_db_lookup(h);
if node.len == 0 then {
fatal_error(WitnessDeficient)
} else {
node
}
},
}function fatal_error(_reason) = exit(())The witness node bytes whose KECCAK-256 digest is h, retained as a
slice into the stateless input; empty if unwitnessed.
function node_db_lookup(h : hash) -> StatelessInputSlice = {
nodedb_lookup(h)
}let EMPTY_STATELESS_INPUT_SLICE : StatelessInputSliceFields(0, 0) = stateless_input_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,
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}function merge_ext_node¶
Re-keys a decoded child node under prefix without copying a leaf
value.
function merge_ext_node(prefix : TriePath, childnode : StatelessInputSlice) -> NodeRef = {
let prefix_length = path_len(prefix);
if prefix_length == 0 then {
node_to_ref(childnode)
} else if childnode.len == 0 then {
EmptyRef()
} else {
let decoded = decode_input_trie_node(childnode);
match decoded {
InputLeafNode(path, value) => {
let merged_path = path_concat(prefix, path);
input_leaf_child_ref(merged_path, value)
},
InputExtensionNode(path, child) => {
let merged_path = path_concat(prefix, path);
extension_child_ref(merged_path, child)
},
_ => {
let childref = node_to_ref(childnode);
extension_child_ref(prefix, childref)
},
}
}
}Decodes node bytes into leaf/extension/branch form by field count (2 = leaf or extension by the HP flag; 17 = branch).
function decode_input_trie_node(node : StatelessInputSlice) -> InputTrieNode = {
if node.len == 0 then {
fatal_error(RlpDecode)
};
let fields = rlp_node_cursor(node);
let first = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, first.source.len);
let second = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, second.source.len);
if fields.len == 0 then {
let (is_leaf, path) = hex_prefix_decode_ref(first);
if is_leaf then {
let value = rlp_item_content(second);
InputLeafNode(path, value)
} else {
let path_length = path_len(path);
if path_length == 0 then {
fatal_error(RlpDecode)
} else {
let child = input_field_to_ref(second);
InputExtensionNode(path, child)
}
}
} else {
let empty_child = EmptyRef();
let first_child = input_field_to_ref(first);
let second_child = input_field_to_ref(second);
var children : BranchRefs = vector_init(16, empty_child);
children[0] = first_child;
children[1] = second_child;
decode_input_branch_node(fields, 2, children)
}
}The child reference of an extension node.
function extension_child_ref(key : TriePath, childref : NodeRef) -> NodeRef = {
let path_length = rlp_hex_prefix_size(key, false);
let child_length = node_ref_size(childref);
let content_len = path_length + child_length;
let encoded_size = rlp_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, false);
rlp_write_node_ref(childref);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}The child reference of a leaf, keeping the value in its native representation: long nodes hash the RLP framing and value as segments; only an inline node materializes a slice.
function input_leaf_child_ref(key : TriePath, value : StatelessInputSlice) -> NodeRef = {
let path_size = rlp_hex_prefix_size(key, true);
let value_size = rlp_scratch_slice_size(value);
let content_len = rlp_scratch_length_add(path_size, value_size);
let encoded_size = rlp_scratch_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, true);
rlp_write_slice(value);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}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)
}
}The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenA decoded authenticated node. Every borrowed field remains a stateless input slice, including fields reached through an input-inline child.
union InputTrieNode = {
/* a two-field leaf: its path and value bytes */
InputLeafNode : (TriePath, StatelessInputSlice),
/* a two-field extension: its path and single child reference */
InputExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
InputBranchNode : (BranchRefs, StatelessInputSlice),
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}A 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 merge_ext_ref¶
merge_ext_node over a child reference: an inline reference carries its node bytes and re-keys canonically; a 32-byte hash reference is wrapped in an extension, which is canonical only when the referenced node is a branch.
function merge_ext_ref(prefix : TriePath, childref : NodeRef) -> NodeRef = {
let prefix_length = path_len(prefix);
if prefix_length == 0 then {
childref
} else {
match childref {
EmptyRef() => EmptyRef(),
HashRef(_) => extension_child_ref(prefix, childref),
InputInlineRef(node) => {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let merged_path = path_concat(prefix, path);
input_leaf_child_ref(merged_path, value)
},
InputExtensionNode(path, child) => {
let merged_path = path_concat(prefix, path);
extension_child_ref(merged_path, child)
},
_ => extension_child_ref(prefix, childref),
}
},
ScratchInlineRef(node) => {
let node_slice = inline_node_slice(node);
let decoded = decode_scratch_trie_node(node_slice);
match decoded {
ScratchLeafNode(path, value) => {
let merged_path = path_concat(prefix, path);
scratch_leaf_child_ref(merged_path, value)
},
ScratchExtensionNode(path, child) => {
let merged_path = path_concat(prefix, path);
extension_child_ref(merged_path, child)
},
_ => extension_child_ref(prefix, childref),
}
},
}
}
}Decodes node bytes into leaf/extension/branch form by field count (2 = leaf or extension by the HP flag; 17 = branch).
function decode_input_trie_node(node : StatelessInputSlice) -> InputTrieNode = {
if node.len == 0 then {
fatal_error(RlpDecode)
};
let fields = rlp_node_cursor(node);
let first = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, first.source.len);
let second = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, second.source.len);
if fields.len == 0 then {
let (is_leaf, path) = hex_prefix_decode_ref(first);
if is_leaf then {
let value = rlp_item_content(second);
InputLeafNode(path, value)
} else {
let path_length = path_len(path);
if path_length == 0 then {
fatal_error(RlpDecode)
} else {
let child = input_field_to_ref(second);
InputExtensionNode(path, child)
}
}
} else {
let empty_child = EmptyRef();
let first_child = input_field_to_ref(first);
let second_child = input_field_to_ref(second);
var children : BranchRefs = vector_init(16, empty_child);
children[0] = first_child;
children[1] = second_child;
decode_input_branch_node(fields, 2, children)
}
}decode_input_trie_node over freshly encoded scratch node bytes.
function decode_scratch_trie_node(node : ScratchSlice) -> ScratchTrieNode = {
let fields = scratch_rlp_node_cursor(node);
let first = scratch_rlp_decode_item(fields);
let fields = scratch_rlp_cursor_advance(fields, first.source.len);
let second = scratch_rlp_decode_item(fields);
let fields = scratch_rlp_cursor_advance(fields, second.source.len);
if fields.len == 0 then {
let (is_leaf, path) = scratch_hex_prefix_decode_ref(first);
if is_leaf then {
let value = scratch_rlp_item_content(second);
ScratchLeafNode(path, value)
} else {
let path_length = path_len(path);
if path_length == 0 then {
fatal_error(RlpDecode)
} else {
let child = scratch_field_to_ref(second);
ScratchExtensionNode(path, child)
}
}
} else {
let empty_child = EmptyRef();
let first_child = scratch_field_to_ref(first);
let second_child = scratch_field_to_ref(second);
var children : BranchRefs = vector_init(16, empty_child);
children[0] = first_child;
children[1] = second_child;
decode_scratch_branch_node(fields, 2, children)
}
}The child reference of an extension node.
function extension_child_ref(key : TriePath, childref : NodeRef) -> NodeRef = {
let path_length = rlp_hex_prefix_size(key, false);
let child_length = node_ref_size(childref);
let content_len = path_length + child_length;
let encoded_size = rlp_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, false);
rlp_write_node_ref(childref);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}Materializes an inline node in scratch memory as a byte slice.
function inline_node_slice(node : InlineNode) -> ScratchSlice = {
let start = scratch_reserve(node.len);
scratch_push_b256(node.data, node.len);
scratch_finish(start)
}The child reference of a leaf, keeping the value in its native representation: long nodes hash the RLP framing and value as segments; only an inline node materializes a slice.
function input_leaf_child_ref(key : TriePath, value : StatelessInputSlice) -> NodeRef = {
let path_size = rlp_hex_prefix_size(key, true);
let value_size = rlp_scratch_slice_size(value);
let content_len = rlp_scratch_length_add(path_size, value_size);
let encoded_size = rlp_scratch_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, true);
rlp_write_slice(value);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}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)
}
}The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.leninput_leaf_child_ref for a leaf value encoded in the scratch arena (YP Appendix D, Eq. 208).
function scratch_leaf_child_ref(key : TriePath, value : ScratchSlice) -> NodeRef = {
let path_size = rlp_hex_prefix_size(key, true);
let value_size = rlp_scratch_slice_size(value);
let content_len = rlp_scratch_length_add(path_size, value_size);
let encoded_size = rlp_scratch_list_size(content_len);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_len);
rlp_write_hex_prefix(key, true);
rlp_write_slice(value);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}A decoded authenticated node. Every borrowed field remains a stateless input slice, including fields reached through an input-inline child.
union InputTrieNode = {
/* a two-field leaf: its path and value bytes */
InputLeafNode : (TriePath, StatelessInputSlice),
/* a two-field extension: its path and single child reference */
InputExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
InputBranchNode : (BranchRefs, StatelessInputSlice),
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A decoded node freshly encoded in scratch during canonical rebuilding.
union ScratchTrieNode = {
/* a two-field leaf: its path and scratch value bytes */
ScratchLeafNode : (TriePath, ScratchSlice),
/* a two-field extension: its path and single child reference */
ScratchExtensionNode : (TriePath, NodeRef),
/* a seventeen-field branch: sixteen children and the value bytes */
ScratchBranchNode : (BranchRefs, 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 }