Skip to content

perf(l1): use 4KB RocksDB blocks for trie-node and flat-KV CFs#6940

Open
edg-l wants to merge 5 commits into
mainfrom
perf/rocksdb-4kb-blocks
Open

perf(l1): use 4KB RocksDB blocks for trie-node and flat-KV CFs#6940
edg-l wants to merge 5 commits into
mainfrom
perf/rocksdb-4kb-blocks

Conversation

@edg-l

@edg-l edg-l commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What

Lower the RocksDB data-block size from 16KB to 4KB on the four execution-read-path column families:

  • ACCOUNT_TRIE_NODES / STORAGE_TRIE_NODES
  • ACCOUNT_FLATKEYVALUE / STORAGE_FLATKEYVALUE

Why

These CFs are pure exact-key point lookups (trie nodes and flat KV) hit on the hot execution read path; there are no range scans. With a 16KB block, every cold random get pulls a full 16KB block off disk to return a ~32-byte value — ~4x read amplification. A page-sized 4KB block minimizes per-get read + in-block search amplification.

set_block_size is a write-time option: it applies to newly flushed/compacted SSTs. Existing SSTs are read with whatever block size they were written at, so the benefit accrues as the DB rewrites (or after a regen/reblock of a snapshot).

Measurement

A/B on a cold, RAM-exceeding bloated state DB (sstore_bloated 10GB-class prestate, 300M-gas block), reblocking a copy of the same DB from 16KB to 4KB:

metric 16KB 4KB delta
Mgas/s 135.4 160.1 +18%
merkle drain 1305 ms 1104 ms -15%
disk read -36%

The win is read-amplification driven, so it shows up most on cold, larger-than-RAM state where reads actually hit the device.

Scope

Bloom filters (set_bloom_filter(10.0, false)) and memtable_prefix_bloom_ratio were already present on these CFs; this PR only changes the block size. No behavior change to other CFs (headers/bodies, codes, receipts, tx locations stay at their current sizes).

ACCOUNT_TRIE_NODES/STORAGE_TRIE_NODES and ACCOUNT_FLATKEYVALUE/
STORAGE_FLATKEYVALUE are pure exact-key point lookups on the execution
read path. Drop their block size from 16KB (scan-tuned) to a page-sized
4KB to cut per-get read+search amplification. Write-time option: applies
to newly flushed/compacted SSTs; existing SSTs are read as-is.
@edg-l edg-l requested a review from a team as a code owner June 30, 2026 15:33
@github-actions github-actions Bot added L1 Ethereum client performance Block execution throughput and performance in general labels Jun 30, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

This is a solid performance optimization with correct rationale.

Summary
The change reduces RocksDB block size from 16KB to 4KB for state column families (ACCOUNT_TRIE_NODES, FLAT_KV). This is appropriate because:

  1. Workload alignment: Ethereum state access is predominantly random point lookups (MPT traversals, SLOAD operations), not range scans. Smaller blocks minimize read amplification for exact-key fetches.
  2. Hardware alignment: 4KB matches standard OS page sizes, reducing I/O overhead.
  3. Backwards compatibility: As noted in the comments, this only affects newly flushed/compacted SSTs; existing data remains readable.

Minor Suggestions

  1. Line 193-196: The comment is excellent and explains the "why" clearly. Consider adding a brief note about the trade-off (slightly higher index memory footprint) for completeness, though this is minor for state data.

  2. Consistency check: Ensure configure_block_cache() (called on lines 197 and 210) properly accounts for the smaller block granularity. The block cache should ideally be sized in bytes regardless of block size, but verify it doesn't assume 16KB blocks elsewhere.

No blocking issues. This follows established RocksDB tuning practices for LSM-tree workloads heavy on point lookups (OLTP-style vs. analytics).


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

No findings.

This PR only retunes RocksDB block size for ACCOUNT_TRIE_NODES / STORAGE_TRIE_NODES and ACCOUNT_FLATKEYVALUE / STORAGE_FLATKEYVALUE in crates/storage/backend/rocksdb.rs and crates/storage/backend/rocksdb.rs. I don’t see any correctness, consensus, EVM, RLP, or memory-safety risk from the change itself; it is a storage-engine tuning knob.

