EIP-7745 Trustless log and transaction index

No call decision · Scaling & data · EIP status Draft · created 2024-07-17

Replaces the 2048-bit bloom filter in the block header with a new index structure that places all logs, transactions, and blocks in a single linear index space, which can be verified and searched using Merkle proofs.

Why it is needed

Today the block header contains a 2048-bit bloom filter that roughly tells you which logs a block contains. But this filter is only useful when it is sufficiently empty. As block gas limits have grown, a single mainnet block now holds more than 1000 log addresses and topics on average, and the rate at which the filter wrongly answers that something is present (the false positive rate) has soared, making it practically useless. Restoring it to a usable level would require growing the filter about 10 times, which would enlarge the header to about 3 kilobytes, and even then finding a year of recent logs would require downloading more than 6 gigabytes of header data. Checking which block a transaction was included in also requires the bodies of every block in the search range. Full nodes and RPC servers build and use their own indexes, but these indexes cannot be verified remotely, so users end up having to trust the server. The starting point of this proposal is that no matter how much transaction throughput is increased, it goes against the purpose of Ethereum if there is no way to verify the results without trust.

What changes

Today

After EIP-7745

Who is affected

In depth

Header change: replace logs_bloom with log_index_root

After FORK_TIMESTAMP, execution clients must replace the header's logs_bloom field with log_index_root. This value is the root hash of the LogIndex structure after adding the logs emitted in that block. Unlike the per-block independent bloom filters, each block header commits to a single global index that accumulates the events of the entire chain. As a result, a searcher needs to trust only the latest header to get proofs for log search results over the entire past range.

The linear index space and the three index entry types

Three kinds of index entries, log, transaction, and block, are laid out in order in the global linear index space. Each entry produces one or more map values: a log entry produces 1 address value and 0 to 4 topic values, and a transaction entry and a block entry produce 1 each. Map value hashes are computed as sha2(address), sha2(topic), sha2(tx_hash + b"\x01"), and sha2(block_hash + b"\x02"). For each transaction, the transaction entry is added first, followed by its log entries in EVM execution order. The block entry for block N can be inserted only after the block hash is final, so it appears as the first entry in the index state of block N+1. Each entry carries LogMeta, TransactionMeta, or BlockMeta metadata, which makes it possible to prove receipt and block hash lookups as well.

Filter map structure and proposed constants

The index space has one filter map for every interval of VALUES_PER_MAP (2**16) length. A filter map is a sparse bitmap of size MAP_WIDTH (2**24) x MAP_HEIGHT (2**16), and one bit is set per map value. Because each map holds a fixed number of values, VALUES_PER_MAP, the false positive probability stays constant. Maps are grouped into index epochs of MAPS_PER_EPOCH (2**10) maps each, and within an epoch the rows with the same row number are placed close together in the hash tree, so they can be fetched cheaply with a single Merkle multiproof. A row is a ProgressiveList[uint32], encoded by listing the column indexes in ascending order. The full history holds up to MAX_EPOCH_HISTORY (2**24) epochs.

Mapping layers: solving row saturation for popular values

If a particular address or topic appears very frequently, the row assigned to that value grows long and search efficiency drops. To prevent this, once a row reaches MAX_ROW_LENGTH[0] (8), later values move to a different row in layer 1, and the limit also grows to MAX_ROW_LENGTH[1] (168). It continues this way up to 2728 and 10920. Row assignment in layer 0 changes every epoch (MAPPING_FREQUENCY[0]=2**10 maps), but higher layers change more often (2**6, 2**2, 1 map), so that rare values that happened to collide with a popular value are not harmed for long. If a row is filled to the limit of its layer, the searcher also checks the row in the next layer, and data beyond the limit can safely be ignored as belonging to the higher layer.

Column mapping and false positive rate

The column index is computed by mixing the map value index and the map value hash with fnv1a_64, and each index position sets exactly one bit in a dedicated range of width MAP_WIDTH // VALUES_PER_MAP. This makes it possible to recover the original position from the column index, and candidates are filtered by checking that the recomputed column index matches. The average false positives per map come to about 0.0044, from about VALUES_PER_MAP ** 2 / MAP_WIDTH / MAP_HEIGHT plus the extra per-layer check cost. As of March 2025 a block has just over 1000 map values, so that is roughly 1 per 14000 blocks, or about 1200 over the full chain history. Pattern searches that match address and topic together need several positions to match at once, so the false positive rate drops exponentially further.

Minimal state, initialization, and wire protocol extension

A finalized subtree in the tree can be collapsed into a single node, so a client that only verifies can update the root hash while holding just a minimal state whose size is bounded by a hard cap. Serialized efficiently, the minimal state is 21300808 bytes (about 21 megabytes), and this much data is enough to initialize the index at any point. Initializing at an epoch boundary is cheaper, but the last incomplete epoch must then be built directly from the blocks and receipts after the boundary. Both approaches require an Ethereum wire protocol extension that requests Merkle multiproofs against a recent finalized block, and because the data is large it is received split into several independently verifiable proofs.

Compatibility and cost

Existing log filter APIs such as eth_getLogs, eth_newFilter, eth_getFilterLogs and eth_getFilterChanges can be implemented unchanged on top of the new structure, so existing applications get faster search without modification. The EVM only emits logs and never reads them directly, so it is unaffected. The extra processing cost is small, but storage cost rises by about 15%, so the source says an adjustment to LOG opcode pricing could be considered after benchmarking. On security, the source analyzes that a collision attack that deliberately raises the false positive rate for a specific value is theoretically possible, but because the column index depends on both the value and its exact position, only a block builder could attempt it, and the payoff is small.

Status: Draft stage

This EIP is in Draft status, and its content, including constants and data structures, may change. It assumes EIP-7916, which defines the SSZ types ProgressiveList and ProgressiveByteList. In the call records it was discussed in the 08.29 AMA #14, and no decisions such as fork inclusion were made.

Original EIP · ← EIPs