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
- Each block's header contains a fixed 2048-bit bloom filter, but it has too many false positives to be practically useful
- Finding logs over a long range requires downloading all the headers and a large part of the receipts
- Checking whether a transaction was included requires the bodies of every block in the search range
- The search indexes that full nodes build themselves cannot be verified remotely, so you must trust the server
After EIP-7745
- The header's logs_bloom slot is replaced by log_index_root, the root hash of the entire index
- You can search by receiving and verifying only the small pieces related to the events you want, using Merkle proofs
- Not only logs but also transaction hash lookups and block hash and block number lookups are proven with the same structure
- The number of values held in a single map is fixed, so the false positive rate stays consistently low
Who is affected
- Wallet & app developers: You get the search performance improvement while continuing to use existing log filter APIs such as eth_getLogs. No application code changes are required.
- Light client users: Without a trusted server, you can verify using only the Merkle proofs received from a remote prover that log search results are complete and correct.
- Client teams: Implementations must handle the header schema change, the new index data structure, and an extension of the wire protocol for initialization. The additional storage is about 15 to 20% of the actual log size.
- Validators: Even a node that has no interest in log search or proofs can produce the root hash by maintaining only the minimal index state, whose size is limited by a hard cap.
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.