State trie RLP codec¶
Ethereum account and storage tries over the generic MPT core: secure-trie reads for stateless execution, and the post-state root computation (YP §4.1).
function decode_state_account¶
Decodes an account trie leaf — rlp([nonce, balance, storage_root,
code_hash]) — into an AccountInfo; empty
root/hash fields decode to their empty-sentinel digests.
function decode_state_account(value : StatelessInputSlice) -> AccountInfo = {
let fields = rlp_node_cursor(value);
let nonce = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, nonce.source.len);
let balance = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, balance.source.len);
let storage = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, storage.source.len);
let code = rlp_decode_item(fields);
let fields = rlp_cursor_advance(fields, code.source.len);
rlp_cursor_expect_end(fields);
let storage_root =
if storage.content_len == 0 then {
EMPTY_TRIE_ROOT
} else {
let storage_word = rlp_decode_word(storage);
word_to_hash(storage_word)
};
let code_hash =
if code.content_len == 0 then {
KECCAK_EMPTY
} else {
let code_word = rlp_decode_word(code);
word_to_hash(code_word)
};
struct {
nonce = rlp_decode_uint64(nonce),
balance = rlp_decode_u256(balance),
storage_root = storage_root,
code_hash = code_hash,
}
}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)
}
}Decodes a canonical unsigned integer field into a word; throws otherwise.
function rlp_decode_u256 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 canonical = rlp_item_uint_canonical(f) in
if canonical then {
rlp_decode_word(f)
} else {
fatal_error(RlpDecode)
}Decodes a canonical unsigned integer into the uint64 wire domain used by EIP-2681 account nonces and EIP-4844 excess blob gas.
function rlp_decode_uint64 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)) -> (
ssz_uint
) = {
let canonical = rlp_item_uint_canonical(f);
let invalid_encoding = not_bool(canonical);
if invalid_encoding | (RLP_UINT64_LENGTH_LIMIT < f.content_len) then {
fatal_error(RlpDecode)
};
let content_length = f.content_len;
let content = rlp_item_content(f);
/* The semantic check above establishes content_length <= 8. Modulo nine
* only exposes that already-proven bound to Sail's dependent type system. */
let width : range(0, 8) = tmod_nat(content_length, 9);
rlp_uint64_width(content, width)
}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)
}
}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)
}
}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
}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)keccak256 of the empty string: the codeHash of every codeless
account.
let KECCAK_EMPTY : hash = hash_from_bits(0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470)Account state (Yellow Paper §4.1, the account tuple σ[a]).
storage_root is the account's pre-state storage root — the witness
anchor that stateless storage reads walk on a cached-state miss. It is
set when the account is materialized from the witness and is not kept
live during execution (storage mutates in the overlay maps; the
post-state root is computed on demand). Code bytes are not held here —
the account carries only code_hash, a content address into the code
store; the code hash is itself observable state (EXTCODEHASH,
EIP-1052) and the binding a stateless witness checks code against.
struct AccountInfo = {
/* sigma[a]_n: nonce (EIP-2681 caps it at 2^64-1) */
nonce : account_nonce,
/* sigma[a]_b: balance in wei */
balance : word,
/* sigma[a]_c: keccak256(code); KECCAK_EMPTY if codeless */
code_hash : hash,
/* sigma[a]_s: PRE-STATE storage root, the witness anchor */
storage_root : 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)
}The leaf encoders¶
function encode_storage_value¶
Encodes a nonzero storage value as its minimal RLP integer leaf payload.
function encode_storage_value(value : word) -> ScratchSlice = {
let encoded_len = rlp_uint_word_size(value);
let encoder = rlp_encoder_begin(encoded_len);
rlp_write_uint_word(value);
rlp_encoder_finish(encoder)
}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
}function rlp_uint_word_size(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
if (len == 1) & (first[7] == bitzero) then {
1
} else {
1 + len
}
}function rlp_write_uint_word(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
rlp_write_string_prefix(len, first);
let word = u256(w);
scratch_push_word_be(word, len)
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}The EVM 256-bit machine word (YP §9.1). A transparent range keeps the mathematical subtype relation visible: narrower non-negative ranges can be passed as words without a model-level conversion.
type word = range(0, 2 ^ 256 - 1)function encode_state_account¶
Encodes an account trie leaf with its recomputed storage root.
function encode_state_account(info : AccountInfo, storage_root : hash) -> ScratchSlice = {
let nonce_length = rlp_uint_size(info.nonce);
let balance_length = rlp_uint_word_size(info.balance);
let storage_root_length = rlp_word_size();
let code_hash_length = rlp_word_size();
let content_len = nonce_length + balance_length + storage_root_length + code_hash_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(info.nonce);
rlp_write_uint_word(info.balance);
let storage_root_word = hash_to_word(storage_root);
rlp_write_word(storage_root_word);
let code_hash_word = hash_to_word(info.code_hash);
rlp_write_word(code_hash_word);
rlp_encoder_finish(encoder)
}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],
)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
}function rlp_list_size(content_len) = {
rlp_length_prefix_len(content_len) + content_len
}function rlp_uint_word_size(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
if (len == 1) & (first[7] == bitzero) then {
1
} else {
1 + len
}
}function rlp_word_size() -> int(33) = RLP_ENCODED_WORD_LENGTHfunction 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)
}
}function rlp_write_uint_word(w) = {
let len = rlp_minimal_word_len(w);
let first =
if len == 1 then word_low_byte(w) else 0x00;
rlp_write_string_prefix(len, first);
let word = u256(w);
scratch_push_word_be(word, len)
}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)
}Account state (Yellow Paper §4.1, the account tuple σ[a]).
storage_root is the account's pre-state storage root — the witness
anchor that stateless storage reads walk on a cached-state miss. It is
set when the account is materialized from the witness and is not kept
live during execution (storage mutates in the overlay maps; the
post-state root is computed on demand). Code bytes are not held here —
the account carries only code_hash, a content address into the code
store; the code hash is itself observable state (EXTCODEHASH,
EIP-1052) and the binding a stateless witness checks code against.
struct AccountInfo = {
/* sigma[a]_n: nonce (EIP-2681 caps it at 2^64-1) */
nonce : account_nonce,
/* sigma[a]_b: balance in wei */
balance : word,
/* sigma[a]_c: keccak256(code); KECCAK_EMPTY if codeless */
code_hash : hash,
/* sigma[a]_s: PRE-STATE storage root, the witness anchor */
storage_root : hash,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}The common digest type used by trie, code, and block hashes.
type hash = b256