Residual risk / testing gap: because this only affects newly written or compacted SSTs, the performance effect will be delayed and mixed on existing databases. I’d want benchmark evidence on a fresh sync or after forced compaction, plus block-cache hit/miss stats, before concluding the 4KB setting is a net win for these CFs.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR tunes RocksDB block sizing for hot execution state reads. The main changes are:

  • Trie-node column families now use 4KB data blocks.
  • Flat-KV column families now use 4KB data blocks.
  • Comments document that the option applies to newly written SSTs.

Confidence Score: 4/5

The changed flow looks mergeable after checking the cache impact of the smaller blocks.

  • The targeted read paths are point lookups, which matches the performance goal.
  • The smaller blocks can increase shared-cache metadata pressure on large state databases.
  • No correctness or security issue was found in the changed code.

crates/storage/backend/rocksdb.rs

Important Files Changed

Filename Overview
crates/storage/backend/rocksdb.rs Changes RocksDB block-based table options for trie-node and flat-KV column families from 16KB to 4KB blocks.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/storage/backend/rocksdb.rs:195
**Shared Cache Metadata Pressure**

With `cache_index_and_filter_blocks(true)`, the smaller 4KB blocks create about four times as many index entries for the large trie-node CFs, and those index blocks compete in the same shared block cache as data and filters for every CF. On large state DBs or smaller cache configurations, newly flushed or compacted SSTs can evict hot data blocks and slow block import or RPC reads even though the point lookup itself reads less data.

### Issue 2 of 2
crates/storage/backend/rocksdb.rs:210
**Flat KV Index Expansion**

`STORAGE_FLATKEYVALUE` is likely one of the largest state CFs, and 4KB blocks substantially increase its per-SST index footprint while sharing the same cache with other column families. After compaction rewrites this CF, cache space that previously held data blocks can be consumed by extra index blocks, causing slower reads outside the flat-KV path as well.

Reviews (1): Last reviewed commit: "perf(l1): use 4KB RocksDB blocks for tri..." | Re-trigger Greptile

block_opts.set_block_size(16 * 1024); // 16KB
// 4KB blocks: these CFs are pure exact-key point lookups
// (trie nodes / flat KV) on the execution read path, so a
// page-sized block minimizes per-get read+search amplification

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Shared Cache Metadata Pressure

With cache_index_and_filter_blocks(true), the smaller 4KB blocks create about four times as many index entries for the large trie-node CFs, and those index blocks compete in the same shared block cache as data and filters for every CF. On large state DBs or smaller cache configurations, newly flushed or compacted SSTs can evict hot data blocks and slow block import or RPC reads even though the point lookup itself reads less data.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/backend/rocksdb.rs
Line: 195

Comment:
**Shared Cache Metadata Pressure**

