## EPF Final Report - Vineet Pant
**Project:** EIP-7745 Log Value Index Implementation
**Client:** Nimbus Execution Client
**Cohort:** 6
**Report Date:** January 12, 2025
**Mentors:** [Etan](https://github.com/etan-status) and [Advaita](https://github.com/advaita-saha)
### Project Abstract
Implementation of EIP-7745 (Log Value Index) in Nimbus Execution Client
**Summary:**
I decided to work on [EIP 7745](https://eips.ethereum.org/EIPS/eip-7745) and thankfully with the help of mentors from Nimbus team we have achieved M0 implementation.
EIP 7745 aims at replacing traditional `logsBloom` with `logIndexSummary` structure which offers:
- Efficient log queries
- Cryptographic proofs
- Significantly improved light client capabilities
**Links:**
- [Proposal Document](https://github.com/eth-protocol-fellows/cohort-six/blob/master/projects/pureth-eip-7919-nimbus.md)
- [EIP 7745 implementation guide](https://pureth.guide/implementations-7745/)
- [nimbus-eth1 PR](https://github.com/status-im/nimbus-eth1/pull/3820)
- [nim-eth PR](https://github.com/status-im/nim-eth/pull/827)
- [Pureth Glamsterdam](https://pureth.guide/glamsterdam/)
### Project Description
EIP-7745 addresses fundamental limitations of Ethereum's bloom filters by introducing `LogIndex` and `FilterMaps` which are used to generate `logIndexSummary` to replace `logsBloom`.
- LogIndex answers: "What logs exist?"
- FilterMaps answers: "Where can I find logs from address X?"
- LogIndexSummary = The table of contents + index + cryptographic fingerprint
Following benefits are achieved with the introduction of `logIndexSummary`
- Zero false positives
* `logsBloom` has high false positive rate (approx 40%) while `logIndexSummary` has guaranteed result.
- Direct queries by address or topic instead of scanning blocks sequentially
* With traditional `logsBloom` all blocks are sequentially checked so time complexity is O(n) but in `LogIndexSummary` Approach:
1. proc findLogs(address: Address, blockRange: 0..100000) =
2. let results = eth_getLogsByAddress(address, blockRange)
3. Returns only matching blocks immediately [5, 42, 156, 789, ...]
* Performance:
1. RPC calls: 1 (direct query)
2. Time: O(log n) or O(1) depending on index structure
3. Example: 100k blocks = ~1 second to query index
- Cryptographic proofs via merkle roots
* With traditional `logsBloom` Light Client has to trust full nodes but with `logIndexSummary` Light client would do following:
1. Extracts LogIndexSummary from block header
2. Gets root from summary
3. Verifies merkle proof against root
4. If valid: log is GUARANTEED to be in block !
- Light client optimization
* With traditional `logsBloom` all block headers have to be downloaded, parsed and verified which takes significant amount of Bandwidth and consumes time.
* While `logIndexSummary` gives matching blocks with single query which can be downloaded and verified with proofs
The same 256-byte `header.logsBloom` field is reused and replaced with 256 byte `logIndexSummary`
**Critical Implementation Requirement:** All data structures use **SSZ (Simple Serialize)** encoding to enable:
- Merkleization for cryptographic proofs
- Deterministic serialization for consensus
This required migrating Nimbus from traditional `seq[Topic]` types to SSZ-compatible `List[Topic, 4]` types throughout the codebase.
## Status
The core implementation is functional and verified in a Kurtosis testnet environment. All primary objectives have been achieved for [M0 milestone](https://pureth.guide/implementations-7745/) as per objective:
- **Core LogIndex System** - Core implemented with all data structures
- **Block Creation Integration** - tx_packer.nim generates LogIndexSummary
- **Block Validation Integration** - process_block.nim validates LogIndexSummary
- **Fork Activation Logic** - Dual validation (bloom vs LogIndexSummary), pre and post EIP 7745 activation.
- **LogIndex Accumulation** - Properly accumulates from genesis
- **Testnet Verification** - Successfully validated in Kurtosis
- **Draft PRs** - Created for both nim-eth and nimbus-eth1
### High-Level Design
```
┌─────────────────────────────────────────────────────────────┐
│ Block Processing │
│ │
│ 1. Receive parent's LogIndex state │
│ 2. Process transactions → collect logs │
│ 3. add_block_logs() → accumulate to LogIndex │
│ 4. createLogIndexSummary() → generate 256-byte summary │
│ 5. Encode summary → header.logsBloom │
│ 6. Return (receipts, updatedLogIndex) to chain │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LogIndex Storage │
│ │
│ Store in BlockRef for next block to use │
└─────────────────────────────────────────────────────────────┘
```
### Core LogIndex Data Structures
All Data Structures and Core functions for LogIndex implementations are in file @`execution_chain/core/log_index.nim`.
#### 1. LogIndex (The Global Accumulator)
Accumulates all logs from genesis. This grows with each block.
```nim
LogIndex = object
epochs*: seq[LogIndexEpoch] # All log data organized by epochs
next_index*: uint64 # Total entries since genesis
latest_block_delimiter_index*: uint64
latest_log_entry_index*: uint64
latest_value_index*: uint64
```
---
#### 2. LogIndexSummary (Block Header Payload)
256-byte summary that replaces bloom filter in block headers.
```nim
LogIndexSummary = object
root*: Hash32 # Merkle root of entire LogIndex
epochs_root*: Hash32 # Merkle root of epochs
epoch_0_filter_maps_root*: Hash32 # Merkle root of filter maps
latest_block_delimiter_index*: uint64 # Index of last block marker
latest_log_entry_index*: uint64 # Index of last log
latest_value_index*: uint32 # Position in filter maps
latest_layer_index*: uint32
latest_row_index*: uint32
latest_column_index*: uint32
latest_log_value*: Hash32
latest_row_root*: Hash32
```
```
Offset Size Field
─────────────────────────────────────────────────
0x00 32 root (Hash32)
0x20 32 epochs_root (Hash32)
0x40 32 epoch_0_filter_maps_root (Hash32)
0x60 8 latest_block_delimiter_index (uint64)
0x68 32 latest_block_delimiter_root (Hash32)
0x88 8 latest_log_entry_index (uint64)
0x90 32 latest_log_entry_root (Hash32)
0xb0 4 latest_value_index (uint32)
0xb4 4 latest_layer_index (uint32)
0xb8 4 latest_row_index (uint32)
0xbc 4 latest_column_index (uint32)
0xc0 32 latest_log_value (Hash32)
0xe0 32 latest_row_root (Hash32)
─────────────────────────────────────────────────
Total: 256 bytes
```
---
#### 3. FilterMap (Sparse Bitmap)
Efficiently store which addresses/topics exist without requiring massive bitmaps.
- Sparse: Only store coordinates of set bits and takes few KB of storage
```nim
FilterMap = object
rows*: Table[uint64, seq[uint64]] # row_index -> [column_indices]
```
```nim
For value V (address hash, topic hash):
row = V mod 2^16 # 65,536 possible rows
column = V div 2^16 # Position within row
Store: rows[row] = [col1, col2, col3, ...]
```
---
#### 4. LogIndexEpoch
Organizes logs into epochs for efficient processing and storage.
```nim
LogIndexEpoch = object
records*: Table[uint64, LogRecord] # index -> log record
log_index_root*: Hash32 # Merkle root of this epoch
filter_maps*: FilterMaps # MAPS_PER_EPOCH filter maps
```
---
### Code Implementation
#### SSZ Type Migration Challenge in `nim-eth`
Apart from implementation in `nimbus-eth1` there were design challenges to be addressed in `nim-eth` dependency.
LogIndex uses List[Topic, 4] (SSZ list with max size), but Nimbus historically used seq[Topic] (unbounded sequence).
**Resolution:**
- Updated type definitions in nim-eth to use SSZ-compatible types
- Modified serialization/deserialization logic
- Ensured backward compatibility during migration
- Resolved compilation errors and fixed tests
**Impact:** Enabled proper merkleization for proof generation while maintaining type safety
#### `nimbus-eth1` Code Implementation
The implementation basically handles 2 scenarios:
1. Block Import
When receiving blocks from peers via P2P:
```
forked_chain.nim: importBlock()
↓
chain_private.nim: processBlock(parentBlk, ...)
↓
state.nim: BaseVMState.init()
├─ Receives logIndex parameter
└─ logIndex = parentBlk.logIndex ← Pass parent's state!
↓
process_block.nim: validate block
├─ add_block_logs() accumulates current block's logs
└─ Validate LogIndexSummary matches header.logsBloom
↓
Return (receipts, updatedLogIndex)
↓
forked_chain.nim: appendBlock()
└─ Store in BlockRef { logIndex: updatedLogIndex }
```
**Key Files:**
- `execution_chain/core/chain/forked_chain/chain_private.nim` - processBlock()
- `execution_chain/core/chain/forked_chain.nim` - appendBlock()
- `execution_chain/evm/state.nim` - BaseVMState initialization
2. Block Creation
When creating new blocks to propose:
```
tx_packer.nim: assembleBlock()
↓
tx_desc.nim: updateVmState()
└─ setupVMState(logIndex = chain.latest.logIndex)
↓
tx_packer.nim: pack transactions
├─ Collect logs from transaction execution
├─ add_block_logs() accumulates logs
├─ createLogIndexSummary() generates 256-byte summary
└─ Encode to header.logsBloom
↓
Propose new block with LogIndexSummary
```
**Key Files:**
- `execution_chain/core/tx_pool/tx_desc.nim` - setupVMState()
- `execution_chain/core/tx_pool/tx_packer.nim` - assembleBlock()
### Unfinished Work
The M0 milestone is complete and functional, but there's still work to do before this is production-ready.
#### 1. Code Cleanup
The PRs are currently in draft state. I need to go through and clean things up:
- Remove debug logging that I added while debugging accumulation issues
- Clean up commented code from experiments that didn't work out
- Add proper documentation comments for public functions
#### 2. RPC Integration and Testing
- The M0 milestone focused on block processing, so RPC integration wasn't part of the scope. The next step is exposing LogIndex functionality through RPC calls:
1. eth_getLogs - Currently this uses bloom filters to find matching blocks. With LogIndex, we can query the FilterMap directly to get only blocks that actually contain logs from a specific address or topic. This should be much faster and eliminate false positives.
2. eth_getLogProof - This is one of the main benefits of EIP-7745 - generating merkle proofs for logs. Light clients can verify these proofs against the LogIndexSummary in the block header without trusting full nodes. This needs to be implemented.
#### 3. Performance Testing
I haven't done proper performance comparisons yet. Need to measure:
- Query time: LogIndex vs bloom filters for eth_getLogs
- Memory usage: How much does LogIndex grow over time?
- Block processing speed: Does LogIndex add noticeable overhead during block import and creation?
#### 4. Kurtosis Testing with Other Clients
- Kurtosis works great for testing Nimbus in isolation, but I couldn't test interoperability with other clients since no other client has M0 implemented yet.
### Pivots and Changes
#### Pivot: LogIndex Accumulation Strategy
- Initially I stored LogIndex in a separate global state but later to accumulate all logs from Parent to Next Block I had to change it.
- Pivot: Pass LogIndex as parameter through all vmState initialization functions
**Impact:**
- Simpler architecture (fewer state containers)
- Clearer data flow (explicit parameter passing)
- Easier to debug (visible in function signatures)
### Current Usability
- Code compiles and runs correctly
- Technical docs available
- Verified in testnet environment
- Kurtosis deployment successful
- Blocks process without failures
- LogIndexSummary validates correctly
### Current Impact
In the current status of EIP 7745 with M0 milestone, following points are worth mentioning:
- Reference implementation for implementers
- Testnet-ready code for early adopters
- Architectural proof of replacing `logsBloom` with `logIndexSummary` shows that it can be used by light clients in future
### Limitations
- Pending code review from Nimbus core team
- No RPC query extensions yet
- Not yet production-ready
- Depends on EIP-7745 acceptance into Ethereum roadmap
## Journey with EPF
Well, as Mario and Josh kept reminding us - EPF is not a sprint, it's a marathon. They were right.
The most challenging part wasn't writing code - it was understanding where the code should go. When I started, Nimbus felt like a massive codebase I didn't understand. I remember spending two days trying to figure out where LogIndex accumulation should happen.
The SSZ type migration was brutal. Changing seq[Topic] to List[Topic, 4] in nim-eth broke compilation and tests.
Kurtosis Docker builds kept failing with weird errors. I spent days debugging that instead of writing code. Sometimes protocol development feels more like DevOps than blockchain research.
The Pureth guide was a lifesaver. Whenever I got stuck, I'd go back and re-read the M0 requirements. Breaking it into milestones prevented me from getting overwhelmed by the full scope.
To be honest, there were weeks where I questioned if I'd finish. But taking it day by day - implement one data structure, fix one test, understand one more piece of Nimbus architecture - that's what got me through. It's less about crossing a finish line and more about the daily habit of showing up and learning.
## Acknowledgments
**Mentors:**
- **Etan** - For creating the Pureth guide, providing architectural guidance, and patiently explaining Nimbus internals
- **Advaita** - For code reviews, debugging assistance, and answering countless questions about Nim
**EPF Program:**
- **Josh Davis & Mario Havel** - For organizing EPF Cohort 6 and creating a supportive learning environment
- **Fellow EPF Participants** - For cooperation and interesting conversations throughout the EPF program.