Trie updates and subtree assembly¶
Ordered leaf updates, their structural items, and the recursive assembly that recomposes canonical nodes (YP Appendix D).
An account update carries its account's post-state storage root, so pulling one account update runs this same builder one trie level down and the cursor, the source pull, and the builder form a single recursive group. Every member of that group declares the same constant termination measure, 1024, which bounds the nested call depth of a complete state-root computation: at most two trie levels, each at most 65 nibble depths deep, each depth reached through at most four nested builder calls, plus the constant pull chain that opens the nested level. The measure is a proof-obligation budget only; it is erased from the executable backends.
type TrieChange¶
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 scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}type TrieUpdate¶
An update: a full-path key and its change. Sources yield updates in ascending key order.
struct TrieUpdate = { key : TriePath, change : TrieChange }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 trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }type TrieUpdateSource¶
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,
}A 20-byte account address (YP §4.1), in canonical protocol byte order.
type address = vector(20, inc, byte)type TrieUpdateFetch¶
One pull from an ordered update source. update is meaningful exactly
when available is true.
struct TrieUpdateFetch = { available : bool, update : TrieUpdate }An update: a full-path key and its change. Sources yield updates in ascending key order.
struct TrieUpdate = { key : TriePath, change : TrieChange }let EMPTY_TRIE_UPDATE¶
The payload sentinel used by exhausted source pulls.
let EMPTY_TRIE_UPDATE : TrieUpdate = struct { key = path_empty(), change = TrieDelete() }The empty path.
function path_empty() -> TriePath =
path_new(ZERO_HASH, 0)An update: a full-path key and its change. Sources yield updates in ascending key order.
struct TrieUpdate = { key : TriePath, change : TrieChange }type 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 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 number of nibbles in a trie path.
type trie_path_len = range(0, 64)type TrieUpdateCursor¶
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,
}An update: a full-path key and its change. Sources yield updates in ascending key order.
struct TrieUpdate = { key : TriePath, change : TrieChange }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,
}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,
}function trie_updates_begin¶
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 }
}State-backed implementation of the generic trie's pull-source contract.
function trie_update_source_next(source : TrieUpdateSource) -> TrieUpdateFetch =
match source {
StorageTrieUpdates(addr) => next_storage_trie_update(addr),
ChangedAccountTrieUpdates() => next_changed_account_trie_update(),
}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,
}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,
}function updates_empty¶
Whether the pull cursor has reached the end of its source.
function updates_empty(updates : TrieUpdateCursor) -> bool =
match updates.relation {
UpdateSourceExhausted(_) => true,
_ => 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 trie_updates_pop¶
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)
},
}The common-prefix length of two canonical nibble paths.
function common_prefix_length(a : TriePath, b : TriePath) -> trie_path_len = {
let alen = path_len(a);
let blen = path_len(b);
let stop =
if alen < blen then alen else blen;
var length : trie_path_len = 0;
var matching : bool = true;
while matching & length < stop termination_measure(stop - length) do {
let a_nibble = path_nibble(a, length);
let b_nibble = path_nibble(b, length);
if a_nibble == b_nibble then {
let current_length = length;
length =
if current_length < 64 then {
current_length + 1
} else {
fatal_error(WitnessDeficient)
}
} else {
matching = false
}
};
length
}function fatal_error(_reason) = exit(())State-backed implementation of the generic trie's pull-source contract.
function trie_update_source_next(source : TrieUpdateSource) -> TrieUpdateFetch =
match source {
StorageTrieUpdates(addr) => next_storage_trie_update(addr),
ChangedAccountTrieUpdates() => next_changed_account_trie_update(),
}The payload sentinel used by exhausted source pulls.
let EMPTY_TRIE_UPDATE : TrieUpdate = struct { key = path_empty(), change = TrieDelete() }The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}An update: a full-path key and its change. Sources yield updates in ascending key order.
struct TrieUpdate = { key : TriePath, change : TrieChange }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 trie_updates_rebase¶
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
},
}The path with its first n nibbles removed.
function path_drop(path : TriePath, n : trie_path_len) -> TriePath = {
let length = path_len(path);
if length <= n then {
path_empty()
} else if n == 0 then {
path
} else {
let remain : trie_path_len = length - n;
var result = path_empty();
var offset : trie_path_len = 0;
while offset < remain termination_measure(remain - offset) do {
let candidate = n + offset;
let source_index : trie_path_cursor =
if (0 <= candidate) & (candidate <= 64) then {
candidate
} else {
assert(false);
0
};
let nibble = path_nibble(path, source_index);
result = path_append_nibble(result, nibble);
let current_offset = offset;
offset =
if current_offset < 64 then {
current_offset + 1
} else {
fatal_error(WitnessDeficient)
}
};
result
}
}The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenA 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,
}function trie_updates_descend¶
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),
}function fatal_error(_reason) = exit(())The path with its first n nibbles removed.
function path_drop(path : TriePath, n : trie_path_len) -> TriePath = {
let length = path_len(path);
if length <= n then {
path_empty()
} else if n == 0 then {
path
} else {
let remain : trie_path_len = length - n;
var result = path_empty();
var offset : trie_path_len = 0;
while offset < remain termination_measure(remain - offset) do {
let candidate = n + offset;
let source_index : trie_path_cursor =
if (0 <= candidate) & (candidate <= 64) then {
candidate
} else {
assert(false);
0
};
let nibble = path_nibble(path, source_index);
result = path_append_nibble(result, nibble);
let current_offset = offset;
offset =
if current_offset < 64 then {
current_offset + 1
} else {
fatal_error(WitnessDeficient)
}
};
result
}
}The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenThe 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,
}The structural items¶
The walker merges updates into structural items. Recursive parent calls turn those items back into canonical trie nodes while untouched subtrees stay references.
type TrieItemValue¶
An item's payload: no subtree at all, a live leaf, a known-branch
reference (extension children are always branches), or a subtree
reference of unknown kind. The empty member realizes YP Eq. 207's
n(I,i) = () if I = {} directly, so recursive assembly is total; the
reference distinction permits untouched hashes to stay opaque.
union TrieItemValue = {
/* no subtree exists at this prefix (YP Eq. 207) */
EmptySubtree : unit,
/* a live leaf's bytes */
LeafItem : TrieLeafValue,
/* a known-branch child reference */
BranchItem : NodeRef,
/* an opaque subtree reference of unknown kind */
SubtreeItem : NodeRef,
}A reference to a trie node: empty, inline (encodings under 32 bytes), or by KECCAK-256 hash (YP Appendix D, Eq. 207).
union NodeRef = {
/* the empty node */
EmptyRef : unit,
/* an authenticated node embedded in witness input */
InputInlineRef : StatelessInputSliceAtMost(31),
/* a freshly encoded node embedded in a generated parent */
ScratchInlineRef : InlineNode,
/* a node referenced by its KECCAK-256 hash */
HashRef : hash,
}A leaf value retained by trie assembly. Authenticated witness and transaction leaves borrow immutable input bytes; newly encoded state, receipt, and withdrawal leaves borrow the scratch arena.
union TrieLeafValue = {
/* a leaf borrowing immutable stateless input bytes */
InputTrieLeaf : StatelessInputSlice,
/* a leaf borrowing freshly encoded scratch bytes */
ScratchTrieLeaf : ScratchSlice,
}type TrieItem¶
A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }An item's payload: no subtree at all, a live leaf, a known-branch
reference (extension children are always branches), or a subtree
reference of unknown kind. The empty member realizes YP Eq. 207's
n(I,i) = () if I = {} directly, so recursive assembly is total; the
reference distinction permits untouched hashes to stay opaque.
union TrieItemValue = {
/* no subtree exists at this prefix (YP Eq. 207) */
EmptySubtree : unit,
/* a live leaf's bytes */
LeafItem : TrieLeafValue,
/* a known-branch child reference */
BranchItem : NodeRef,
/* an opaque subtree reference of unknown kind */
SubtreeItem : NodeRef,
}A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function trie_empty_subtree¶
The absent subtree (YP n(I,i) = ()). Its path carries no meaning.
function trie_empty_subtree() -> TrieItem =
struct { path = path_empty(), value = EmptySubtree() }The empty path.
function path_empty() -> TriePath =
path_new(ZERO_HASH, 0)A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }function trie_leaf¶
function trie_leaf(path : TriePath, value : TrieLeafValue) -> TrieItem =
struct { path = path, value = LeafItem(value) }A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A leaf value retained by trie assembly. Authenticated witness and transaction leaves borrow immutable input bytes; newly encoded state, receipt, and withdrawal leaves borrow the scratch arena.
union TrieLeafValue = {
/* a leaf borrowing immutable stateless input bytes */
InputTrieLeaf : StatelessInputSlice,
/* a leaf borrowing freshly encoded scratch bytes */
ScratchTrieLeaf : ScratchSlice,
}A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function trie_input_leaf¶
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)
}function trie_leaf(path : TriePath, value : TrieLeafValue) -> TrieItem =
struct { path = path, value = LeafItem(value) }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 sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function trie_scratch_leaf¶
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)
}function trie_leaf(path : TriePath, value : TrieLeafValue) -> TrieItem =
struct { path = path, value = LeafItem(value) }A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function trie_branch¶
function trie_branch(path : TriePath, childref : NodeRef) -> TrieItem =
struct { path = path, value = BranchItem(childref) }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 }function trie_subtree¶
function trie_subtree(path : TriePath, childref : NodeRef) -> TrieItem =
struct { path = path, value = SubtreeItem(childref) }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 }function trie_child_ref¶
The child reference of a single-item subtree at depth: the item's
remaining path is absorbed into it. This is the one place a delete
collapse can demand node material: an unknown-type hash reference
absorbing a nonempty suffix resolves its node from the witness db
(fail-closed).
function trie_child_ref(it : TrieItem, depth : trie_path_len) -> NodeRef = {
let suffix = path_drop(it.path, depth);
let suffix_len = path_len(suffix);
match it.value {
EmptySubtree() => EmptyRef(),
LeafItem(value) => leaf_child_ref(suffix, value),
BranchItem(subref) => if suffix_len == 0 then {
subref
} else {
merge_ext_ref(suffix, subref)
},
SubtreeItem(subref) => if suffix_len == 0 then {
subref
} else {
match subref {
HashRef(h) => {
let node = node_db_lookup(h);
if node.len == 0 then {
fatal_error(WitnessDeficient)
} else {
merge_ext_node(suffix, node)
}
},
_ => merge_ext_ref(suffix, subref),
}
},
}
}function fatal_error(_reason) = exit(())The child reference of a leaf, selecting the encoder for the value's backing region.
function leaf_child_ref(key : TriePath, value : TrieLeafValue) -> NodeRef =
match value {
InputTrieLeaf(bytes) => input_leaf_child_ref(key, bytes),
ScratchTrieLeaf(bytes) => scratch_leaf_child_ref(key, bytes),
}Re-keys a decoded child node under prefix without copying a leaf
value.
function merge_ext_node(prefix : TriePath, childnode : StatelessInputSlice) -> NodeRef = {
let prefix_length = path_len(prefix);
if prefix_length == 0 then {
node_to_ref(childnode)
} else if childnode.len == 0 then {
EmptyRef()
} else {
let decoded = decode_input_trie_node(childnode);
match decoded {
InputLeafNode(path, value) => {
let merged_path = path_concat(prefix, path);
input_leaf_child_ref(merged_path, value)
},
InputExtensionNode(path, child) => {
let merged_path = path_concat(prefix, path);
extension_child_ref(merged_path, child)
},
_ => {
let childref = node_to_ref(childnode);
extension_child_ref(prefix, childref)
},
}
}
}merge_ext_node over a child reference: an inline reference carries its node bytes and re-keys canonically; a 32-byte hash reference is wrapped in an extension, which is canonical only when the referenced node is a branch.
function merge_ext_ref(prefix : TriePath, childref : NodeRef) -> NodeRef = {
let prefix_length = path_len(prefix);
if prefix_length == 0 then {
childref
} else {
match childref {
EmptyRef() => EmptyRef(),
HashRef(_) => extension_child_ref(prefix, childref),
InputInlineRef(node) => {
let decoded = decode_input_trie_node(node);
match decoded {
InputLeafNode(path, value) => {
let merged_path = path_concat(prefix, path);
input_leaf_child_ref(merged_path, value)
},
InputExtensionNode(path, child) => {
let merged_path = path_concat(prefix, path);
extension_child_ref(merged_path, child)
},
_ => extension_child_ref(prefix, childref),
}
},
ScratchInlineRef(node) => {
let node_slice = inline_node_slice(node);
let decoded = decode_scratch_trie_node(node_slice);
match decoded {
ScratchLeafNode(path, value) => {
let merged_path = path_concat(prefix, path);
scratch_leaf_child_ref(merged_path, value)
},
ScratchExtensionNode(path, child) => {
let merged_path = path_concat(prefix, path);
extension_child_ref(merged_path, child)
},
_ => extension_child_ref(prefix, childref),
}
},
}
}
}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 path with its first n nibbles removed.
function path_drop(path : TriePath, n : trie_path_len) -> TriePath = {
let length = path_len(path);
if length <= n then {
path_empty()
} else if n == 0 then {
path
} else {
let remain : trie_path_len = length - n;
var result = path_empty();
var offset : trie_path_len = 0;
while offset < remain termination_measure(remain - offset) do {
let candidate = n + offset;
let source_index : trie_path_cursor =
if (0 <= candidate) & (candidate <= 64) then {
candidate
} else {
assert(false);
0
};
let nibble = path_nibble(path, source_index);
result = path_append_nibble(result, nibble);
let current_offset = offset;
offset =
if current_offset < 64 then {
current_offset + 1
} else {
fatal_error(WitnessDeficient)
}
};
result
}
}The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenThe 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 }An item's payload: no subtree at all, a live leaf, a known-branch
reference (extension children are always branches), or a subtree
reference of unknown kind. The empty member realizes YP Eq. 207's
n(I,i) = () if I = {} directly, so recursive assembly is total; the
reference distinction permits untouched hashes to stay opaque.
union TrieItemValue = {
/* no subtree exists at this prefix (YP Eq. 207) */
EmptySubtree : unit,
/* a live leaf's bytes */
LeafItem : TrieLeafValue,
/* a known-branch child reference */
BranchItem : NodeRef,
/* an opaque subtree reference of unknown kind */
SubtreeItem : NodeRef,
}The number of nibbles in a trie path.
type trie_path_len = range(0, 64)The subtree assembly¶
The canonical builder (YP Appendix D) is expressed as a recursive partition of the key space. Each recursive call returns either no subtree or one structural item. A single child bubbles upward unchanged; two or more children form a branch at the current prefix. Consequently leaf and extension paths are encoded only when they meet a real branch or become the root, and no explicit frame stack is required.
c(I,i): leaf | extension | 17-item branch (Eq. 208) trie_children_finish
n(I,i): () if I = {}; c(I,i) if ||RLP|| < 32;
KEC(RLP(c(I,i))) otherwise (Eq. 207) child_ref
TRIE(I) = KEC(RLP(c(I,0))) (Eq. 206) trie_subtree_root
type TrieChildren¶
Child references accumulated while one recursive branch is assembled.
only retains the structural item when exactly one child survives, so
canonical leaf/extension collapse does not need to reopen an encoded
hash.
struct TrieChildren = {
mask : branch_mask,
children : BranchRefs,
only : TrieItem,
count : range(0, 16),
}The sixteen child references of a branch, indexed by nibble.
type BranchRefs = vector(16, dec, NodeRef)A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A compact presence bitset for the sixteen children of a branch.
type branch_mask = bits(16)function trie_children_empty¶
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 }
}The absent subtree (YP n(I,i) = ()). Its path carries no meaning.
function trie_empty_subtree() -> TrieItem =
struct { path = path_empty(), value = EmptySubtree() }val vector_init = pure {lean: "vectorInit", _: "vector_init"}: forall ('n : Int) ('a : Type), 'n >= 0.
(implicit('n), 'a) -> vector('n, 'a)Child references accumulated while one recursive branch is assembled.
only retains the structural item when exactly one child survives, so
canonical leaf/extension collapse does not need to reopen an encoded
hash.
struct TrieChildren = {
mask : branch_mask,
children : BranchRefs,
only : TrieItem,
count : range(0, 16),
}function trie_children_add¶
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
},
}Whether the mask records a child at the given nibble.
function branch_mask_has(mask : branch_mask, index : nibble) -> bool = {
let index_mask = branch_mask_for(index);
and_vec(mask, index_mask) != 0x0000
}Returns the mask with the child at the given nibble marked present.
function branch_mask_set(mask : branch_mask, index : nibble) -> branch_mask = {
let index_mask = branch_mask_for(index);
or_vec(mask, index_mask)
}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)
}
}The path length in nibbles.
function path_len(path : TriePath) -> trie_path_len = path.lenWhether 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)
}The child reference of a single-item subtree at depth: the item's
remaining path is absorbed into it. This is the one place a delete
collapse can demand node material: an unknown-type hash reference
absorbing a nonempty suffix resolves its node from the witness db
(fail-closed).
function trie_child_ref(it : TrieItem, depth : trie_path_len) -> NodeRef = {
let suffix = path_drop(it.path, depth);
let suffix_len = path_len(suffix);
match it.value {
EmptySubtree() => EmptyRef(),
LeafItem(value) => leaf_child_ref(suffix, value),
BranchItem(subref) => if suffix_len == 0 then {
subref
} else {
merge_ext_ref(suffix, subref)
},
SubtreeItem(subref) => if suffix_len == 0 then {
subref
} else {
match subref {
HashRef(h) => {
let node = node_db_lookup(h);
if node.len == 0 then {
fatal_error(WitnessDeficient)
} else {
merge_ext_node(suffix, node)
}
},
_ => merge_ext_ref(suffix, subref),
}
},
}
}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 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,
}Child references accumulated while one recursive branch is assembled.
only retains the structural item when exactly one child survives, so
canonical leaf/extension collapse does not need to reopen an encoded
hash.
struct TrieChildren = {
mask : branch_mask,
children : BranchRefs,
only : TrieItem,
count : range(0, 16),
}A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }An item's payload: no subtree at all, a live leaf, a known-branch
reference (extension children are always branches), or a subtree
reference of unknown kind. The empty member realizes YP Eq. 207's
n(I,i) = () if I = {} directly, so recursive assembly is total; the
reference distinction permits untouched hashes to stay opaque.
union TrieItemValue = {
/* no subtree exists at this prefix (YP Eq. 207) */
EmptySubtree : unit,
/* a live leaf's bytes */
LeafItem : TrieLeafValue,
/* a known-branch child reference */
BranchItem : NodeRef,
/* an opaque subtree reference of unknown kind */
SubtreeItem : NodeRef,
}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 four-bit path element (YP Appendix D).
type nibble = bits(4)function trie_children_finish¶
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 child reference of a branch node.
function branch_child_ref(mask : branch_mask, children : BranchRefs) -> NodeRef = {
var content_length : branch_content_length = 1;
var child_bit : branch_mask = 0x0001;
foreach (i from 0 to 15) {
let child_present = and_vec(mask, child_bit) != 0x0000;
if child_present then {
let childref = children[i];
let child_length = node_ref_size(childref);
content_length = branch_content_length_add(content_length, child_length)
} else {
content_length = branch_content_length_add(content_length, 1)
};
child_bit = sail_shiftleft(child_bit, 1)
};
let scratch_content_length = rlp_scratch_length_add(content_length, 0);
let encoded_size = rlp_scratch_list_size(scratch_content_length);
let encoder = rlp_encoder_begin(encoded_size);
rlp_write_list_prefix(content_length);
child_bit = 0x0001;
foreach (i from 0 to 15) {
let child_present = and_vec(mask, child_bit) != 0x0000;
if child_present then {
let childref = children[i];
rlp_write_node_ref(childref)
} else {
scratch_push_byte(0x80)
};
child_bit = sail_shiftleft(child_bit, 1)
};
scratch_push_byte(0x80);
let encoded = rlp_encoder_finish(encoder);
let result = child_ref(encoded);
rlp_encoder_rewind(encoder);
result
}function trie_branch(path : TriePath, childref : NodeRef) -> TrieItem =
struct { path = path, value = BranchItem(childref) }Child references accumulated while one recursive branch is assembled.
only retains the structural item when exactly one child survives, so
canonical leaf/extension collapse does not need to reopen an encoded
hash.
struct TrieChildren = {
mask : branch_mask,
children : BranchRefs,
only : TrieItem,
count : range(0, 16),
}A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }A trie path of at most 64 nibbles — secure state and storage keys are
32-byte hashes, and list tries use short RLP indices. data is
high-aligned; len preserves leading zeroes and prefixes.
struct TriePath = { data : b256, len : trie_path_len }function trie_subtree_root¶
Converts a recursively assembled top-level item to the committed root.
function trie_subtree_root(subtree : TrieItem) -> hash =
match subtree.value {
EmptySubtree() => EMPTY_TRIE_ROOT,
_ => {
let root_ref = trie_child_ref(subtree, 0);
trie_ref_to_root(root_ref)
},
}The child reference of a single-item subtree at depth: the item's
remaining path is absorbed into it. This is the one place a delete
collapse can demand node material: an unknown-type hash reference
absorbing a nonempty suffix resolves its node from the witness db
(fail-closed).
function trie_child_ref(it : TrieItem, depth : trie_path_len) -> NodeRef = {
let suffix = path_drop(it.path, depth);
let suffix_len = path_len(suffix);
match it.value {
EmptySubtree() => EmptyRef(),
LeafItem(value) => leaf_child_ref(suffix, value),
BranchItem(subref) => if suffix_len == 0 then {
subref
} else {
merge_ext_ref(suffix, subref)
},
SubtreeItem(subref) => if suffix_len == 0 then {
subref
} else {
match subref {
HashRef(h) => {
let node = node_db_lookup(h);
if node.len == 0 then {
fatal_error(WitnessDeficient)
} else {
merge_ext_node(suffix, node)
}
},
_ => merge_ext_ref(suffix, subref),
}
},
}
}The root hash a node reference commits to; the empty reference is the empty-trie root.
function trie_ref_to_root(r : NodeRef) -> hash =
match r {
EmptyRef() => EMPTY_TRIE_ROOT,
InputInlineRef(node) => keccak256(node),
ScratchInlineRef(node) => inline_node_hash(node),
HashRef(h) => h,
}keccak256(rlp("")) — the root of an empty Merkle-Patricia trie: the
storage root of every account with no storage (EMPTY_ACCOUNT, freshly
created).
let EMPTY_TRIE_ROOT : hash = hash_from_bits(0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421)A sorted-stream item: a path and its payload.
struct TrieItem = { path : TriePath, value : TrieItemValue }An item's payload: no subtree at all, a live leaf, a known-branch
reference (extension children are always branches), or a subtree
reference of unknown kind. The empty member realizes YP Eq. 207's
n(I,i) = () if I = {} directly, so recursive assembly is total; the
reference distinction permits untouched hashes to stay opaque.
union TrieItemValue = {
/* no subtree exists at this prefix (YP Eq. 207) */
EmptySubtree : unit,
/* a live leaf's bytes */
LeafItem : TrieLeafValue,
/* a known-branch child reference */
BranchItem : NodeRef,
/* an opaque subtree reference of unknown kind */
SubtreeItem : NodeRef,
}The common digest type used by trie, code, and block hashes.
type hash = b256