With `cache_index_and_filter_blocks(true)`, the smaller 4KB blocks create about four times as many index entries for the large trie-node CFs, and those index blocks compete in the same shared block cache as data and filters for every CF. On large state DBs or smaller cache configurations, newly flushed or compacted SSTs can evict hot data blocks and slow block import or RPC reads even though the point lookup itself reads less data.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@@ -203,7 +208,9 @@ impl RocksDBBackend {
cf_opts.set_memtable_prefix_bloom_ratio(0.2); // Bloom filter

let mut block_opts = BlockBasedOptions::default();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Flat KV Index Expansion

STORAGE_FLATKEYVALUE is likely one of the largest state CFs, and 4KB blocks substantially increase its per-SST index footprint while sharing the same cache with other column families. After compaction rewrites this CF, cache space that previously held data blocks can be consumed by extra index blocks, causing slower reads outside the flat-KV path as well.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/storage/backend/rocksdb.rs
Line: 210

Comment:
**Flat KV Index Expansion**

`STORAGE_FLATKEYVALUE` is likely one of the largest state CFs, and 4KB blocks substantially increase its per-SST index footprint while sharing the same cache with other column families. After compaction rewrites this CF, cache space that previously held data blocks can be consumed by extra index blocks, causing slower reads outside the flat-KV path as well.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have verified findings across all angles. Let me compose the final review.


PR Review: perf(l1): use 4KB RocksDB blocks for trie-node and flat-KV CFs

This is a single-file, 11-line performance change that reduces the RocksDB data-block size from 16KB to 4KB on the four CFs used in the hot execution read path (ACCOUNT_TRIE_NODES, STORAGE_TRIE_NODES, ACCOUNT_FLATKEYVALUE, STORAGE_FLATKEYVALUE). The benchmark evidence is solid (+18% Mgas/s, −36% disk reads on a cold, RAM-exceeding state DB), and the reasoning about random-key read-amplification is sound. Three issues survived verification, plus one cleanup finding.


Stale cache-floor documentation (CONFIRMED)

crates/storage/store.rs:82, cmd/ethrex/cli.rs:110–114, docs/CLI.md:190

All three locations cite ~8 GiB as the point where the bloom-filter/index working-set starts to thrash, described as measured on a synced mainnet node. That sweep was done under the old 16KB-block configuration. Moving four CFs to 4KB blocks multiplies the number of bloom-filter blocks for those CFs by ~4×, increasing the total filter footprint in the shared block cache. The 8 GiB floor is now an underestimate. Node operators who tuned --rocksdb.block-cache-size to exactly that threshold may find themselves below the new floor after a full compaction.

The fix is to either re-run the sweep (ideal) or qualify the claim — e.g., note that the figure was measured at the prior 16KB block sizes for the trie/FKV CFs and may need re-evaluation.


Migration seeding tool hardcodes conflicting block size (CONFIRMED)

tooling/migrations/src/bin/seed_migration_test.rs:144

block_opts.set_block_size(32 * 1024);

This line applies a uniform 32KB block size to every CF it opens, including the four CFs now configured at 4KB in the main backend. The tool does not call RocksDBBackend::open(); it manually constructs CF descriptors. Any SSTs written by the seeder will use 32KB blocks, but the main backend opens those same CFs expecting 4KB — a 8× divergence from the intent. Since RocksDB reads existing SSTs at their written block size, there is no crash, but the seeded DB does not reflect the production-4KB-block configuration, so benchmarks run on seeded data are measuring a mixed-era database, not the 4KB configuration.

This pre-dated the PR (the seeder was already 32KB vs 16KB), but the divergence triples here.


Comment claims CFs are "pure exact-key point lookups" — not fully accurate (PLAUSIBLE)

crates/storage/backend/rocksdb.rs:193–197

The new comment reads:

// 4KB blocks: these CFs are pure exact-key point lookups
// (trie nodes / flat KV) on the execution read path…

In practice, ACCOUNT_TRIE_NODES and STORAGE_TRIE_NODES are also traversed sequentially during:

  • FKV generation (store.rs:3344–3388): full DFS walk of the account and storage tries
  • Snap-sync serving (snap/server.rs:28, 68): iter_accounts_from / iter_storage_from
  • Post-sync validation (snap_sync.rs:912): parallel full trie walk

Each of these issues individual get_cf calls (not prefix_iterator_cf), so RocksDB does not perform a range scan, and since trie-node keys are random cryptographic hashes there is no spatial locality advantage to larger blocks anyway. The 4KB optimisation is still correct. The issue is purely with the comment's stated rationale; a reader auditing snap-sync or FKV generation performance will find this comment misleading.

Suggested fix: change "pure exact-key point lookups on the execution read path" to "point-lookup dominant; trie walks also occur but keys are random hashes so no locality advantage from larger blocks."


Both arms are now byte-for-byte identical (cleanup, CONFIRMED)

crates/storage/backend/rocksdb.rs:185–217

After this PR, every cf_opts and block_opts call in the ACCOUNT_TRIE_NODES | STORAGE_TRIE_NODES arm is identical to the ACCOUNT_FLATKEYVALUE | STORAGE_FLATKEYVALUE arm (same write-buffer sizes, bloom ratio, block size, bloom-filter bits, cache config). The second arm's own comment acknowledges this: "see ACCOUNT_TRIE_NODES above." If any parameter is changed in one arm in a future PR, silent divergence is the likely failure mode. Consider collapsing into one four-way arm:

ACCOUNT_TRIE_NODES | STORAGE_TRIE_NODES
| ACCOUNT_FLATKEYVALUE | STORAGE_FLATKEYVALUE => {}

If keeping them separate for future tuning flexibility is the intent, a brief // kept separate to allow independent tuning comment would clarify the decision.


Automated review by Claude (Anthropic) · sonnet · custom prompt

edg-l added 2 commits June 30, 2026 18:53
The cache-floor sweep cited in the block-cache-size docs was measured at
16KB blocks for the trie-node/flat-KV CFs. The 4KB change (#6940) raises
their index/filter block count ~4x, so the ~8GiB thrash floor is likely
now somewhat higher and warrants a re-sweep.
@github-actions

Copy link
Copy Markdown

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Stateless blockchain EF-tests (zkevm bundle) skipped under Amsterdam v6.1.0

make -C tooling/ef_tests/blockchain test-stateless runs against the
tests-zkevm@v0.4.1 fixture bundle — currently the only published zkevm test
release. Those fixtures were filled against an older glamsterdam devnet and
re-execute every case under the for_amsterdam fork, so they lag the
glamsterdam-devnet v6.1.0 gas accounting this client now implements
(EIP-8037 / EIP-8038 / EIP-2780 / EIP-7976 / EIP-7981 …). Re-executing them
yields ~2790/2864 stale-gas failures ("Transaction execution unexpectedly
failed"), spread pervasively across every fork and through the EIP-8025 proof
suite, so there is no clean passing subset to keep.

Until a v6.1.0-aligned zkevm bundle is published, the entire bundle is skipped
for the stateless run via the fork_Amsterdam entry in the stateless-only
EXTRA_SKIPS (tooling/ef_tests/blockchain/tests/all.rs) — every test in this
Amsterdam-only bundle carries the [fork_Amsterdam-…] parametrization in its
test key. The skip is #[cfg(feature = "stateless")], so it does not touch
the non-stateless test-levm run. Coverage of these EIPs is retained by
test-levm, the engine EF-tests, and the state EF-tests, all of which execute
against the live v6.1.0 fixtures and pass.

Re-enable by removing the "fork_Amsterdam" skip once .fixtures_url_zkevm
points at a zkevm bundle filled for glamsterdam-devnet v6.1.0.

@github-actions

Copy link
Copy Markdown

Lines of code report

Total lines added: 2
Total lines removed: 0
Total lines changed: 2

Detailed view
+--------------------------+-------+------+
| File                     | Lines | Diff |
+--------------------------+-------+------+
| ethrex/cmd/ethrex/cli.rs | 1269  | +2   |
+--------------------------+-------+------+

@github-actions

github-actions Bot commented Jun 30, 2026

Copy link
Copy Markdown

Benchmark Block Execution Results Comparison Against Main

Command Mean [s] Min [s] Max [s] Relative
base 61.481 ± 0.403 61.071 62.411 1.00 ± 0.01
head 61.334 ± 0.252 60.984 61.855 1.00

// vs the prior 16KB (scan-tuned). Write-time option: applies to
// newly flushed/compacted SSTs; existing SSTs are read as-is.
block_opts.set_block_size(4 * 1024); // 4KB
block_opts.set_bloom_filter(10.0, false); // 10 bits per key

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking follow-up thought: given the ~4x index/filter block-count growth on these two large CFs (already flagged in the block-cache-size doc), what do you think about pairing this later with partitioned filters (set_bloom_filter(10.0, true) + a two-level partitioned index)? That's the standard mitigation for exactly this pattern — small data block + large SST → filter/index memory blowup — because filter loads become finer-grained under cache eviction instead of the whole per-SST filter block being pulled in. The doc update on DEFAULT_ROCKSDB_BLOCK_CACHE_SIZE_BYTES covers this from the sizing side; partitioning would attack the same pressure from the CF-config side. Not for this PR, just checking if you'd already considered it.

@github-project-automation github-project-automation Bot moved this to In Review in ethrex_l1 Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client performance Block execution throughput and performance in general

Projects

Status: In Review
Status: Todo

Development

Successfully merging this pull request may close these issues.

4 participants