The witness-native trie¶
The witness-native Ethereum Merkle-Patricia trie. trie_root merges
ordered updates into an authenticated base trie and fails closed when a
touched hash is absent from the witness. With an empty base the same
builder computes the Yellow Paper TRIE(I) directly.
The witness walker¶
witness_subtree(node, prefix, updates, cursor) returns the post-state
subtree rooted at node together with the first update after that subtree.
updates is a cursor into the one globally sorted update stream; each call
consumes its contiguous prefix range and returns the cursor to its parent.
The walker descends only along touched paths; untouched children pass through as single reference items with zero node-db work. Deletes are consumed here and only here: a delete suppresses its base leaf, and a delete with no base leaf (the walk proves absence) contributes no subtree. The recursive child combiner collapses branches as it returns. RLP fields retain their source and spans, so embedded nodes and leaf values remain witness slices.
function update_under_current_prefix¶
Whether the active update belongs to the subtree at the cursor's current prefix. This inspects only the relation already carried by the cursor; it never pulls or compares another source key.
function update_under_current_prefix(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateUnderPrefix(_) => true,
UpdateBeyondPrefix(_) => false,
UpdateSourceExhausted(_) => false,
}A consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}The active update's position relative to the subtree currently consuming it. An under-prefix state carries only the unconsumed key suffix. A beyond-prefix state carries the absolute common-prefix depth of the update just consumed and its already loaded successor, allowing recursive callers to unwind directly to their divergence point.
union TrieUpdateRelation = {
/* the active update belongs to this prefix; payload is its remaining path */
UpdateUnderPrefix : TriePath,
/* the active successor is beyond this prefix; payload is its unwind depth */
UpdateBeyondPrefix : trie_path_len,
/* the source has no active update */
UpdateSourceExhausted : unit,
}function update_child_nibble¶
The active update's next child nibble.
function update_child_nibble(updates : TrieUpdateCursor) -> nibble =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
path_nibble(path_postfix, 0)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}function fatal_error(_reason) = exit(())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]
}
}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 consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}The active update's position relative to the subtree currently consuming it. An under-prefix state carries only the unconsumed key suffix. A beyond-prefix state carries the absolute common-prefix depth of the update just consumed and its already loaded successor, allowing recursive callers to unwind directly to their divergence point.
union TrieUpdateRelation = {
/* the active update belongs to this prefix; payload is its remaining path */
UpdateUnderPrefix : TriePath,
/* the active successor is beyond this prefix; payload is its unwind depth */
UpdateBeyondPrefix : trie_path_len,
/* the source has no active update */
UpdateSourceExhausted : unit,
}A four-bit path element (YP Appendix D).
type nibble = bits(4)function update_child_ranges_remaining¶
Remaining sibling-key order after the active update's child nibble. Recursive consumption returns only a strictly later sibling range.
function update_child_ranges_remaining(updates : TrieUpdateCursor) -> range(0, 16) =
let update_pending = update_under_current_prefix(updates) in
if update_pending then {
let child_nibble = update_child_nibble(updates) in let child_index = unsigned(child_nibble) in 16 - child_index
} else {
0
}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)The active update's next child nibble.
function update_child_nibble(updates : TrieUpdateCursor) -> nibble =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
path_nibble(path_postfix, 0)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Whether the active update belongs to the subtree at the cursor's current prefix. This inspects only the relation already carried by the cursor; it never pulls or compares another source key.
function update_under_current_prefix(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateUnderPrefix(_) => true,
UpdateBeyondPrefix(_) => false,
UpdateSourceExhausted(_) => false,
}A consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}function overlay_child_ranges_remaining¶
Remaining event order while one existing path is merged with update child ranges. The next event is either that path's nibble or the active update's nibble, whichever is earlier.
function overlay_child_ranges_remaining(
updates : TrieUpdateCursor,
existing_pending : bool,
existing_nibble : nibble,
) -> (
range(1, 17)
) = {
let update_pending = update_under_current_prefix(updates);
let update_position : range(0, 16) =
if update_pending then {
let child_nibble = update_child_nibble(updates);
unsigned(child_nibble)
} else {
16
};
let existing_position : range(0, 16) =
if existing_pending then unsigned(existing_nibble) else 16;
let next_position =
if update_position < existing_position then update_position else existing_position;
17 - next_position
}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)The active update's next child nibble.
function update_child_nibble(updates : TrieUpdateCursor) -> nibble =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
path_nibble(path_postfix, 0)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Whether the active update belongs to the subtree at the cursor's current prefix. This inspects only the relation already carried by the cursor; it never pulls or compares another source key.
function update_under_current_prefix(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateUnderPrefix(_) => true,
UpdateBeyondPrefix(_) => false,
UpdateSourceExhausted(_) => false,
}A consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}A four-bit path element (YP Appendix D).
type nibble = bits(4)function updates_subtree¶
Builds the trie formed by live put updates beneath prefix, consuming
exactly that contiguous range from the ordered update stream.
function updates_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if not_bool(has_update) then {
(trie_empty_subtree(), updates)
} else if cursor == 64 then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_prefix = path_eq(update.key, prefix);
if not_bool(update_matches_prefix) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(value) => (trie_scratch_leaf(update.key, value), next),
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
var children = trie_children_empty();
var remaining = updates;
var update_pending = update_under_current_prefix(remaining);
while update_pending termination_measure(update_child_ranges_remaining(remaining)) do {
let nib = update_child_nibble(remaining);
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let (child, next) = updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, nib, child);
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
let previous_index = unsigned(nib);
if rebased_index <= previous_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
};
(trie_children_finish(prefix, children), remaining)
}
}function fatal_error(_reason) = exit(())val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))Path concatenation; over 64 nibbles is a witness fault.
function path_concat(a : TriePath, b : TriePath) -> TriePath = {
let alen = path_len(a);
let blen = path_len(b);
let combined = alen + blen;
if combined <= 64 then {
var result = a;
var index : trie_path_len = 0;
while index < blen termination_measure(blen - index) do {
let nibble = path_nibble(b, index);
result = path_append_nibble(result, nibble);
let current_index = index;
index =
if current_index < 64 then {
current_index + 1
} else {
fatal_error(WitnessDeficient)
}
};
result
} else {
fatal_error(WitnessDeficient)
}
}Path equality.
function path_eq(a : TriePath, b : TriePath) -> bool =
(a.len == b.len) & (a.data == b.data)The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenA one-nibble path.
function path_single(n : nibble) -> TriePath = {
let empty_path = path_empty();
path_append_nibble(empty_path, n)
}Adds one recursively built child at index; an empty subtree
contributes nothing.
function trie_children_add(
children : TrieChildren,
prefix : TriePath,
index : nibble,
child : TrieItem,
) -> (
TrieChildren
) =
match child.value {
EmptySubtree() => children,
_ => {
let depth = path_len(prefix);
let child_count = children.count;
let child_depth : range(1, 64) =
if depth < 64 then depth + 1 else fatal_error(WitnessDeficient);
let next_child_count : range(1, 16) =
if child_count < 16 then child_count + 1 else fatal_error(WitnessDeficient);
let child_segment = path_single(index);
let child_prefix = path_concat(prefix, child_segment);
let path_below_child = path_prefix_of(child_prefix, child.path);
let path_outside_child = not_bool(path_below_child);
if path_outside_child then {
fatal_error(WitnessDeficient)
};
let child_already_present = branch_mask_has(children.mask, index);
if child_already_present then {
fatal_error(WitnessDeficient)
};
var updated = children;
updated.mask = branch_mask_set(updated.mask, index);
updated.children[unsigned(index)] = trie_child_ref(child, child_depth);
updated.only = child;
updated.count = next_child_count;
updated
},
}Constructs an empty child accumulator. only is meaningful iff exactly
one child has been added.
function trie_children_empty() -> TrieChildren = {
let empty_ref = EmptyRef();
let children = vector_init(16, empty_ref);
let empty_subtree = trie_empty_subtree();
struct { mask = 0x0000, children = children, only = empty_subtree, count = 0 }
}Finishes a recursive branch: zero children disappear, one child bubbles upward structurally, and multiple children form a canonical branch item.
function trie_children_finish(prefix : TriePath, children : TrieChildren) -> TrieItem =
if children.count == 0 then {
struct { path = prefix, value = EmptySubtree() }
} else if children.count == 1 then {
children.only
} else {
let branch_ref = branch_child_ref(children.mask, children.children);
trie_branch(prefix, branch_ref)
}The absent subtree (YP n(I,i) = ()). Its path carries no meaning.
function trie_empty_subtree() -> TrieItem =
struct { path = path_empty(), value = EmptySubtree() }Constructs a leaf item over freshly encoded scratch bytes.
function trie_scratch_leaf(path : TriePath, value : ScratchSlice) -> TrieItem = {
let leaf_value = ScratchTrieLeaf(value);
trie_leaf(path, leaf_value)
}Moves an under-prefix active update through one selected child edge.
function trie_updates_descend(updates : TrieUpdateCursor) -> TrieUpdateCursor =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
let child_postfix = path_drop(path_postfix, 1);
var descended = updates;
descended.relation = UpdateUnderPrefix(child_postfix);
descended
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Consumes the active update and pulls its successor. Sources establish strict
ascending order and unique keys before opening the cursor. The successor
starts beyond the consumed key's full prefix, carrying the one common-prefix
depth computed for this adjacent key pair; parents rebase it to
UpdateUnderPrefix(path_postfix) when that depth reaches their subtree.
function trie_updates_pop(updates : TrieUpdateCursor) -> (TrieUpdate, TrieUpdateCursor) =
match updates.relation {
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
_ => {
let current = updates.current;
let successor = trie_update_source_next(updates.source);
let next : TrieUpdateCursor =
if successor.available then {
let common_prefix_len = common_prefix_length(current.key, successor.update.key);
struct {
source = updates.source,
current = successor.update,
relation = UpdateBeyondPrefix(common_prefix_len),
}
} else {
struct { source = updates.source, current = EMPTY_TRIE_UPDATE, relation = UpdateSourceExhausted() }
};
(current, next)
},
}Reinterprets an already-loaded successor at an ancestor prefix. No source item is fetched and no prefix scan is repeated: the adjacent-key common prefix computed by trie_updates_pop decides whether the successor has reached the subtree where traversal should resume.
function trie_updates_rebase(updates : TrieUpdateCursor, prefix : TriePath) -> TrieUpdateCursor =
let prefix_len = path_len(prefix) in
match updates.relation {
UpdateSourceExhausted(_) => updates,
UpdateUnderPrefix(_) => updates,
UpdateBeyondPrefix(common_prefix_len) => if prefix_len <= common_prefix_len then {
let path_postfix = path_drop(updates.current.key, prefix_len);
struct { source = updates.source, current = updates.current, relation = UpdateUnderPrefix(path_postfix) }
} else {
updates
},
}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)The active update's next child nibble.
function update_child_nibble(updates : TrieUpdateCursor) -> nibble =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
path_nibble(path_postfix, 0)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Remaining sibling-key order after the active update's child nibble. Recursive consumption returns only a strictly later sibling range.
function update_child_ranges_remaining(updates : TrieUpdateCursor) -> range(0, 16) =
let update_pending = update_under_current_prefix(updates) in
if update_pending then {
let child_nibble = update_child_nibble(updates) in let child_index = unsigned(child_nibble) in 16 - child_index
} else {
0
}Whether the active update belongs to the subtree at the cursor's current prefix. This inspects only the relation already carried by the cursor; it never pulls or compares another source key.
function update_under_current_prefix(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateUnderPrefix(_) => true,
UpdateBeyondPrefix(_) => false,
UpdateSourceExhausted(_) => false,
}Builds the trie formed by live put updates beneath prefix, consuming
exactly that contiguous range from the ordered update stream.
function updates_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if not_bool(has_update) then {
(trie_empty_subtree(), updates)
} else if cursor == 64 then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_prefix = path_eq(update.key, prefix);
if not_bool(update_matches_prefix) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(value) => (trie_scratch_leaf(update.key, value), next),
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
var children = trie_children_empty();
var remaining = updates;
var update_pending = update_under_current_prefix(remaining);
while update_pending termination_measure(update_child_ranges_remaining(remaining)) do {
let nib = update_child_nibble(remaining);
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let (child, next) = updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, nib, child);
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
let previous_index = unsigned(nib);
if rebased_index <= previous_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
};
(trie_children_finish(prefix, children), remaining)
}
}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 pending change at a trie key: a put of new leaf bytes, or a delete.
union TrieChange = {
/* insert or replace the leaf bytes at the key */
TriePut : ScratchSlice,
/* remove the key */
TrieDelete : unit,
}A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }A consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}The active update's position relative to the subtree currently consuming it. An under-prefix state carries only the unconsumed key suffix. A beyond-prefix state carries the absolute common-prefix depth of the update just consumed and its already loaded successor, allowing recursive callers to unwind directly to their divergence point.
union TrieUpdateRelation = {
/* the active update belongs to this prefix; payload is its remaining path */
UpdateUnderPrefix : TriePath,
/* the active successor is beyond this prefix; payload is its unwind depth */
UpdateBeyondPrefix : trie_path_len,
/* the source has no active update */
UpdateSourceExhausted : unit,
}A cursor at or immediately after a position in a trie path.
type trie_path_cursor = range(0, 64)function overlay_leaf_subtree¶
Merges one witness leaf with all ordered updates beneath its containing prefix. Virtual single-child branches along the leaf path are expanded recursively and collapse again on return.
function overlay_leaf_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
key : TriePath,
value : StatelessInputSlice,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
let key_below_prefix = path_prefix_of(prefix, key);
if prefix_len != cursor | not_bool(key_below_prefix) then {
fatal_error(WitnessDeficient)
};
if cursor == 64 then {
let key_matches_prefix = path_eq(prefix, key);
if not_bool(key_matches_prefix) then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if has_update then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_key = path_eq(update.key, key);
if not_bool(update_matches_key) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(updated) => (trie_scratch_leaf(key, updated), next),
}
} else {
(trie_input_leaf(key, value), updates)
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
let leaf_nibble = path_nibble(key, cursor);
var children = trie_children_empty();
var remaining = updates;
var leaf_pending : bool = true;
var update_pending = update_under_current_prefix(remaining);
while leaf_pending | update_pending termination_measure(
overlay_child_ranges_remaining(remaining, leaf_pending, leaf_nibble)
) do {
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let leaf_index = unsigned(leaf_nibble);
let update_index = unsigned(update_nibble);
if leaf_pending & (leaf_index < update_index) then {
let leaf = trie_input_leaf(key, value);
children = trie_children_add(children, prefix, leaf_nibble, leaf);
leaf_pending = false
} else {
let child_path = path_single(update_nibble);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let consumes_leaf = leaf_pending & (update_nibble == leaf_nibble);
let (child, next) =
if consumes_leaf
then overlay_leaf_subtree(descended, child_prefix, key, value, next_cursor)
else updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, update_nibble, child);
if consumes_leaf then {
leaf_pending = false
};
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
if rebased_index <= update_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
}
} else {
let leaf = trie_input_leaf(key, value);
children = trie_children_add(children, prefix, leaf_nibble, leaf);
leaf_pending = false
}
};
(trie_children_finish(prefix, children), remaining)
}
}function fatal_error(_reason) = exit(())val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))Remaining event order while one existing path is merged with update child ranges. The next event is either that path's nibble or the active update's nibble, whichever is earlier.
function overlay_child_ranges_remaining(
updates : TrieUpdateCursor,
existing_pending : bool,
existing_nibble : nibble,
) -> (
range(1, 17)
) = {
let update_pending = update_under_current_prefix(updates);
let update_position : range(0, 16) =
if update_pending then {
let child_nibble = update_child_nibble(updates);
unsigned(child_nibble)
} else {
16
};
let existing_position : range(0, 16) =
if existing_pending then unsigned(existing_nibble) else 16;
let next_position =
if update_position < existing_position then update_position else existing_position;
17 - next_position
}Merges one witness leaf with all ordered updates beneath its containing prefix. Virtual single-child branches along the leaf path are expanded recursively and collapse again on return.
function overlay_leaf_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
key : TriePath,
value : StatelessInputSlice,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
let key_below_prefix = path_prefix_of(prefix, key);
if prefix_len != cursor | not_bool(key_below_prefix) then {
fatal_error(WitnessDeficient)
};
if cursor == 64 then {
let key_matches_prefix = path_eq(prefix, key);
if not_bool(key_matches_prefix) then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if has_update then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_key = path_eq(update.key, key);
if not_bool(update_matches_key) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(updated) => (trie_scratch_leaf(key, updated), next),
}
} else {
(trie_input_leaf(key, value), updates)
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
let leaf_nibble = path_nibble(key, cursor);
var children = trie_children_empty();
var remaining = updates;
var leaf_pending : bool = true;
var update_pending = update_under_current_prefix(remaining);
while leaf_pending | update_pending termination_measure(
overlay_child_ranges_remaining(remaining, leaf_pending, leaf_nibble)
) do {
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let leaf_index = unsigned(leaf_nibble);
let update_index = unsigned(update_nibble);
if leaf_pending & (leaf_index < update_index) then {
let leaf = trie_input_leaf(key, value);
children = trie_children_add(children, prefix, leaf_nibble, leaf);
leaf_pending = false
} else {
let child_path = path_single(update_nibble);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let consumes_leaf = leaf_pending & (update_nibble == leaf_nibble);
let (child, next) =
if consumes_leaf
then overlay_leaf_subtree(descended, child_prefix, key, value, next_cursor)
else updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, update_nibble, child);
if consumes_leaf then {
leaf_pending = false
};
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
if rebased_index <= update_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
}
} else {
let leaf = trie_input_leaf(key, value);
children = trie_children_add(children, prefix, leaf_nibble, leaf);
leaf_pending = false
}
};
(trie_children_finish(prefix, children), remaining)
}
}Path concatenation; over 64 nibbles is a witness fault.
function path_concat(a : TriePath, b : TriePath) -> TriePath = {
let alen = path_len(a);
let blen = path_len(b);
let combined = alen + blen;
if combined <= 64 then {
var result = a;
var index : trie_path_len = 0;
while index < blen termination_measure(blen - index) do {
let nibble = path_nibble(b, index);
result = path_append_nibble(result, nibble);
let current_index = index;
index =
if current_index < 64 then {
current_index + 1
} else {
fatal_error(WitnessDeficient)
}
};
result
} else {
fatal_error(WitnessDeficient)
}
}Path equality.
function path_eq(a : TriePath, b : TriePath) -> bool =
(a.len == b.len) & (a.data == b.data)The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenThe i-th nibble, most significant first; out of range yields 0.
function path_nibble(path : TriePath, i : trie_path_cursor) -> nibble =
let length = path_len(path) in
if length <= i then {
0x0
} else {
let bytes = path.data;
let byte_index = path_byte_index(i);
let parity = tmod_int(i, 2);
if parity == 0 then {
bytes[byte_index][7 .. 4]
} else {
bytes[byte_index][3 .. 0]
}
}Whether prefix is a prefix of path.
function path_prefix_of(prefix : TriePath, path : TriePath) -> bool =
path_matches(path, 0, prefix)A one-nibble path.
function path_single(n : nibble) -> TriePath = {
let empty_path = path_empty();
path_append_nibble(empty_path, n)
}Adds one recursively built child at index; an empty subtree
contributes nothing.
function trie_children_add(
children : TrieChildren,
prefix : TriePath,
index : nibble,
child : TrieItem,
) -> (
TrieChildren
) =
match child.value {
EmptySubtree() => children,
_ => {
let depth = path_len(prefix);
let child_count = children.count;
let child_depth : range(1, 64) =
if depth < 64 then depth + 1 else fatal_error(WitnessDeficient);
let next_child_count : range(1, 16) =
if child_count < 16 then child_count + 1 else fatal_error(WitnessDeficient);
let child_segment = path_single(index);
let child_prefix = path_concat(prefix, child_segment);
let path_below_child = path_prefix_of(child_prefix, child.path);
let path_outside_child = not_bool(path_below_child);
if path_outside_child then {
fatal_error(WitnessDeficient)
};
let child_already_present = branch_mask_has(children.mask, index);
if child_already_present then {
fatal_error(WitnessDeficient)
};
var updated = children;
updated.mask = branch_mask_set(updated.mask, index);
updated.children[unsigned(index)] = trie_child_ref(child, child_depth);
updated.only = child;
updated.count = next_child_count;
updated
},
}Constructs an empty child accumulator. only is meaningful iff exactly
one child has been added.
function trie_children_empty() -> TrieChildren = {
let empty_ref = EmptyRef();
let children = vector_init(16, empty_ref);
let empty_subtree = trie_empty_subtree();
struct { mask = 0x0000, children = children, only = empty_subtree, count = 0 }
}Finishes a recursive branch: zero children disappear, one child bubbles upward structurally, and multiple children form a canonical branch item.
function trie_children_finish(prefix : TriePath, children : TrieChildren) -> TrieItem =
if children.count == 0 then {
struct { path = prefix, value = EmptySubtree() }
} else if children.count == 1 then {
children.only
} else {
let branch_ref = branch_child_ref(children.mask, children.children);
trie_branch(prefix, branch_ref)
}The absent subtree (YP n(I,i) = ()). Its path carries no meaning.
function trie_empty_subtree() -> TrieItem =
struct { path = path_empty(), value = EmptySubtree() }Constructs a leaf item over immutable input bytes.
function trie_input_leaf(path : TriePath, value : StatelessInputSlice) -> TrieItem = {
let leaf_value = InputTrieLeaf(value);
trie_leaf(path, leaf_value)
}Constructs a leaf item over freshly encoded scratch bytes.
function trie_scratch_leaf(path : TriePath, value : ScratchSlice) -> TrieItem = {
let leaf_value = ScratchTrieLeaf(value);
trie_leaf(path, leaf_value)
}Moves an under-prefix active update through one selected child edge.
function trie_updates_descend(updates : TrieUpdateCursor) -> TrieUpdateCursor =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
let child_postfix = path_drop(path_postfix, 1);
var descended = updates;
descended.relation = UpdateUnderPrefix(child_postfix);
descended
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Consumes the active update and pulls its successor. Sources establish strict
ascending order and unique keys before opening the cursor. The successor
starts beyond the consumed key's full prefix, carrying the one common-prefix
depth computed for this adjacent key pair; parents rebase it to
UpdateUnderPrefix(path_postfix) when that depth reaches their subtree.
function trie_updates_pop(updates : TrieUpdateCursor) -> (TrieUpdate, TrieUpdateCursor) =
match updates.relation {
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
_ => {
let current = updates.current;
let successor = trie_update_source_next(updates.source);
let next : TrieUpdateCursor =
if successor.available then {
let common_prefix_len = common_prefix_length(current.key, successor.update.key);
struct {
source = updates.source,
current = successor.update,
relation = UpdateBeyondPrefix(common_prefix_len),
}
} else {
struct { source = updates.source, current = EMPTY_TRIE_UPDATE, relation = UpdateSourceExhausted() }
};
(current, next)
},
}Reinterprets an already-loaded successor at an ancestor prefix. No source item is fetched and no prefix scan is repeated: the adjacent-key common prefix computed by trie_updates_pop decides whether the successor has reached the subtree where traversal should resume.
function trie_updates_rebase(updates : TrieUpdateCursor, prefix : TriePath) -> TrieUpdateCursor =
let prefix_len = path_len(prefix) in
match updates.relation {
UpdateSourceExhausted(_) => updates,
UpdateUnderPrefix(_) => updates,
UpdateBeyondPrefix(common_prefix_len) => if prefix_len <= common_prefix_len then {
let path_postfix = path_drop(updates.current.key, prefix_len);
struct { source = updates.source, current = updates.current, relation = UpdateUnderPrefix(path_postfix) }
} else {
updates
},
}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)The active update's next child nibble.
function update_child_nibble(updates : TrieUpdateCursor) -> nibble =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
path_nibble(path_postfix, 0)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Whether the active update belongs to the subtree at the cursor's current prefix. This inspects only the relation already carried by the cursor; it never pulls or compares another source key.
function update_under_current_prefix(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateUnderPrefix(_) => true,
UpdateBeyondPrefix(_) => false,
UpdateSourceExhausted(_) => false,
}Builds the trie formed by live put updates beneath prefix, consuming
exactly that contiguous range from the ordered update stream.
function updates_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if not_bool(has_update) then {
(trie_empty_subtree(), updates)
} else if cursor == 64 then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_prefix = path_eq(update.key, prefix);
if not_bool(update_matches_prefix) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(value) => (trie_scratch_leaf(update.key, value), next),
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
var children = trie_children_empty();
var remaining = updates;
var update_pending = update_under_current_prefix(remaining);
while update_pending termination_measure(update_child_ranges_remaining(remaining)) do {
let nib = update_child_nibble(remaining);
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let (child, next) = updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, nib, child);
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
let previous_index = unsigned(nib);
if rebased_index <= previous_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
};
(trie_children_finish(prefix, children), remaining)
}
}The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}A pending change at a trie key: a put of new leaf bytes, or a delete.
union TrieChange = {
/* insert or replace the leaf bytes at the key */
TriePut : ScratchSlice,
/* remove the key */
TrieDelete : unit,
}A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }A consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}The active update's position relative to the subtree currently consuming it. An under-prefix state carries only the unconsumed key suffix. A beyond-prefix state carries the absolute common-prefix depth of the update just consumed and its already loaded successor, allowing recursive callers to unwind directly to their divergence point.
union TrieUpdateRelation = {
/* the active update belongs to this prefix; payload is its remaining path */
UpdateUnderPrefix : TriePath,
/* the active successor is beyond this prefix; payload is its unwind depth */
UpdateBeyondPrefix : trie_path_len,
/* the source has no active update */
UpdateSourceExhausted : unit,
}A cursor at or immediately after a position in a trie path.
type trie_path_cursor = range(0, 64)function overlay_extension_subtree¶
Merges updates with the virtual single-child branches represented by an extension path, resolving the real child only when an update reaches it.
function overlay_extension_subtree(
childref : NodeRef,
child_prefix : TriePath,
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
let child_below_prefix = path_prefix_of(prefix, child_prefix);
if prefix_len != cursor | not_bool(child_below_prefix) then {
fatal_error(WitnessDeficient)
};
let at_child_prefix = path_eq(prefix, child_prefix);
if at_child_prefix then {
let has_update = update_under_current_prefix(updates);
if has_update then {
let child = resolve_witness_ref(childref);
witness_subtree(child, child_prefix, updates, cursor)
} else {
(trie_branch(child_prefix, childref), updates)
}
} else if cursor == 64 then {
fatal_error(WitnessDeficient)
} else {
let next_cursor : trie_path_cursor = cursor + 1;
let extension_nibble = path_nibble(child_prefix, cursor);
var children = trie_children_empty();
var remaining = updates;
var extension_pending : bool = true;
var update_pending = update_under_current_prefix(remaining);
while extension_pending | update_pending termination_measure(
overlay_child_ranges_remaining(remaining, extension_pending, extension_nibble)
) do {
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let extension_index = unsigned(extension_nibble);
let update_index = unsigned(update_nibble);
if extension_pending & (extension_index < update_index) then {
let extension = trie_branch(child_prefix, childref);
children = trie_children_add(children, prefix, extension_nibble, extension);
extension_pending = false
} else {
let next_path = path_single(update_nibble);
let next_prefix = path_concat(prefix, next_path);
let descended = trie_updates_descend(remaining);
let consumes_extension = extension_pending & (update_nibble == extension_nibble);
let (child, next) =
if consumes_extension
then overlay_extension_subtree(childref, child_prefix, descended, next_prefix, next_cursor)
else updates_subtree(descended, next_prefix, next_cursor);
children = trie_children_add(children, prefix, update_nibble, child);
if consumes_extension then {
extension_pending = false
};
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
if rebased_index <= update_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
}
} else {
let extension = trie_branch(child_prefix, childref);
children = trie_children_add(children, prefix, extension_nibble, extension);
extension_pending = false
}
};
(trie_children_finish(prefix, children), remaining)
}
}function fatal_error(_reason) = exit(())val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))Remaining event order while one existing path is merged with update child ranges. The next event is either that path's nibble or the active update's nibble, whichever is earlier.
function overlay_child_ranges_remaining(
updates : TrieUpdateCursor,
existing_pending : bool,
existing_nibble : nibble,
) -> (
range(1, 17)
) = {
let update_pending = update_under_current_prefix(updates);
let update_position : range(0, 16) =
if update_pending then {
let child_nibble = update_child_nibble(updates);
unsigned(child_nibble)
} else {
16
};
let existing_position : range(0, 16) =
if existing_pending then unsigned(existing_nibble) else 16;
let next_position =
if update_position < existing_position then update_position else existing_position;
17 - next_position
}Merges updates with the virtual single-child branches represented by an extension path, resolving the real child only when an update reaches it.
function overlay_extension_subtree(
childref : NodeRef,
child_prefix : TriePath,
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
let child_below_prefix = path_prefix_of(prefix, child_prefix);
if prefix_len != cursor | not_bool(child_below_prefix) then {
fatal_error(WitnessDeficient)
};
let at_child_prefix = path_eq(prefix, child_prefix);
if at_child_prefix then {
let has_update = update_under_current_prefix(updates);
if has_update then {
let child = resolve_witness_ref(childref);
witness_subtree(child, child_prefix, updates, cursor)
} else {
(trie_branch(child_prefix, childref), updates)
}
} else if cursor == 64 then {
fatal_error(WitnessDeficient)
} else {
let next_cursor : trie_path_cursor = cursor + 1;
let extension_nibble = path_nibble(child_prefix, cursor);
var children = trie_children_empty();
var remaining = updates;
var extension_pending : bool = true;
var update_pending = update_under_current_prefix(remaining);
while extension_pending | update_pending termination_measure(
overlay_child_ranges_remaining(remaining, extension_pending, extension_nibble)
) do {
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let extension_index = unsigned(extension_nibble);
let update_index = unsigned(update_nibble);
if extension_pending & (extension_index < update_index) then {
let extension = trie_branch(child_prefix, childref);
children = trie_children_add(children, prefix, extension_nibble, extension);
extension_pending = false
} else {
let next_path = path_single(update_nibble);
let next_prefix = path_concat(prefix, next_path);
let descended = trie_updates_descend(remaining);
let consumes_extension = extension_pending & (update_nibble == extension_nibble);
let (child, next) =
if consumes_extension
then overlay_extension_subtree(childref, child_prefix, descended, next_prefix, next_cursor)
else updates_subtree(descended, next_prefix, next_cursor);
children = trie_children_add(children, prefix, update_nibble, child);
if consumes_extension then {
extension_pending = false
};
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
if rebased_index <= update_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
}
} else {
let extension = trie_branch(child_prefix, childref);
children = trie_children_add(children, prefix, extension_nibble, extension);
extension_pending = false
}
};
(trie_children_finish(prefix, children), remaining)
}
}Path concatenation; over 64 nibbles is a witness fault.
function path_concat(a : TriePath, b : TriePath) -> TriePath = {
let alen = path_len(a);
let blen = path_len(b);
let combined = alen + blen;
if combined <= 64 then {
var result = a;
var index : trie_path_len = 0;
while index < blen termination_measure(blen - index) do {
let nibble = path_nibble(b, index);
result = path_append_nibble(result, nibble);
let current_index = index;
index =
if current_index < 64 then {
current_index + 1
} else {
fatal_error(WitnessDeficient)
}
};
result
} else {
fatal_error(WitnessDeficient)
}
}Path equality.
function path_eq(a : TriePath, b : TriePath) -> bool =
(a.len == b.len) & (a.data == b.data)The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenThe i-th nibble, most significant first; out of range yields 0.
function path_nibble(path : TriePath, i : trie_path_cursor) -> nibble =
let length = path_len(path) in
if length <= i then {
0x0
} else {
let bytes = path.data;
let byte_index = path_byte_index(i);
let parity = tmod_int(i, 2);
if parity == 0 then {
bytes[byte_index][7 .. 4]
} else {
bytes[byte_index][3 .. 0]
}
}Whether prefix is a prefix of path.
function path_prefix_of(prefix : TriePath, path : TriePath) -> bool =
path_matches(path, 0, prefix)A one-nibble path.
function path_single(n : nibble) -> TriePath = {
let empty_path = path_empty();
path_append_nibble(empty_path, n)
}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 trie_branch(path : TriePath, childref : NodeRef) -> TrieItem =
struct { path = path, value = BranchItem(childref) }Adds one recursively built child at index; an empty subtree
contributes nothing.
function trie_children_add(
children : TrieChildren,
prefix : TriePath,
index : nibble,
child : TrieItem,
) -> (
TrieChildren
) =
match child.value {
EmptySubtree() => children,
_ => {
let depth = path_len(prefix);
let child_count = children.count;
let child_depth : range(1, 64) =
if depth < 64 then depth + 1 else fatal_error(WitnessDeficient);
let next_child_count : range(1, 16) =
if child_count < 16 then child_count + 1 else fatal_error(WitnessDeficient);
let child_segment = path_single(index);
let child_prefix = path_concat(prefix, child_segment);
let path_below_child = path_prefix_of(child_prefix, child.path);
let path_outside_child = not_bool(path_below_child);
if path_outside_child then {
fatal_error(WitnessDeficient)
};
let child_already_present = branch_mask_has(children.mask, index);
if child_already_present then {
fatal_error(WitnessDeficient)
};
var updated = children;
updated.mask = branch_mask_set(updated.mask, index);
updated.children[unsigned(index)] = trie_child_ref(child, child_depth);
updated.only = child;
updated.count = next_child_count;
updated
},
}Constructs an empty child accumulator. only is meaningful iff exactly
one child has been added.
function trie_children_empty() -> TrieChildren = {
let empty_ref = EmptyRef();
let children = vector_init(16, empty_ref);
let empty_subtree = trie_empty_subtree();
struct { mask = 0x0000, children = children, only = empty_subtree, count = 0 }
}Finishes a recursive branch: zero children disappear, one child bubbles upward structurally, and multiple children form a canonical branch item.
function trie_children_finish(prefix : TriePath, children : TrieChildren) -> TrieItem =
if children.count == 0 then {
struct { path = prefix, value = EmptySubtree() }
} else if children.count == 1 then {
children.only
} else {
let branch_ref = branch_child_ref(children.mask, children.children);
trie_branch(prefix, branch_ref)
}Moves an under-prefix active update through one selected child edge.
function trie_updates_descend(updates : TrieUpdateCursor) -> TrieUpdateCursor =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
let child_postfix = path_drop(path_postfix, 1);
var descended = updates;
descended.relation = UpdateUnderPrefix(child_postfix);
descended
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Reinterprets an already-loaded successor at an ancestor prefix. No source item is fetched and no prefix scan is repeated: the adjacent-key common prefix computed by trie_updates_pop decides whether the successor has reached the subtree where traversal should resume.
function trie_updates_rebase(updates : TrieUpdateCursor, prefix : TriePath) -> TrieUpdateCursor =
let prefix_len = path_len(prefix) in
match updates.relation {
UpdateSourceExhausted(_) => updates,
UpdateUnderPrefix(_) => updates,
UpdateBeyondPrefix(common_prefix_len) => if prefix_len <= common_prefix_len then {
let path_postfix = path_drop(updates.current.key, prefix_len);
struct { source = updates.source, current = updates.current, relation = UpdateUnderPrefix(path_postfix) }
} else {
updates
},
}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)The active update's next child nibble.
function update_child_nibble(updates : TrieUpdateCursor) -> nibble =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
path_nibble(path_postfix, 0)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Whether the active update belongs to the subtree at the cursor's current prefix. This inspects only the relation already carried by the cursor; it never pulls or compares another source key.
function update_under_current_prefix(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateUnderPrefix(_) => true,
UpdateBeyondPrefix(_) => false,
UpdateSourceExhausted(_) => false,
}Builds the trie formed by live put updates beneath prefix, consuming
exactly that contiguous range from the ordered update stream.
function updates_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if not_bool(has_update) then {
(trie_empty_subtree(), updates)
} else if cursor == 64 then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_prefix = path_eq(update.key, prefix);
if not_bool(update_matches_prefix) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(value) => (trie_scratch_leaf(update.key, value), next),
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
var children = trie_children_empty();
var remaining = updates;
var update_pending = update_under_current_prefix(remaining);
while update_pending termination_measure(update_child_ranges_remaining(remaining)) do {
let nib = update_child_nibble(remaining);
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let (child, next) = updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, nib, child);
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
let previous_index = unsigned(nib);
if rebased_index <= previous_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
};
(trie_children_finish(prefix, children), remaining)
}
}Walks a touched witness subtree and recursively returns its canonical post-update structural item.
function witness_subtree(node, prefix, updates, cursor) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
if node.len == 0 then {
updates_subtree(updates, prefix, cursor)
} else {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let key = path_concat(prefix, path);
overlay_leaf_subtree(updates, prefix, key, value, cursor)
},
InputExtensionNode(path, childref) => {
let extension_len = path_len(path);
let next_cursor = cursor + extension_len;
if extension_len == 0 | 64 < next_cursor then {
fatal_error(WitnessDeficient)
} else {
let child_prefix = path_concat(prefix, path);
overlay_extension_subtree(childref, child_prefix, updates, prefix, cursor)
}
},
InputBranchNode(children, value) => {
if value.len != 0 | 64 <= cursor then {
fatal_error(WitnessDeficient)
} else {
let next_cursor = cursor + 1;
var built = trie_children_empty();
var remaining = updates;
var nib : nibble = 0x0;
foreach (i from 0 to 15) {
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let childref = children[i];
var present : bool = false;
match childref {
EmptyRef() => (),
_ => present = true,
};
let update_pending = update_under_current_prefix(remaining);
let update_here =
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let update_index = unsigned(update_nibble);
let child_index = unsigned(nib);
if update_index < child_index then {
fatal_error(WitnessDeficient)
};
update_nibble == nib
} else {
false
};
let (child, next_updates) =
if update_here then {
let descended = trie_updates_descend(remaining);
if present then {
let child = resolve_witness_ref(childref);
witness_subtree(child, child_prefix, descended, next_cursor)
} else {
updates_subtree(descended, child_prefix, next_cursor)
}
} else if present
then (trie_subtree(child_prefix, childref), remaining)
else (trie_empty_subtree(), remaining);
built = trie_children_add(built, prefix, nib, child);
remaining =
if update_here then {
trie_updates_rebase(next_updates, prefix)
} else {
next_updates
};
nib = add_bits(nib, 0x1)
};
let update_pending = update_under_current_prefix(remaining);
if update_pending then {
fatal_error(WitnessDeficient)
};
(trie_children_finish(prefix, built), remaining)
}
},
}
}
}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 sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }A consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}A cursor at or immediately after a position in a trie path.
type trie_path_cursor = range(0, 64)function witness_subtree¶
Walks a touched witness subtree and recursively returns its canonical post-update structural item.
function witness_subtree(node, prefix, updates, cursor) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
if node.len == 0 then {
updates_subtree(updates, prefix, cursor)
} else {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let key = path_concat(prefix, path);
overlay_leaf_subtree(updates, prefix, key, value, cursor)
},
InputExtensionNode(path, childref) => {
let extension_len = path_len(path);
let next_cursor = cursor + extension_len;
if extension_len == 0 | 64 < next_cursor then {
fatal_error(WitnessDeficient)
} else {
let child_prefix = path_concat(prefix, path);
overlay_extension_subtree(childref, child_prefix, updates, prefix, cursor)
}
},
InputBranchNode(children, value) => {
if value.len != 0 | 64 <= cursor then {
fatal_error(WitnessDeficient)
} else {
let next_cursor = cursor + 1;
var built = trie_children_empty();
var remaining = updates;
var nib : nibble = 0x0;
foreach (i from 0 to 15) {
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let childref = children[i];
var present : bool = false;
match childref {
EmptyRef() => (),
_ => present = true,
};
let update_pending = update_under_current_prefix(remaining);
let update_here =
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let update_index = unsigned(update_nibble);
let child_index = unsigned(nib);
if update_index < child_index then {
fatal_error(WitnessDeficient)
};
update_nibble == nib
} else {
false
};
let (child, next_updates) =
if update_here then {
let descended = trie_updates_descend(remaining);
if present then {
let child = resolve_witness_ref(childref);
witness_subtree(child, child_prefix, descended, next_cursor)
} else {
updates_subtree(descended, child_prefix, next_cursor)
}
} else if present
then (trie_subtree(child_prefix, childref), remaining)
else (trie_empty_subtree(), remaining);
built = trie_children_add(built, prefix, nib, child);
remaining =
if update_here then {
trie_updates_rebase(next_updates, prefix)
} else {
next_updates
};
nib = add_bits(nib, 0x1)
};
let update_pending = update_under_current_prefix(remaining);
if update_pending then {
fatal_error(WitnessDeficient)
};
(trie_children_finish(prefix, built), remaining)
}
},
}
}
}val add_bits = pure {ocaml: "add_vec", interpreter: "add_vec", lem: "add_vec", coq: "add_vec", lean: "_lean_add", _: "add_bits"}: forall ('n : Int).
(bits('n), bits('n)) -> bits('n)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)
}
}function fatal_error(_reason) = exit(())Merges updates with the virtual single-child branches represented by an extension path, resolving the real child only when an update reaches it.
function overlay_extension_subtree(
childref : NodeRef,
child_prefix : TriePath,
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
let child_below_prefix = path_prefix_of(prefix, child_prefix);
if prefix_len != cursor | not_bool(child_below_prefix) then {
fatal_error(WitnessDeficient)
};
let at_child_prefix = path_eq(prefix, child_prefix);
if at_child_prefix then {
let has_update = update_under_current_prefix(updates);
if has_update then {
let child = resolve_witness_ref(childref);
witness_subtree(child, child_prefix, updates, cursor)
} else {
(trie_branch(child_prefix, childref), updates)
}
} else if cursor == 64 then {
fatal_error(WitnessDeficient)
} else {
let next_cursor : trie_path_cursor = cursor + 1;
let extension_nibble = path_nibble(child_prefix, cursor);
var children = trie_children_empty();
var remaining = updates;
var extension_pending : bool = true;
var update_pending = update_under_current_prefix(remaining);
while extension_pending | update_pending termination_measure(
overlay_child_ranges_remaining(remaining, extension_pending, extension_nibble)
) do {
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let extension_index = unsigned(extension_nibble);
let update_index = unsigned(update_nibble);
if extension_pending & (extension_index < update_index) then {
let extension = trie_branch(child_prefix, childref);
children = trie_children_add(children, prefix, extension_nibble, extension);
extension_pending = false
} else {
let next_path = path_single(update_nibble);
let next_prefix = path_concat(prefix, next_path);
let descended = trie_updates_descend(remaining);
let consumes_extension = extension_pending & (update_nibble == extension_nibble);
let (child, next) =
if consumes_extension
then overlay_extension_subtree(childref, child_prefix, descended, next_prefix, next_cursor)
else updates_subtree(descended, next_prefix, next_cursor);
children = trie_children_add(children, prefix, update_nibble, child);
if consumes_extension then {
extension_pending = false
};
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
if rebased_index <= update_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
}
} else {
let extension = trie_branch(child_prefix, childref);
children = trie_children_add(children, prefix, extension_nibble, extension);
extension_pending = false
}
};
(trie_children_finish(prefix, children), remaining)
}
}Merges one witness leaf with all ordered updates beneath its containing prefix. Virtual single-child branches along the leaf path are expanded recursively and collapse again on return.
function overlay_leaf_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
key : TriePath,
value : StatelessInputSlice,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
let key_below_prefix = path_prefix_of(prefix, key);
if prefix_len != cursor | not_bool(key_below_prefix) then {
fatal_error(WitnessDeficient)
};
if cursor == 64 then {
let key_matches_prefix = path_eq(prefix, key);
if not_bool(key_matches_prefix) then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if has_update then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_key = path_eq(update.key, key);
if not_bool(update_matches_key) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(updated) => (trie_scratch_leaf(key, updated), next),
}
} else {
(trie_input_leaf(key, value), updates)
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
let leaf_nibble = path_nibble(key, cursor);
var children = trie_children_empty();
var remaining = updates;
var leaf_pending : bool = true;
var update_pending = update_under_current_prefix(remaining);
while leaf_pending | update_pending termination_measure(
overlay_child_ranges_remaining(remaining, leaf_pending, leaf_nibble)
) do {
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let leaf_index = unsigned(leaf_nibble);
let update_index = unsigned(update_nibble);
if leaf_pending & (leaf_index < update_index) then {
let leaf = trie_input_leaf(key, value);
children = trie_children_add(children, prefix, leaf_nibble, leaf);
leaf_pending = false
} else {
let child_path = path_single(update_nibble);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let consumes_leaf = leaf_pending & (update_nibble == leaf_nibble);
let (child, next) =
if consumes_leaf
then overlay_leaf_subtree(descended, child_prefix, key, value, next_cursor)
else updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, update_nibble, child);
if consumes_leaf then {
leaf_pending = false
};
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
if rebased_index <= update_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
}
} else {
let leaf = trie_input_leaf(key, value);
children = trie_children_add(children, prefix, leaf_nibble, leaf);
leaf_pending = false
}
};
(trie_children_finish(prefix, children), remaining)
}
}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 one-nibble path.
function path_single(n : nibble) -> TriePath = {
let empty_path = path_empty();
path_append_nibble(empty_path, n)
}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
}
},
}Adds one recursively built child at index; an empty subtree
contributes nothing.
function trie_children_add(
children : TrieChildren,
prefix : TriePath,
index : nibble,
child : TrieItem,
) -> (
TrieChildren
) =
match child.value {
EmptySubtree() => children,
_ => {
let depth = path_len(prefix);
let child_count = children.count;
let child_depth : range(1, 64) =
if depth < 64 then depth + 1 else fatal_error(WitnessDeficient);
let next_child_count : range(1, 16) =
if child_count < 16 then child_count + 1 else fatal_error(WitnessDeficient);
let child_segment = path_single(index);
let child_prefix = path_concat(prefix, child_segment);
let path_below_child = path_prefix_of(child_prefix, child.path);
let path_outside_child = not_bool(path_below_child);
if path_outside_child then {
fatal_error(WitnessDeficient)
};
let child_already_present = branch_mask_has(children.mask, index);
if child_already_present then {
fatal_error(WitnessDeficient)
};
var updated = children;
updated.mask = branch_mask_set(updated.mask, index);
updated.children[unsigned(index)] = trie_child_ref(child, child_depth);
updated.only = child;
updated.count = next_child_count;
updated
},
}Constructs an empty child accumulator. only is meaningful iff exactly
one child has been added.
function trie_children_empty() -> TrieChildren = {
let empty_ref = EmptyRef();
let children = vector_init(16, empty_ref);
let empty_subtree = trie_empty_subtree();
struct { mask = 0x0000, children = children, only = empty_subtree, count = 0 }
}Finishes a recursive branch: zero children disappear, one child bubbles upward structurally, and multiple children form a canonical branch item.
function trie_children_finish(prefix : TriePath, children : TrieChildren) -> TrieItem =
if children.count == 0 then {
struct { path = prefix, value = EmptySubtree() }
} else if children.count == 1 then {
children.only
} else {
let branch_ref = branch_child_ref(children.mask, children.children);
trie_branch(prefix, branch_ref)
}The absent subtree (YP n(I,i) = ()). Its path carries no meaning.
function trie_empty_subtree() -> TrieItem =
struct { path = path_empty(), value = EmptySubtree() }function trie_subtree(path : TriePath, childref : NodeRef) -> TrieItem =
struct { path = path, value = SubtreeItem(childref) }Moves an under-prefix active update through one selected child edge.
function trie_updates_descend(updates : TrieUpdateCursor) -> TrieUpdateCursor =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
let child_postfix = path_drop(path_postfix, 1);
var descended = updates;
descended.relation = UpdateUnderPrefix(child_postfix);
descended
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Reinterprets an already-loaded successor at an ancestor prefix. No source item is fetched and no prefix scan is repeated: the adjacent-key common prefix computed by trie_updates_pop decides whether the successor has reached the subtree where traversal should resume.
function trie_updates_rebase(updates : TrieUpdateCursor, prefix : TriePath) -> TrieUpdateCursor =
let prefix_len = path_len(prefix) in
match updates.relation {
UpdateSourceExhausted(_) => updates,
UpdateUnderPrefix(_) => updates,
UpdateBeyondPrefix(common_prefix_len) => if prefix_len <= common_prefix_len then {
let path_postfix = path_drop(updates.current.key, prefix_len);
struct { source = updates.source, current = updates.current, relation = UpdateUnderPrefix(path_postfix) }
} else {
updates
},
}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)The active update's next child nibble.
function update_child_nibble(updates : TrieUpdateCursor) -> nibble =
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len == 0 then {
fatal_error(WitnessDeficient)
} else {
path_nibble(path_postfix, 0)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
}Whether the active update belongs to the subtree at the cursor's current prefix. This inspects only the relation already carried by the cursor; it never pulls or compares another source key.
function update_under_current_prefix(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateUnderPrefix(_) => true,
UpdateBeyondPrefix(_) => false,
UpdateSourceExhausted(_) => false,
}Builds the trie formed by live put updates beneath prefix, consuming
exactly that contiguous range from the ordered update stream.
function updates_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if not_bool(has_update) then {
(trie_empty_subtree(), updates)
} else if cursor == 64 then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_prefix = path_eq(update.key, prefix);
if not_bool(update_matches_prefix) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(value) => (trie_scratch_leaf(update.key, value), next),
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
var children = trie_children_empty();
var remaining = updates;
var update_pending = update_under_current_prefix(remaining);
while update_pending termination_measure(update_child_ranges_remaining(remaining)) do {
let nib = update_child_nibble(remaining);
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let (child, next) = updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, nib, child);
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
let previous_index = unsigned(nib);
if rebased_index <= previous_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
};
(trie_children_finish(prefix, children), remaining)
}
}Walks a touched witness subtree and recursively returns its canonical post-update structural item.
function witness_subtree(node, prefix, updates, cursor) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
if node.len == 0 then {
updates_subtree(updates, prefix, cursor)
} else {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let key = path_concat(prefix, path);
overlay_leaf_subtree(updates, prefix, key, value, cursor)
},
InputExtensionNode(path, childref) => {
let extension_len = path_len(path);
let next_cursor = cursor + extension_len;
if extension_len == 0 | 64 < next_cursor then {
fatal_error(WitnessDeficient)
} else {
let child_prefix = path_concat(prefix, path);
overlay_extension_subtree(childref, child_prefix, updates, prefix, cursor)
}
},
InputBranchNode(children, value) => {
if value.len != 0 | 64 <= cursor then {
fatal_error(WitnessDeficient)
} else {
let next_cursor = cursor + 1;
var built = trie_children_empty();
var remaining = updates;
var nib : nibble = 0x0;
foreach (i from 0 to 15) {
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let childref = children[i];
var present : bool = false;
match childref {
EmptyRef() => (),
_ => present = true,
};
let update_pending = update_under_current_prefix(remaining);
let update_here =
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let update_index = unsigned(update_nibble);
let child_index = unsigned(nib);
if update_index < child_index then {
fatal_error(WitnessDeficient)
};
update_nibble == nib
} else {
false
};
let (child, next_updates) =
if update_here then {
let descended = trie_updates_descend(remaining);
if present then {
let child = resolve_witness_ref(childref);
witness_subtree(child, child_prefix, descended, next_cursor)
} else {
updates_subtree(descended, child_prefix, next_cursor)
}
} else if present
then (trie_subtree(child_prefix, childref), remaining)
else (trie_empty_subtree(), remaining);
built = trie_children_add(built, prefix, nib, child);
remaining =
if update_here then {
trie_updates_rebase(next_updates, prefix)
} else {
next_updates
};
nib = add_bits(nib, 0x1)
};
let update_pending = update_under_current_prefix(remaining);
if update_pending then {
fatal_error(WitnessDeficient)
};
(trie_children_finish(prefix, built), remaining)
}
},
}
}
}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 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)The root computation¶
type TrieRootResult¶
The root of the trie anchored at base_root after applying the
ordered update stream. This is the only public root computation:
witness-native and fail-closed — the walker resolves every touched
hash reference in the witness node-db and any missing node calls
fatal_error(WitnessDeficient); otherwise the builder recomposes the
emitted stream canonically.
Theorem-shaped remark: restricted to an empty base
(base_root = EMPTY_TRIE_ROOT), the walker is the identity on the
live update leaves and trie_root computes TRIE(I) of Appendix D
directly — an empty base contains no hash references, so the node-db
is never consulted and no failure path can fire. The native
(full-state) backend exercises exactly this restriction: same
implementation, different input.
struct TrieRootResult = { root : hash, changed : bool }The common digest type used by trie, code, and block hashes.
type hash = b256function trie_root_cursor¶
Applies an already-open update cursor. The changed result records
whether the source contained at least one update.
function trie_root_cursor(base_root : hash, updates : TrieUpdateCursor) -> TrieRootResult = {
let no_updates = updates_empty(updates);
if no_updates then {
struct { root = base_root, changed = false }
} else {
let empty_prefix = path_empty();
let (subtree, remaining) =
if base_root == EMPTY_TRIE_ROOT then {
updates_subtree(updates, empty_prefix, 0)
} else {
let node = node_db_lookup(base_root);
if node.len == 0 then {
fatal_error(WitnessDeficient)
} else {
witness_subtree(node, empty_prefix, updates, 0)
}
};
let all_updates_consumed = updates_empty(remaining);
if all_updates_consumed then {
struct { root = trie_subtree_root(subtree), changed = true }
} else {
fatal_error(WitnessDeficient)
}
}
}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)
}The empty path.
function path_empty() -> TriePath =
path_new(ZERO_HASH, 0)Converts a recursively assembled top-level item to the committed root.
function trie_subtree_root(subtree : TrieItem) -> hash =
match subtree.value {
EmptySubtree() => EMPTY_TRIE_ROOT,
_ => {
let root_ref = trie_child_ref(subtree, 0);
trie_ref_to_root(root_ref)
},
}Whether the pull cursor has reached the end of its source.
function updates_empty(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateSourceExhausted(_) => true,
_ => false,
}Builds the trie formed by live put updates beneath prefix, consuming
exactly that contiguous range from the ordered update stream.
function updates_subtree(
updates : TrieUpdateCursor,
prefix : TriePath,
cursor : trie_path_cursor,
) -> (
(TrieItem, TrieUpdateCursor)
) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
let has_update = update_under_current_prefix(updates);
if not_bool(has_update) then {
(trie_empty_subtree(), updates)
} else if cursor == 64 then {
match updates.relation {
UpdateUnderPrefix(path_postfix) => {
let postfix_len = path_len(path_postfix);
if postfix_len != 0 then {
fatal_error(WitnessDeficient)
}
},
UpdateBeyondPrefix(_) => fatal_error(WitnessDeficient),
UpdateSourceExhausted(_) => fatal_error(WitnessDeficient),
};
let (update, next) = trie_updates_pop(updates);
let update_matches_prefix = path_eq(update.key, prefix);
if not_bool(update_matches_prefix) then {
fatal_error(WitnessDeficient)
};
match update.change {
TrieDelete() => (trie_empty_subtree(), next),
TriePut(value) => (trie_scratch_leaf(update.key, value), next),
}
} else {
let next_cursor : trie_path_cursor = cursor + 1;
var children = trie_children_empty();
var remaining = updates;
var update_pending = update_under_current_prefix(remaining);
while update_pending termination_measure(update_child_ranges_remaining(remaining)) do {
let nib = update_child_nibble(remaining);
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let descended = trie_updates_descend(remaining);
let (child, next) = updates_subtree(descended, child_prefix, next_cursor);
children = trie_children_add(children, prefix, nib, child);
let rebased = trie_updates_rebase(next, prefix);
let rebased_update_pending = update_under_current_prefix(rebased);
if rebased_update_pending then {
let rebased_nibble = update_child_nibble(rebased);
let rebased_index = unsigned(rebased_nibble);
let previous_index = unsigned(nib);
if rebased_index <= previous_index then {
fatal_error(WitnessDeficient)
}
};
remaining = rebased;
update_pending = rebased_update_pending
};
(trie_children_finish(prefix, children), remaining)
}
}Walks a touched witness subtree and recursively returns its canonical post-update structural item.
function witness_subtree(node, prefix, updates, cursor) = {
let prefix_len = path_len(prefix);
if prefix_len != cursor then {
fatal_error(WitnessDeficient)
};
if node.len == 0 then {
updates_subtree(updates, prefix, cursor)
} else {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let key = path_concat(prefix, path);
overlay_leaf_subtree(updates, prefix, key, value, cursor)
},
InputExtensionNode(path, childref) => {
let extension_len = path_len(path);
let next_cursor = cursor + extension_len;
if extension_len == 0 | 64 < next_cursor then {
fatal_error(WitnessDeficient)
} else {
let child_prefix = path_concat(prefix, path);
overlay_extension_subtree(childref, child_prefix, updates, prefix, cursor)
}
},
InputBranchNode(children, value) => {
if value.len != 0 | 64 <= cursor then {
fatal_error(WitnessDeficient)
} else {
let next_cursor = cursor + 1;
var built = trie_children_empty();
var remaining = updates;
var nib : nibble = 0x0;
foreach (i from 0 to 15) {
let child_path = path_single(nib);
let child_prefix = path_concat(prefix, child_path);
let childref = children[i];
var present : bool = false;
match childref {
EmptyRef() => (),
_ => present = true,
};
let update_pending = update_under_current_prefix(remaining);
let update_here =
if update_pending then {
let update_nibble = update_child_nibble(remaining);
let update_index = unsigned(update_nibble);
let child_index = unsigned(nib);
if update_index < child_index then {
fatal_error(WitnessDeficient)
};
update_nibble == nib
} else {
false
};
let (child, next_updates) =
if update_here then {
let descended = trie_updates_descend(remaining);
if present then {
let child = resolve_witness_ref(childref);
witness_subtree(child, child_prefix, descended, next_cursor)
} else {
updates_subtree(descended, child_prefix, next_cursor)
}
} else if present
then (trie_subtree(child_prefix, childref), remaining)
else (trie_empty_subtree(), remaining);
built = trie_children_add(built, prefix, nib, child);
remaining =
if update_here then {
trie_updates_rebase(next_updates, prefix)
} else {
next_updates
};
nib = add_bits(nib, 0x1)
};
let update_pending = update_under_current_prefix(remaining);
if update_pending then {
fatal_error(WitnessDeficient)
};
(trie_children_finish(prefix, built), remaining)
}
},
}
}
}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)The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}The root of the trie anchored at base_root after applying the
ordered update stream. This is the only public root computation:
witness-native and fail-closed — the walker resolves every touched
hash reference in the witness node-db and any missing node calls
fatal_error(WitnessDeficient); otherwise the builder recomposes the
emitted stream canonically.
Theorem-shaped remark: restricted to an empty base
(base_root = EMPTY_TRIE_ROOT), the walker is the identity on the
live update leaves and trie_root computes TRIE(I) of Appendix D
directly — an empty base contains no hash references, so the node-db
is never consulted and no failure path can fire. The native
(full-state) backend exercises exactly this restriction: same
implementation, different input.
struct TrieRootResult = { root : hash, changed : bool }A consuming cursor over an ordered update source. current is the active
item, not a lookahead: it remains owned by the cursor until
trie_updates_pop consumes it and loads its successor exactly once.
struct TrieUpdateCursor = {
source : TrieUpdateSource,
current : TrieUpdate,
relation : TrieUpdateRelation,
}The common digest type used by trie, code, and block hashes.
type hash = b256function trie_root¶
The root of the trie after pulling and applying the source's ordered updates. The source's host iterator must be opened by its owner first.
function trie_root(base_root : hash, source : TrieUpdateSource) -> TrieRootResult =
let updates = trie_updates_begin(source) in
trie_root_cursor(base_root, updates)Applies an already-open update cursor. The changed result records
whether the source contained at least one update.
function trie_root_cursor(base_root : hash, updates : TrieUpdateCursor) -> TrieRootResult = {
let no_updates = updates_empty(updates);
if no_updates then {
struct { root = base_root, changed = false }
} else {
let empty_prefix = path_empty();
let (subtree, remaining) =
if base_root == EMPTY_TRIE_ROOT then {
updates_subtree(updates, empty_prefix, 0)
} else {
let node = node_db_lookup(base_root);
if node.len == 0 then {
fatal_error(WitnessDeficient)
} else {
witness_subtree(node, empty_prefix, updates, 0)
}
};
let all_updates_consumed = updates_empty(remaining);
if all_updates_consumed then {
struct { root = trie_subtree_root(subtree), changed = true }
} else {
fatal_error(WitnessDeficient)
}
}
}Opens a pull cursor by fetching only its first update.
function trie_updates_begin(source : TrieUpdateSource) -> TrieUpdateCursor = {
let first = trie_update_source_next(source);
let relation : TrieUpdateRelation =
if first.available then UpdateUnderPrefix(first.update.key) else UpdateSourceExhausted();
struct { source = source, current = first.update, relation = relation }
}The root of the trie anchored at base_root after applying the
ordered update stream. This is the only public root computation:
witness-native and fail-closed — the walker resolves every touched
hash reference in the witness node-db and any missing node calls
fatal_error(WitnessDeficient); otherwise the builder recomposes the
emitted stream canonically.
Theorem-shaped remark: restricted to an empty base
(base_root = EMPTY_TRIE_ROOT), the walker is the identity on the
live update leaves and trie_root computes TRIE(I) of Appendix D
directly — an empty base contains no hash references, so the node-db
is never consulted and no failure path can fire. The native
(full-state) backend exercises exactly this restriction: same
implementation, different input.
struct TrieRootResult = { root : hash, changed : bool }The closed pull-source algebra for ordered trie updates. Each variant owns an independently opened host iterator and trie_update_source_next is its sole interpreter, allowing one cursor and rebuild algorithm without function-valued callbacks.
union TrieUpdateSource = {
/* changed storage rows for one account */
StorageTrieUpdates : address,
/* block-final account update candidates, net-filtered in Sail */
ChangedAccountTrieUpdates : unit,
}The common digest type used by trie, code, and block hashes.
type hash = b256function trie_walk¶
Walks the trie toward key from pos, returning the leaf value
without copying it; absent paths yield empty bytes.
function trie_walk(node : StatelessInputSlice, key : TriePath, pos : trie_path_cursor) -> StatelessInputSlice = {
if node.len == 0 then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let matches = path_matches(key, pos, path);
if not_bool(matches) then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let path_length = path_len(path);
let key_length = path_len(key);
if pos + path_length == key_length then {
value
} else {
EMPTY_STATELESS_INPUT_SLICE
}
}
},
InputExtensionNode(path, childref) => {
let extension_len : trie_path_len = path_len(path);
if extension_len == 0 then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let matches = path_matches(key, pos, path);
if not_bool(matches) then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let next_pos = pos + extension_len;
if next_pos <= 64 then {
let child = resolve_witness_ref(childref);
trie_walk(child, key, next_pos)
} else {
EMPTY_STATELESS_INPUT_SLICE
}
}
}
},
InputBranchNode(children, value) => {
let key_length = path_len(key);
if pos == key_length then {
value
} else if pos < key_length then {
let child_nibble = path_nibble(key, pos);
let childref = branch_refs_get(children, child_nibble);
let child = resolve_witness_ref(childref);
trie_walk(child, key, pos + 1)
} else {
EMPTY_STATELESS_INPUT_SLICE
}
},
}
}
}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],
}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)
}
}val not_bool = pure {coq: "negb", lean: "_lean_not", _: "not"}: forall ('p : Bool). bool('p) -> bool(not('p))The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenWhether seg occurs in key at nibble position pos.
function path_matches(key : TriePath, pos : trie_path_cursor, seg : TriePath) -> bool = {
let segment_len = path_len(seg);
let key_len = path_len(key);
let stop = pos + segment_len;
if key_len < stop then {
false
} else {
var ok : bool = true;
var offset : trie_path_len = 0;
while ok & offset < segment_len termination_measure(segment_len - offset) do {
let key_index = pos + offset;
if key_index <= 64 then {
let key_nibble = path_nibble(key, key_index);
let segment_nibble = path_nibble(seg, offset);
if key_nibble != segment_nibble then {
ok = false
}
} else {
ok = false
};
let current_offset = offset;
offset =
if current_offset < 64 then {
current_offset + 1
} else {
fatal_error(WitnessDeficient)
}
};
ok
}
}The 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]
}
}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
}
},
}Walks the trie toward key from pos, returning the leaf value
without copying it; absent paths yield empty bytes.
function trie_walk(node : StatelessInputSlice, key : TriePath, pos : trie_path_cursor) -> StatelessInputSlice = {
if node.len == 0 then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let matches = path_matches(key, pos, path);
if not_bool(matches) then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let path_length = path_len(path);
let key_length = path_len(key);
if pos + path_length == key_length then {
value
} else {
EMPTY_STATELESS_INPUT_SLICE
}
}
},
InputExtensionNode(path, childref) => {
let extension_len : trie_path_len = path_len(path);
if extension_len == 0 then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let matches = path_matches(key, pos, path);
if not_bool(matches) then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let next_pos = pos + extension_len;
if next_pos <= 64 then {
let child = resolve_witness_ref(childref);
trie_walk(child, key, next_pos)
} else {
EMPTY_STATELESS_INPUT_SLICE
}
}
}
},
InputBranchNode(children, value) => {
let key_length = path_len(key);
if pos == key_length then {
value
} else if pos < key_length then {
let child_nibble = path_nibble(key, pos);
let childref = branch_refs_get(children, child_nibble);
let child = resolve_witness_ref(childref);
trie_walk(child, key, pos + 1)
} else {
EMPTY_STATELESS_INPUT_SLICE
}
},
}
}
}let EMPTY_STATELESS_INPUT_SLICE : StatelessInputSliceFields(0, 0) = stateless_input_slice(0, 0)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)
}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 at or immediately after a position in a trie path.
type trie_path_cursor = range(0, 64)The number of nibbles in a trie path.
type trie_path_len = range(0, 64)function trie_lookup¶
Looks up key from a root hash; the root node itself must be
witnessed.
function trie_lookup(root : hash, key : TriePath) -> StatelessInputSlice = {
if root == EMPTY_TRIE_ROOT then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let node = node_db_lookup(root);
if node.len == 0 then {
fatal_error(WitnessDeficient)
} else {
trie_walk(node, key, 0)
}
}
}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)
}Walks the trie toward key from pos, returning the leaf value
without copying it; absent paths yield empty bytes.
function trie_walk(node : StatelessInputSlice, key : TriePath, pos : trie_path_cursor) -> StatelessInputSlice = {
if node.len == 0 then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let matches = path_matches(key, pos, path);
if not_bool(matches) then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let path_length = path_len(path);
let key_length = path_len(key);
if pos + path_length == key_length then {
value
} else {
EMPTY_STATELESS_INPUT_SLICE
}
}
},
InputExtensionNode(path, childref) => {
let extension_len : trie_path_len = path_len(path);
if extension_len == 0 then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let matches = path_matches(key, pos, path);
if not_bool(matches) then {
EMPTY_STATELESS_INPUT_SLICE
} else {
let next_pos = pos + extension_len;
if next_pos <= 64 then {
let child = resolve_witness_ref(childref);
trie_walk(child, key, next_pos)
} else {
EMPTY_STATELESS_INPUT_SLICE
}
}
}
},
InputBranchNode(children, value) => {
let key_length = path_len(key);
if pos == key_length then {
value
} else if pos < key_length then {
let child_nibble = path_nibble(key, pos);
let childref = branch_refs_get(children, child_nibble);
let child = resolve_witness_ref(childref);
trie_walk(child, key, pos + 1)
} else {
EMPTY_STATELESS_INPUT_SLICE
}
},
}
}
}let EMPTY_STATELESS_INPUT_SLICE : StatelessInputSliceFields(0, 0) = stateless_input_slice(0, 0)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)The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}A stateless-input range with its coordinate and length packed existentially.
type StatelessInputSlice = {
'off 'len,
stateless_input_valid_range('off, 'len).
StatelessInputSliceFields('off, 'len)
}A 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 }The common digest type used by trie, code, and block hashes.
type hash = b256