Receipt retention and the receipts trie¶
Execution-ordered retention of encoded receipts and the post-execution block-wide receipt reductions. Canonical wire encoding is owned by the receipt RLP codec.
type ReceiptRecordsRef¶
One execution-ordered sequence of length-prefixed encoded receipts in the scratch arena. The records contain no Sail list or aggregate receipt values: each push writes an eight-byte little-endian length followed by the canonical trie value.
struct ReceiptRecordsRef = {
bytes : ScratchSlice,
count : transaction_count,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}Number of transactions in a schema-valid execution payload.
type transaction_count = range(0, transaction_count_bound)function receipt_store_begin¶
Opens the execution-ordered receipt record region for one block.
function receipt_store_begin() -> source_pointer =
scratch_begin()Marks the start of a scratch construction.
function scratch_begin() -> source_pointer = {
let arena = scratch_arena;
arena.len
}An absolute byte position in a named source region.
type source_pointer = range(0, default_host_region_bound)function receipt_record_pop¶
Splits the first retained record from an execution-ordered record span.
function receipt_record_pop(records : ScratchSlice) -> (ScratchSlice, ScratchSlice) = {
let records : ScratchSliceAtLeast(8) =
if EIGHT_BYTE_LENGTH <= records.len then records else fatal_error(WitnessDeficient);
let value_length = decode_scratch_uint(records, 0);
let payload = slice_suffix(records, EIGHT_BYTE_LENGTH);
if value_length <= payload.len then {
(sub_slice(payload, 0, value_length), slice_suffix(payload, value_length))
} else {
fatal_error(WitnessDeficient)
}
}function decode_scratch_uint(input, offset) = {
let byte0 = slice_byte(input, offset);
let offset1 = scratch_field_offset(offset, 1);
let byte1 = slice_byte(input, offset1);
let offset2 = scratch_field_offset(offset, 2);
let byte2 = slice_byte(input, offset2);
let offset3 = scratch_field_offset(offset, 3);
let byte3 = slice_byte(input, offset3);
let offset4 = scratch_field_offset(offset, 4);
let byte4 = slice_byte(input, offset4);
let offset5 = scratch_field_offset(offset, 5);
let byte5 = slice_byte(input, offset5);
let offset6 = scratch_field_offset(offset, 6);
let byte6 = slice_byte(input, offset6);
let offset7 = scratch_field_offset(offset, 7);
let byte7 = slice_byte(input, offset7);
unsigned(byte0)
+ unsigned(byte1)
* 2 ^ 8
+ unsigned(byte2)
* 2 ^ 16
+ unsigned(byte3)
* 2 ^ 24
+ unsigned(byte4)
* 2 ^ 32
+ unsigned(byte5)
* 2 ^ 40
+ unsigned(byte6)
* 2 ^ 48
+ unsigned(byte7)
* 2 ^ 56
}function fatal_error(_reason) = exit(())let EIGHT_BYTE_LENGTH : int(8) = 8The reason a block fails validation; one variant per violated block-validity rule.
enum FatalError = {
/* chain config: wrong fork / inactive activation */
InvalidConfig,
/* witness ancestor headers not contiguous */
HeaderChainBroken,
/* a transaction failed to RLP-decode */
RlpDecode,
/* a tx signature did not authenticate its sender */
InvalidSignature,
/* header.gas_limit is outside the consensus domain */
InvalidGasLimit,
/* EIP-7778: a tx exceeds the block's remaining gas */
GasUsedExceedsLimit,
/* a tx exceeds the block's remaining blob gas */
BlobGasLimitExceeded,
/* an invalid tx or a failed block-end system call */
ExecutionInvalid,
/* recomputed cumulative gas != header.gas_used */
InvalidGasUsed,
/* recomputed blob gas != header.blob_gas_used */
InvalidBlobGasUsed,
/* header.excess_blob_gas != expected */
InvalidExcessBlobGas,
/* recomputed post-state root != header.state_root */
InvalidStateRoot,
/* recomputed receipts root != header.receipts_root */
InvalidReceiptsRoot,
/* recomputed logs bloom != header.logs_bloom */
InvalidLogsBloom,
/* recomputed block hash != payload expected hash */
InvalidBlockHash,
/* header.parent_hash != authenticated parent */
InvalidParentHash,
/* EIP-7928: BAL item count > gas_limit / 2000 */
BlockAccessListTooLarge,
/* reconstructed EIP-7928 BAL bytes mismatch */
InvalidBlockAccessList,
/* reconstructed EIP-7685 request bytes mismatch */
InvalidExecutionRequests,
/* a missing/inconsistent proof node (thrown at use) */
WitnessDeficient,
/* an exact protocol integer exceeds its bounded execution representation */
NumericOverflow,
}A scratch-arena range with its coordinate and length packed existentially.
type ScratchSlice = {
'off 'len,
scratch_valid_range('off, 'len).
ScratchSliceFields('off, 'len)
}A scratch-arena range of at least 'minimum bytes.
type ScratchSliceAtLeast('minimum : Int) = {
'off 'len,
scratch_valid_range('off, 'len) & 0 <= 'minimum & 'minimum <= 'len.
ScratchSliceFields('off, 'len)
}function receipt_store_append¶
function receipt_store_append(receipt, cumulative_gas_used, _index) =
receipt_record_append(receipt, cumulative_gas_used)function receipt_record_append(r, cumulative_gas_used) = {
let encoded_len = receipt_encoded_length(r, cumulative_gas_used);
if 2 ^ 64 - 1 < encoded_len then {
fatal_error(RlpDecode)
};
let record_len = scratch_length_add(EIGHT_BYTE_LENGTH, encoded_len);
let _record_start = scratch_reserve(record_len);
receipt_record_write_length(encoded_len);
let encoder = rlp_encoder_begin(encoded_len);
receipt_write_encoded(r, cumulative_gas_used);
let _encoded = rlp_encoder_finish(encoder);
()
}function receipt_store_append(receipt, cumulative_gas_used, _index) =
receipt_record_append(receipt, cumulative_gas_used)function receipt_store_root¶
Computes the canonical receipts root over the retained records after the last transaction has executed, then releases the retained region.
function receipt_store_root(records_start : source_pointer, count : transaction_count) -> hash = {
let records = scratch_finish(records_start);
let root = indexed_receipt_trie_root(struct { bytes = records, count = count });
scratch_rewind(records_start);
root
}The receipts-trie root over execution-ordered retained records.
function indexed_receipt_trie_root(receipts : ReceiptRecordsRef) -> hash = {
let source = IndexedReceipts(receipts);
indexed_trie_root(source)
}The slice covering everything pushed since start.
function scratch_finish(start : source_pointer) -> ScratchSlice =
let start_offset = start in
let arena = scratch_arena in
let stop_offset = arena.len in
if start_offset <= stop_offset then {
sub_slice(arena, start, stop_offset - start_offset)
} else {
assert(false, "scratch finish mark");
EMPTY_SCRATCH_SLICE
}Discards everything pushed since mark.
function scratch_rewind(mark : source_pointer) -> unit =
let mark_offset = mark in
let arena = scratch_arena in
let cursor_offset = arena.len in
if mark_offset <= cursor_offset then {
scratch_arena = sub_slice(arena, 0, mark);
host_scratch_truncate(mark)
} else {
assert(false, "scratch rewind mark")
}The common digest type used by trie, code, and block hashes.
type hash = b256An absolute byte position in a named source region.
type source_pointer = range(0, default_host_region_bound)Number of transactions in a schema-valid execution payload.
type transaction_count = range(0, transaction_count_bound)function block_logs_bloom_matches¶
Compares the block logs bloom of one consecutive retained log range with the payload-header commitment (YP ยง4.4.1). The range covers exactly the logs retained by the block's transaction receipts.
function block_logs_bloom_matches(logs : LogSeriesRef, reference : LogsBloomRef) -> bool = {
let logs_bloom = bloom_add_logs(EMPTY_LOGS_BLOOM, logs);
logs_bloom_matches_ref(logs_bloom, reference)
}Adds a consecutive retained log range to a bloom.
function bloom_add_logs(bloom : LogsBloom, logs : LogSeriesRef) -> LogsBloom = {
var out = bloom;
var offset : log_store_index = 0;
while offset < logs.count termination_measure(logs.count - offset) do {
let index = log_store_index_add(logs.start, offset);
out = bloom_add_log_at(out, index);
offset = log_store_index_increment(offset)
};
out
}Compares the computed block bloom with the payload-header commitment.
function logs_bloom_matches_ref(computed : LogsBloom, reference : LogsBloomRef) -> bool = {
let expected = logs_bloom_from_ref(reference);
logs_bloom_equal(computed, expected)
}let EMPTY_LOGS_BLOOM : LogsBloom = vector_init(256, 0x00)A consecutive transaction-local view into the block-lifetime host log store. Reverted frame records are removed before this view is captured.
struct LogSeriesRef = {
start : log_store_index,
count : log_store_index,
}The payload header's 2048-bit logs bloom in canonical SSZ wire order. Keeping the authenticated input range by reference lets consumers that already operate on bytes avoid an eager 256-byte materialization.
type LogsBloomRef = StatelessInputSliceLength(256)