Compare commits

..

90 Commits

Author SHA1 Message Date
joshieDo
94d7607278 feat(storage): implement use_hashed_state mode for storage tables
When use_hashed_state is enabled:
- Skip PlainStorageState writes, use HashedStorages as canonical state
- StorageChangeSets contain hashed slots (keccak256)
- StoragesHistory uses hashed slots for indexing
- Static file changeset writer hashes slots
- Parallel write thread for PlainStorageState is skipped
- LatestStateProvider/HistoricalStateProvider read from HashedStorages
- take_state_above() writes to HashedStorages on reorg
- CLI db commands support --hashed flag
- Added IdentityKeyHasher for from_reverts() to skip re-hashing

This complements the configuration plumbing from PR #21997.
2026-02-09 21:06:51 +00:00
joshieDo
752d40291a chore: remove temporary debug eprintln from parallel writes 2026-02-09 18:55:02 +00:00
joshieDo
de3b17309e test: add invariant tests for active cursor check and DBI stale handling 2026-02-09 18:35:33 +00:00
joshieDo
8a8e4b5edd feat: add runtime cursor count check and documentation for parallel writes safety
- Add cursor_count tracking via AtomicUsize in TransactionPtr
- commit_subtxns() returns Error::Busy if cursors still active
- Add module-level docs explaining two-tier safety model
- Add test_commit_subtxns_fails_with_active_cursor invariant test
2026-02-09 18:31:39 +00:00
joshieDo
e14adaf79f fix: enforce parallel subtxn invariants at Rust layer
- Cross-DBI access now returns Error::Access when parallel writes enabled
- Parent commit returns Error::Busy if subtxns not committed first
- Split SubTransaction.committed into finished + committed flags
- Transaction Drop only aborts subtxns when last Arc reference
- Add 13 invariant tests covering all safety properties
2026-02-09 17:52:14 +00:00
joshieDo
ae09a5e036 chore: remove stray linkfix.patch 2026-02-09 16:44:20 +00:00
joshieDo
6ae2d53af2 refactor: trigger parallel writes by StorageSettings::is_v2() instead of edge feature flag
- Remove #[cfg(feature = "edge")] guards from parallel_writes module and metrics
- Make revm-state a required dependency
- Rename is_edge_mode() to is_v2()
- Tests no longer require --features edge
2026-02-09 16:42:46 +00:00
joshieDo
bd10b69257 fix: update StorageSettings::edge/legacy to v2/v1 after rebase 2026-02-09 16:19:23 +00:00
joshieDo
b7e190a544 Revert io_uring prefault experiments
Reverting commits:
- 4c708d6e17 perf(mdbx): fire-and-forget io_uring prefault (non-blocking)
- 6bb0ac111d feat(mdbx): per-subtxn io_uring prefault instead of batch
- f0882a5f8b fix(mdbx): fix io_uring error reporting and cap ring size
- dfe03a126f build(mdbx-sys): link liburing for io_uring batch prefault
- 11f2efb2a2 feat(mdbx): io_uring batch prefault for parallel subtxns

Reason: Fire-and-forget pwrite is unsafe - async write can complete
after cursor_put, overwriting real data with pattern. Also, analysis
showed the bottleneck is B-tree READ seeks, not write prefaulting.

Amp-Thread-ID: https://ampcode.com/threads/T-019c2f31-1772-729b-8560-e02485715ed4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:11:21 +00:00
joshieDo
f460459b59 perf(mdbx): fire-and-forget io_uring prefault (non-blocking)
Don't wait for io_uring completions - submit and continue immediately.
Pages will be resident by time cursor_put needs them, or soft fault handles it.
Overlaps prefault I/O with actual cursor work.
2026-02-09 16:11:21 +00:00
joshieDo
fdfb8f8694 feat(mdbx): per-subtxn io_uring prefault instead of batch
- Each subtxn now prefaults its OWN arena via io_uring in parallel
- 7 subtxn threads = 7 io_uring rings = true parallel I/O
- Prefault called at start of each write_*_only method
- Removes blocking batch prefault from mdbx_txn_create_subtxns
- Soft fail on io_uring init (optimization, not required)

Before: main thread prefaults all 30K pages serially, then spawns threads
After: each thread prefaults its ~4K pages in parallel with siblings
2026-02-09 16:11:21 +00:00
joshieDo
65a3033b87 fix(mdbx): fix io_uring error reporting and cap ring size
- Use strerror(-ret) instead of strerror(errno) for liburing errors
- Cap ring size at 32768 to avoid kernel limits
- Add debug logging for batch prefault
2026-02-09 16:11:21 +00:00
joshieDo
c2e1679934 build(mdbx-sys): link liburing for io_uring batch prefault
Amp-Thread-ID: https://ampcode.com/threads/T-019c2f31-1772-729b-8560-e02485715ed4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:11:21 +00:00
joshieDo
9d552797b5 feat(mdbx): io_uring batch prefault for parallel subtxns
- Add io_uring support detection (MDBX_HAVE_IO_URING) for Linux
- Implement subtxn_batch_prefault_arena() to prefault all arena pages
  at subtxn creation using io_uring batch writes
- Single io_uring_submit() for N scattered pages vs N pwrite() syscalls
- Add subtxn_arena_prefaulted flag to skip redundant mincore checks
- Skip mincore_probe() in page_alloc_finalize for batch-prefaulted pages
- Crash if io_uring unavailable (no fallback path)

Performance: eliminates per-page mincore syscalls for initial arena,
front-loads all prefaulting before parallel work begins.

Amp-Thread-ID: https://ampcode.com/threads/T-019c2f31-1772-729b-8560-e02485715ed4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:11:21 +00:00
joshieDo
a5da200c49 Revert "feat(db): add replace_current API for DUPSORT cursor optimization"
This reverts commit 6ff4353e99.
2026-02-09 16:11:21 +00:00
joshieDo
21a6f1ab60 feat(db): add replace_current API for DUPSORT cursor optimization
Add replace_current method to DbDupCursorRW trait that uses MDBX_CURRENT
flag for in-place updates. This reduces StoragesTrie writes from 3 ops
(seek→delete→upsert) to 2 ops (seek→replace_current).

For same-size values, MDBX updates in-place (fast path). For different
sizes, MDBX internally performs delete+insert.

- Add CursorReplaceCurrent to DatabaseWriteOperation enum
- Add CursorReplaceCurrent to metrics Operation enum
- Implement replace_current in MDBX cursor using WriteFlags::CURRENT
- Update write_storage_trie_updates_sorted to use replace_current
- Add test verifying replace_current updates without creating duplicates
2026-02-09 16:11:21 +00:00
joshieDo
afc718b7d9 Revert "perf(mdbx): disable prefault for parallel subtxns"
This reverts commit 230593ac05.
2026-02-09 16:11:21 +00:00
joshieDo
10baffc181 Revert "feat(mdbx): batched madvise prefault at arena creation"
This reverts commit 2db2872cab.
2026-02-09 16:11:21 +00:00
joshieDo
66713e22ad feat(mdbx): batched madvise prefault at arena creation
Add async prefault for subtxn arena pages during creation:
- Batches contiguous pages into single madvise(MADV_WILLNEED) calls
- O(n) single pass through already-sorted descending PNL
- Kernel gets head start fetching pages before write threads need them
- Expected cost: 0.03-3ms depending on page clustering

This combines with disabled mincore (230593ac05) to eliminate both:
- mincore syscall overhead (was 47% of storage_trie time)
- Hard page fault blocking (was 61% after disabling mincore)

Now we get soft async prefetch with minimal syscall overhead.
2026-02-09 16:11:21 +00:00
joshieDo
549e3214d5 perf(mdbx): disable prefault for parallel subtxns
Pages allocated from GC are likely still hot in page cache.
mincore() syscall overhead dominated ~47% of storage trie write time.

Empirical test - revert if page faults increase significantly.
2026-02-09 16:11:21 +00:00
joshieDo
21a5ace5d9 perf(edge): arena tuning, trie op metrics, mutex optimization
- Bump arena ratios: StoragesTrie 1.74→2.2, HashedStorages 1.96→2.2, PlainStorageState 1.80→2.0
- Add StorageTrieOpCounts tracking seek/delete/upsert counts per batch
- Reduce mutex scope in subtxn commit: move diagnostics outside lock
- Add ArenaUsageTracker for P95-based dynamic arena floors
- New metrics: edge_storage_trie_{seek,delete,upsert}_count
2026-02-09 16:11:21 +00:00
joshieDo
89544f6727 perf(mdbx): defer pnl_sort to last subtxn commit
- Only sort parent's repnl when the last subtxn commits/aborts
- Saves O(n log n) × 6 sorting overhead for 7 parallel subtxns
- Add test documenting DUPSORT upsert appends (not replaces) behavior
- Update duplicate check to work on unsorted lists (O(n²) debug only)
2026-02-09 16:11:21 +00:00
joshieDo
fe2f4f759d feat(arena): use floating-point ratios for precise 20% headroom
Updated ratios based on actual observed usage (hint - unused = used):
- PlainAccountState: 1.47 × 1.2 = 1.76
- PlainStorageState: 1.50 × 1.2 = 1.80
- HashedAccounts: 1.55 × 1.2 = 1.86
- HashedStorages: 1.63 × 1.2 = 1.96
- AccountsTrie: 0.94 × 1.2 = 1.13
- StoragesTrie: 1.45 × 1.2 = 1.74
- Bytecodes: 1.67 × 1.2 = 2.00

This targets ~20% unused pages per batch.
2026-02-09 16:11:21 +00:00
joshieDo
d967866e9b feat(arena): use floating-point ratios for precise 20% headroom
Changed from integer ratios to floating-point for tighter arena estimation:
- PlainAccountState: 1.93 × 1.2 = 2.32
- PlainStorageState: 2.00 × 1.2 = 2.40
- HashedAccounts: 2.03 × 1.2 = 2.44
- HashedStorages: 2.31 × 1.2 = 2.77
- AccountsTrie: 1.23 × 1.2 = 1.48
- StoragesTrie: 1.96 × 1.2 = 2.35
- Bytecodes: 1.67 × 1.2 = 2.00

This reduces over-allocation from ~35% to ~20% unused pages per batch.
2026-02-09 16:11:21 +00:00
joshieDo
3c3fef6b5f feat(arena): update estimation formulas from production metrics
Updated pages-per-entry ratios based on observed data (dev-joshie):
- PlainAccountState: 3 pages/account (observed 2.63)
- PlainStorageState: 3 pages/slot (observed 3.03)
- HashedAccounts: 3 pages/account (observed 2.87)
- HashedStorages: 4 pages/slot (observed 3.31)
- AccountsTrie: 2 pages/node (observed 1.59)
- StoragesTrie: 3 pages/node (observed 2.87)
- Bytecodes: 2 pages/contract (observed 1.67)

Simplified calc_linear_with_floor to use direct multiplication instead
of division-based estimation. Floors remain as safety nets.
2026-02-09 16:11:21 +00:00
joshieDo
3d0cc1dc01 fix(mdbx): compute pages_unused at stats retrieval time
Previously pages_unused was always 0 because it was calculated during
commit(), but stats were retrieved before commit (commit invalidates
the subtxn pointer).

Fix: compute unused pages inline from current arena size (subtxn_repnl)
in mdbx_subtxn_get_stats() instead of reading the pre-computed field.
2026-02-09 16:11:21 +00:00
joshieDo
c0ed20e44b feat(metrics): expose arena estimation inputs for correlation analysis
Added/renamed gauges to track per-batch estimation inputs:
- database_edge_input_num_accounts
- database_edge_input_num_storage
- database_edge_input_num_contracts
- database_edge_input_num_account_trie_nodes
- database_edge_input_num_storage_trie_nodes
- database_edge_input_num_storage_trie_addresses

These allow correlating actual inputs with page demand to validate
and improve the estimation formulas.
2026-02-09 16:11:21 +00:00
joshieDo
41d90c3c11 feat(arena): increase hints to 95% coverage and add per-batch histograms
Updated floor values based on observed production data:
- StoragesTrie: 5500 → 8800 (was 59% coverage, now 95%)
- AccountsTrie: 5000 → 7300 (was 65% coverage, now 95%)
- HashedStorages: 2600 → 4000
- PlainStorageState: 2400 → 3600
- HashedAccounts: 2200 → 3400
- PlainAccountState: 2000 → 3000

Removed MAX_ARENA_PAGES cap entirely.

Added histogram metrics for per-batch visibility:
- arena_refills_per_batch: refill events per subtxn (shows p50/p99)
- pages_unused_per_batch: unused pages per subtxn (detects over-allocation)

Expected improvement: ~75% reduction in mutex acquisitions per batch
(from ~370 refills/batch to ~90 refills/batch)
2026-02-09 16:11:21 +00:00
joshieDo
63f0b36c49 feat(arena): improve hint estimation with trie-aware calculation
Updated default hints based on observed production data:
- StoragesTrie: 5000 → 5500 (covers ~80% of observed 6843/batch)
- HashedStorages: 2500 → 2600 (covers ~82% of observed 3166/batch)
- HashedAccounts: 2000 → 2200 (covers ~81% of observed 2718/batch)
- MAX_ARENA_PAGES: 6000 → 7000

Improved estimation algorithm:
- Added separate calc_trie_with_floor() that uses multiplication
  instead of division for trie tables (write amplification model)
- AccountsTrie: 3 pages per node (tree depth + splits)
- StoragesTrie: 4 pages per node (dupsort overhead)
- Refined data table constants (30 entries/page for storage)

Previously trie estimation always hit the floor because division
underestimated the page requirements. Now estimation scales with
actual trie node counts.
2026-02-09 16:11:21 +00:00
joshieDo
d826180b5f fix(metrics): wire up arena_hint_estimated/actual to DB-level metrics
The provider-level metrics were being recorded but the DB-level
metrics (arena_hint_estimated, arena_hint_actual) were always 0
because record_arena_estimation() was never called.

Added calls to tx.record_arena_estimation() for each table to
populate the DB-level metrics correctly.
2026-02-09 16:11:21 +00:00
joshieDo
6d0e37f4fb feat(metrics): rename arena metrics for clarity and add hint estimation tracking
Renamed metrics for clarity:
- arena_hits → arena_page_allocations (pages allocated from arena)
- arena_misses → arena_refill_events (times arena ran dry)
- pages_distributed → arena_initial_pages (pages given at creation)
- pages_acquired → arena_refill_pages (pages fetched during refill)
- Removed redundant fallback_count (same as arena_refill_events)

Added hint estimation metrics:
- arena_hint_estimated: raw calculated estimate before floor/cap
- arena_hint_actual: final value used after floor/cap
- arena_hint_source: 0=estimated, 1=floored, 2=capped
- arena_hint_floored_total: counter for floored events
- arena_hint_capped_total: counter for capped events

Updated arena hint defaults based on observed production usage:
- StoragesTrie: 310 → 5000
- AccountsTrie: 512 → 5000
- HashedStorages: 64 → 2500
- PlainStorageState: 64 → 2500
- HashedAccounts: 66 → 2000
- PlainAccountState: 66 → 2000
- MAX_ARENA_PAGES: 512 → 6000

Amp-Thread-ID: https://ampcode.com/threads/T-019c2da1-f017-7448-b7ae-6ac153f8969c
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:11:21 +00:00
joshieDo
76f884e47d feat(mdbx): try GC refill via ALLOC_RESERVE before EOF extension
When subtxn fallback finds parent's repnl empty, attempt to pull pages
from freeDB using gc_alloc_ex with ALLOC_RESERVE before falling back
to EOF extension. This enables GC page reuse during steady-state.
2026-02-09 16:11:21 +00:00
joshieDo
4cc1df513a feat(metrics): add pages_from_gc and pages_from_eof tracking
Distinguishes page sources during fallback:
- pages_from_gc: pages acquired from parent's repnl (GC/freeDB)
- pages_from_eof: pages acquired via EOF extension (fallocate)

Also adjusts arena hints for better hit rates based on observed metrics.
2026-02-09 16:11:21 +00:00
joshieDo
94e23e4ad2 fix(mdbx): prevent double-commit of parallel subtxns
Use atomic swap instead of load to set parallel_writes_enabled=false
after committing subtxns. This prevents the second commit attempt in
Tx::commit() from trying to access already-freed subtxn pointers.
2026-02-09 16:11:21 +00:00
joshieDo
c0e2ce10a9 feat(metrics): expose pages_acquired and arena_hint in EdgeArenaMetrics
- Add pages_acquired counter to track pages fetched during fallback
- Add arena_hint gauge to verify arena sizing matches workload
- Both fields already existed in SubTransactionStats, now exposed to Prometheus
2026-02-09 16:11:21 +00:00
joshieDo
cd36789d7e feat(metrics): add Prometheus metrics for parallel subtxn arena stats
Added Prometheus metrics to track parallel write page allocation efficiency:

Metrics (per-table labels):
- reth_database_edge_arena_hits: pages allocated from pre-distributed arena (fast path)
- reth_database_edge_arena_misses: times fallback to parent was needed (slow path)
- reth_database_edge_pages_distributed: pages initially distributed to subtxn
- reth_database_edge_pages_unused: pages returned to parent on commit
- reth_database_edge_fallback_count: number of fallback triggers

These metrics help tune arena hints:
- High arena_misses → hints are too low, need more pages upfront
- High pages_unused → hints are too high, wasting pre-allocation
- High fallback_count → contention on parent, need better distribution

Changes:
- Added EdgeArenaMetrics struct to db/metrics.rs
- Added record_edge_arena_stats() to DatabaseEnvMetrics
- Added commit_subtxns_with_stats() to Transaction<RW>
- Updated provider to record metrics after subtxn commits
2026-02-09 16:11:21 +00:00
joshieDo
acce33340b feat(mdbx): add parallel subtxn arena metrics
Added metrics to track parallel subtransaction page allocation efficiency:

C-level (mdbx.c):
- subtxn_arena_hits: pages allocated from pre-distributed subtxn_repnl
- subtxn_arena_misses: times fallback to parent was needed
- subtxn_pages_distributed: initial pages distributed based on arena hints
- subtxn_pages_acquired: additional pages acquired during fallbacks
- subtxn_pages_unused: pages returned to parent on commit
- subtxn_fallback_count: number of fallback requests to parent
- subtxn_arena_hint: original arena hint for comparison

FFI:
- MDBX_subtxn_stats struct and mdbx_subtxn_get_stats() function

Rust:
- SubTransactionStats struct with all metrics
- SubTransaction::get_stats() method to retrieve metrics

DEBUG logging added for each subtxn commit showing:
- DISTRIBUTION: dbi, hint, distributed, acquired
- FINAL STATS: all metrics for tuning arena hints
2026-02-09 16:11:21 +00:00
joshieDo
478d8f7845 debug(mdbx): add comprehensive page accounting and GC diagnostics
Added DEBUG logging to track:
- Duplicate page detection in subtxn commit (cross-list checks)
- Page accounting: distributed/acquired/consumed/returned counts
- Oldest/detent values at parallel subtxn creation
- GC record skipping due to detent threshold
- Page list sizes at gc_update entry

This helps diagnose freelist growth issues by showing:
1. If pages are being double-counted
2. If GC reclaim is blocked by stale oldest reader
3. The page flow through subtxn lifecycle
2026-02-09 16:11:20 +00:00
joshieDo
606d6a2f37 fix(mdbx): fix parallel subtxn race conditions and page leaks
Three critical fixes for parallel subtransaction implementation:

1. Fix shadow_reserve race condition causing branch_nodemax panic
   - page_shadow_alloc/release accessed env->shadow_reserve without sync
   - When parallel subtxns on different threads both performed page_split,
     they could get the same page buffer causing data corruption
   - Fix: bypass shared pool for parallel subtxns, allocate directly via
     osal_malloc with SIZE_MAX marker for identification

2. Fix page leak in mdbx_subtx_abort
   - When subtxn aborted, pages from subtxn_repnl, loose_pages, and repnl
     were simply freed (the PNL itself) but page numbers were lost forever
   - This caused unbounded freelist growth
   - Fix: return all pages to parent->tw.repnl before freeing resources

3. Add mutex protection for mdbx_subtx_commit
   - Commit modified parent state (dirty page count, page lists, DBI metadata)
     without holding any mutex
   - Concurrent sibling commits could cause lost updates
   - Fix: hold subtxn_alloc_mutex during entire commit operation

Also added DEBUG logging for GC reclaim debugging.
2026-02-09 16:11:20 +00:00
joshieDo
cc772c4170 fix: loop GC reclaim and clear txn_gc_drained flag in subtxn fallback
The previous fix only called gc_alloc_ex once, which reads just one GC
record. Now we loop up to 16 times until we have enough pages or GC is
truly depleted.

Key changes:
- Loop gc_alloc_ex until we have num + batch pages
- Clear txn_gc_drained flag before each attempt (was blocking reclaim)
- Removed the txn_gc_drained check that was skipping GC entirely
- Also refill subtxn_repnl while holding the lock to reduce future contention

This should prevent the unbounded freelist growth (96M -> 104M) that was
still happening because we weren't actually reclaiming from GC.
2026-02-09 16:11:20 +00:00
joshieDo
8c6944897f fix: add arena_hint field to MDBX_subtxn_spec_t in test 2026-02-09 16:11:20 +00:00
joshieDo
88fb4a0a4e fix: subtxn fallback now tries GC reclaim before EOF extension
When parallel subtxns exhaust their subtxn_repnl, they now:
1. Take mutex
2. Try parent's repnl first
3. If empty, trigger gc_alloc_ex(ALLOC_RESERVE) to reclaim from GC
4. Only fall back to EOF extension if GC is truly depleted

This fixes the unbounded freelist growth issue where pages were
being retired but never reused.

Also increased MDBX_SUBTXN_PAGE_BATCH from 4 to 32 pages to reduce
mutex contention.

Added test: test_parallel_subtxn_freelist_reuse verifies freelist
stabilizes instead of growing unboundedly.

Amp-Thread-ID: https://ampcode.com/threads/T-019c2b45-6f11-70bd-abae-c80ed02149e6
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:11:20 +00:00
joshieDo
846556af52 feat(mdbx): pre-warm GC before creating parallel subtxns
When creating parallel subtransactions, the parent's repnl (reclaimed
page list) is typically empty because no GC has been triggered yet.
This means our proportional freelist distribution was useless - all
subtxns would immediately fall back to mutex-protected parent allocation.

This change adds GC pre-warming in mdbx_txn_create_subtxns:
1. Calculate total pages needed from arena_hints
2. If repnl + loose_count < total_hints, trigger GC to populate repnl
3. Call gc_alloc_ex with ALLOC_RESERVE to pull pages from freelist
4. Loop until we have enough pages or GC is depleted (max 64 iterations)
5. Re-count gc_available after pre-warming
6. Then distribute proportionally as before

This ensures subtxns actually have freelist pages to draw from without
hitting the mutex on every allocation.
2026-02-09 16:11:20 +00:00
joshieDo
7ce2b55415 feat(edge): proportional per-DBI arena allocation for parallel writes
- Add arena_hint field to MDBX_subtxn_spec_t for per-DBI page estimation
- Distribute freelist pages proportionally based on arena hints instead of equally
- Add ArenaHints struct to estimate pages needed from state data size
- Larger tables (StoragesTrie, AccountsTrie) get more freelist pages upfront
- Reduces mutex contention during parallel writes by better pre-allocation
- Falls back to equal distribution when no hints provided (backward compatible)

Arena sizing is based on:
- Number of account/storage changes
- Number of trie node updates
- Capped at 512 pages max per DBI to prevent memory bloat
- Minimum 8 pages per DBI

Amp-Thread-ID: https://ampcode.com/threads/T-019c2b02-8e81-773c-97a9-628824f9459d
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:11:20 +00:00
joshieDo
751af221fa fix(grafana): show last and mean in legend calcs for p50/p90/p99 2026-02-09 16:11:20 +00:00
joshieDo
23c1ca137d feat(grafana): update edge mode dashboard to use quantiles
- Add quantile template variable (defaults to 0.5 for p50)
- Update all main panel queries to use avg(metric{quantile=$quantile})
- Keep _last versions in comparison panels for real-time view
- Matches pattern from reth-persistence.json dashboard
2026-02-09 16:11:20 +00:00
joshieDo
c65f282907 fix: propagate edge feature to reth-provider in cli/commands
The edge feature was not propagating to reth-provider from cli/commands,
causing #[cfg(feature = "edge")] parallel writes code to be compiled out.

Added reth-provider/edge to the edge feature list.

Also added debug eprintln to verify parallel path is entered.
2026-02-09 16:11:20 +00:00
joshieDo
82f82f4515 fix: gate edge-only code with #[cfg(feature = "edge")]
- Gate EdgeWriteTimings struct and record_edge_writes method
- Gate Action enum edge variants
- Gate parallel_writes module export
- Gate use_parallel_writes variable (defaults to false without edge feature)
- Split if-else to two if blocks for cfg compatibility

Stress test: 4GB written (1M pages), 11 iterations, NO CORRUPTION
2026-02-09 16:11:01 +00:00
joshieDo
8d9ade2b1d feat(edge): fine-grained 7-thread parallel writes + metrics + Grafana dashboard
- Add parallel_writes.rs with single-DBI write methods (1 DBI = 1 subtxn = 1 thread)
- Upgrade save_blocks edge mode from 3 to 7 parallel threads:
  - PlainAccountState, Bytecodes, PlainStorageState
  - HashedAccounts, HashedStorages
  - AccountsTrie, StoragesTrie
- Add preprocessing phase timing (merge, sort, convert before spawning)
- Add per-table edge mode metrics with histograms + gauges
- Add Grafana dashboard: etc/grafana/dashboards/reth-edge-mode.json
- Add 'edge' feature flag for gating edge mode tests

Stress test results (60s, 1000 accounts, 200 slots):
- 21 iterations, 105 blocks, 21M storage entries
- 26,589 pages allocated (~104 MB)
- Read verification passed, NO CORRUPTION DETECTED
2026-02-09 16:11:01 +00:00
joshieDo
dd6e3932f2 perf(libmdbx): remove all debug fprintf/eprintln output
Remove all debug tracing that was added during parallel subtxn development:
- 122 fprintf(stderr, ...) calls from mdbx.c
- eprintln!/println! from cursor.rs, transaction.rs
- Debug output from db/cursor.rs, db/tx.rs

This debug output was causing significant performance overhead and
cluttering logs in production.

Also adds read verification to edge mode tests:
- test_save_blocks_edge_mode_parallel: verifies accounts + storage after commit
- test_save_blocks_edge_mode_stress_unique: samples accounts across iterations
- test_save_blocks_edge_mode_stress_5min: periodic verification every 10 iterations
2026-02-09 16:10:43 +00:00
joshieDo
a7eaad001b fix(libmdbx): fix pnl_check validations for parallel subtxns
Multiple fixes for assertion failures in page allocation:

1. Sort parent's repnl after merging from subtxns - pnl_check assumes
   sorted list, but pnl_append_prereserved doesn't maintain order

2. Replace all pnl_check_allocated assertions to use txn_first_unallocated()
   helper instead of txn->geo.first_unallocated. For parallel subtxns, the
   local geo.first_unallocated is stale (copied at creation time), while
   the parent's value is updated during allocation.

3. Allocate extra 'refund boundary' page in batch allocation to ensure
   highest page in subtxn_repnl is < first_unallocated - MDBX_ENABLE_REFUND

4. Add debug output before assertion to diagnose future failures

Also added 5-minute stress test (test_save_blocks_edge_mode_stress_5min)
that hammers parallel subtxn writes continuously. Configurable via env vars:
- STRESS_DURATION_SECS (default 300)
- STRESS_ACCOUNTS (default 200)
- STRESS_STORAGE_SLOTS (default 50)
- STRESS_BLOCKS (default 5)
2026-02-09 16:10:43 +00:00
joshieDo
670df03a47 fix(libmdbx): block parent writes while parallel subtxns are active
Enforce the invariant that parent transaction cannot write while it has
active parallel subtxns. This prevents race conditions on shared MDBX
state (geo.first_unallocated, MAIN_DBI, etc.) that could cause database
corruption.

Added checks in check_txn_rw() and cursor_check_rw() to return MDBX_BUSY
if txn->tw.subtxn_list != NULL. This catches all 9 write entry points:
- mdbx_cursor_put, mdbx_cursor_del
- mdbx_put, mdbx_del, mdbx_drop
- mdbx_replace, mdbx_canary_put
- mdbx_dbi_rename2, mdbx_dbi_sequence

The current provider code is already safe (parent does no writes during
subtxn window), but this guard prevents future bugs and makes the
invariant explicit.
2026-02-09 16:10:43 +00:00
joshieDo
8dc8d94a02 fix(libmdbx): parent must use subtxn_alloc_mutex when it has active subtxns
Critical fix for database corruption. When parent transaction allocates
pages while parallel subtxns are active, it must use the shared
subtxn_alloc_mutex to prevent race conditions on geo.first_unallocated.

Without this fix, parent and subtxns could allocate the same page:
1. Subtxn reads first_unallocated=1000, acquires mutex
2. Parent (no mutex) also reads first_unallocated=1000
3. Subtxn updates to 1004, releases mutex
4. Parent updates to 1001
5. Both think they own page 1000 -> CORRUPTION

Fix: In gc_alloc_ex 'done:' label, check if txn->tw.subtxn_list is
non-null and use the mutex for synchronized EOF allocation.
2026-02-09 16:10:43 +00:00
joshieDo
2914667f1a fix(libmdbx): fix page validity checks for parallel subtxns
Critical fix for production crash where parallel subtxn failed with
'requested page not found' (-30797) when accessing pages allocated
by sibling subtxns.

Root causes fixed:
1. page_get_inline used txn->parent instead of txn->tw.subparent
   to read first_unallocated for parallel subtxns
2. Multiple page validation checks used stale txn->geo.first_unallocated
   instead of reading from parent via txn_first_unallocated() helper
3. DBI_STALE DBIs could trigger tbl_fetch which traverses MAIN_DBI,
   violating the 1 DBI = 1 SUBTXN isolation invariant

Fixes applied:
- page_get: use txn->tw.subparent->geo.first_unallocated
- check_page (2 locations): use txn_first_unallocated() helper
- check_page_header: use txn_first_unallocated() helper
- tree_search: use txn_first_unallocated() helper
- create_subtxn_with_dbi: pre-fetch stale DBIs on parent before
  creating subtxn to ensure DBI state is fresh
- tbl_fetch: add defensive check to block MAIN_DBI access from
  parallel subtxns (returns MDBX_EINVAL)
2026-02-09 16:10:43 +00:00
joshieDo
6784d2d3ab fix(libmdbx): merge unused subtxn_repnl pages back to parent on commit
Fixed page leak where batch pre-allocated pages in subtxn_repnl were
freed without returning unused pages to parent. Now merges remaining
pages to parent->tw.repnl before freeing.

Also reduced MDBX_SUBTXN_PAGE_BATCH from 64 to 4 to minimize waste
when subtxns don't need many pages.
2026-02-09 16:10:43 +00:00
joshieDo
5dd48ba5f0 perf(libmdbx): optimize parallel subtxn synchronization
Four key optimizations to reduce overhead of parallel subtransactions:

1. Remove O(n) sibling sync loop: Parent's geo.first_unallocated is now
   the single source of truth. Subtxns read from parent instead of
   maintaining cached copies that needed O(n) propagation under mutex.
   Added txn_first_unallocated() and txn_geo_now() helper functions.

2. TLS page_auxbuf cache: Instead of malloc/free 12KB per subtxn, cache
   one buffer per thread in TLS. With 7 subtxns per block, eliminates
   84KB allocation churn. Added page_auxbuf_acquire/release helpers.

3. Batch page pre-allocation: When subtxn exhausts its pre-claimed pages,
   allocate MDBX_SUBTXN_PAGE_BATCH (64) pages at once instead of 1.
   Extra pages stored in subtxn_repnl for future use, reducing mutex
   acquisition frequency from O(pages) to O(pages/64).

4. Doubly-linked subtxn list: Added subtxn_prev pointer for O(1) unlink
   during commit instead of O(n) linear search through sibling list.

Performance impact:
- Page allocation critical section: O(n) -> O(1)
- Memory allocation per subtxn: malloc+free -> TLS cache hit
- Mutex acquisitions: O(pages) -> O(pages/64)
- Commit unlink: O(siblings) -> O(1)
2026-02-09 16:10:43 +00:00
joshieDo
75f9aba1ad fix(libmdbx): merge loose pages from parallel subtxns to parent
Parallel subtxns in WRITEMAP mode can accumulate loose pages when
deleting entries. These pages were previously leaked until parent
commit.

Fix:
- On subtxn commit: merge loose_pages to parent's repnl for reuse
- On subtxn abort: clean up loose_pages in subtx_free_resources
2026-02-09 16:10:43 +00:00
joshieDo
a314e615ea docs(libmdbx): document all parallel subtxn invariants
Add comprehensive documentation for parallel subtxn contract:
- NO READ-THROUGH: subtxns see parent's state at creation time only
- NO CROSS-DBI: enforced by cursor_bind
- SERIAL COMMIT: subtxns must commit before parent
- DISJOINT PAGES: each DBI has separate B-tree

Add THREAD SAFETY section explaining:
- Concurrent execution on different threads
- Mutex-protected page allocation
- Per-subtxn page_auxbuf for DupSort isolation
2026-02-09 16:10:43 +00:00
joshieDo
fa9379052b docs(libmdbx): document parallel subtxn changes and invariants
Add documentation comments explaining:
- Per-txn page_auxbuf for thread-safe DupSort operations
- 1 DBI = 1 SUBTXN invariant for isolation
- geo.first_unallocated synchronization across siblings
- Parallel subtxn creation/commit semantics
- DEBUG timing logs are zero-cost in release builds
2026-02-09 16:10:43 +00:00
joshieDo
aa71ba6621 fix(libmdbx): allocate page_auxbuf for parallel subtxns
Parallel subtxns were setting tw.txn_page_auxbuf but cursor_put checks
tw.page_auxbuf, causing all parallel subtxns to fall back to shared
env->page_auxbuf - a data race for DupSort operations.

Now each parallel subtxn allocates its own tw.page_auxbuf scratch buffer,
matching the behavior of regular nested txns.
2026-02-09 16:10:43 +00:00
joshieDo
80497b5e38 test(libmdbx): add write/read verification to DupSort tests 2026-02-09 16:10:43 +00:00
joshieDo
cc3cd92bb2 fix(libmdbx): fix DEBUG macro calls to use format args 2026-02-09 16:10:43 +00:00
joshieDo
992e22230d fix(libmdbx): per-subtxn page_auxbuf for thread-safe DupSort operations
- Add page_auxbuf field to MDBX_txn.tw struct for nested transactions
- Allocate per-subtxn page_auxbuf on nested txn begin, free on txn end
- Basal transactions continue using shared env->page_auxbuf
- cursor_put uses txn->tw.page_auxbuf when available, falls back to env->page_auxbuf
- Add DEBUG logs for subtxn timing (begin/commit phases)
- Add DupSort stress tests (dupsort_parallel.rs)

This fixes MDBX_PAGE_FULL errors on HashedStorages when parallel subtxns
are used, as page_auxbuf was previously shared and could be corrupted
by concurrent DupSort operations.

Amp-Thread-ID: https://ampcode.com/threads/T-019c29d0-e48e-7073-95e6-478bd8e2e607
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:10:43 +00:00
joshieDo
717aa92b8d fix: add NULL checks for iov_base in decode path
- Check for NULL iov_base in cursor.rs parallel get path
- Check for NULL iov_base in codec.rs decode_val implementations
- Prevents undefined behavior from slice::from_raw_parts with NULL
2026-02-09 16:10:43 +00:00
joshieDo
e5355bfbef fix: allocate retired_pages and repnl for parallel subtxns
- Allocate retired_pages and repnl lists in create_subtxn_with_dbi
  instead of setting them to nullptr (fixes NULL pointer issues)
- Add proper cleanup on allocation failure
- Merge retired_pages and repnl from subtxn into parent on commit
- Abort path already handled by subtx_free_resources
2026-02-09 16:10:43 +00:00
joshieDo
c60a9af94b test: add stress tests for parallel subtxn edge mode
- test_save_blocks_edge_mode_stress: 20 iterations with overlapping accounts
- test_save_blocks_edge_mode_stress_unique: 20 iterations with unique accounts
- test_parallel_subtx_stress: 50 iterations of libmdbx parallel writes
2026-02-09 16:10:43 +00:00
joshieDo
6ad0565b0f debug: add tracing after tree_search in parallel cursor path
Track what happens AFTER cursor_bind returns:
1. After mdbx_cursor_open in new_with_ptr
2. Before/after mdbx_cursor_get in parallel path with decode steps
3. Before/after mdbx_cursor_put with key/data sizes
4. Before/after mdbx_cursor_del
2026-02-09 16:10:43 +00:00
joshieDo
fc59562d46 test: expand test_save_blocks_edge_mode_parallel with large data
- Write 500 accounts with 100 storage slots each to force page allocations
- Assert that new pages were allocated (verifies parallel subtxn allocation works)
- Test now allocates ~99 new pages during parallel writes
2026-02-09 16:10:43 +00:00
joshieDo
0df85f0029 fix: add sibling sync to gc_alloc_ex for multi-page allocations
Sync all sibling subtxns' geo.first_unallocated after updating parent
and current subtxn in the multi-page allocation path (gc_alloc_ex).
This prevents stale geo values that could cause page allocation races
between parallel subtransactions.
2026-02-09 16:10:43 +00:00
joshieDo
a693857608 fix: sync geo.first_unallocated across sibling subtxns
When subtxn A allocates pages from parent, sibling subtxn B's
geo.first_unallocated becomes stale. This adds a loop to sync
the updated value to all siblings under the already-held
subtxn_alloc_mutex.
2026-02-09 16:10:43 +00:00
joshieDo
6c685d0ed3 debug: add more tree_search debug output 2026-02-09 16:10:43 +00:00
joshieDo
04bd81f74d fix(mdbx): handle NULL repnl in txn_refund for parallel subtxns
- Fix SIGSEGV in txn_refund when parallel subtxns have NULL repnl
- Fix pre-touch for DupSort tables using MDBX_NODUPDATA flag
- Fix Cursor Drop to use owned_txn_ptr for parallel cursors
- Add DupSort parallel subtxn tests
- Fix test_save_blocks_edge_mode_parallel to use save_blocks for genesis

Amp-Thread-ID: https://ampcode.com/threads/T-019c294b-a63b-714a-8692-6293db7a1a59
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:10:43 +00:00
joshieDo
877ab61330 fix(provider): move enable_parallel_writes after insert_block_mdbx_only 2026-02-09 16:10:43 +00:00
joshieDo
3e77741fc7 feat(provider): parallelize write_state, write_hashed_state, write_trie_updates_sorted in edge mode
Spawns 3 threads in save_blocks when is_edge_mode() is true:
- Thread 1: write_state for all blocks (PlainAccountState, PlainStorageState, Bytecodes)
- Thread 2: write_hashed_state (HashedAccounts, HashedStorages)
- Thread 3: write_trie_updates_sorted (AccountsTrie, StoragesTrie)

Each thread writes to different tables via parallel subtransactions.
Sequential fallback preserved for non-edge mode.

Added Sync bound to TX where needed for thread::scope.
2026-02-09 16:10:43 +00:00
joshieDo
dea1d3520e chore: remove unused Duration import 2026-02-09 16:10:43 +00:00
joshieDo
34aff1e945 feat(mdbx): parallel subtransactions for edge mode save_blocks
Implements parallel MDBX subtransactions that are enabled only in edge mode,
where receipts/changesets go to static files and history goes to RocksDB.

Key changes:
- Add is_edge_mode() to StorageSettings (checks all 7 edge flags)
- Add parallel subtxn APIs to libmdbx-rs: enable_parallel_writes(),
  put_parallel(), del_parallel(), cursor_with_dbi_parallel(), commit_subtxns()
- Modify mdbx.c to support parallel subtxns with WRITEMAP mode
- Enable parallel writes in save_blocks for 11 MDBX-only tables when
  is_edge_mode() returns true

Edge mode table routing:
- Static files: Receipts, TransactionSenders, AccountChangeSets, StorageChangeSets
- RocksDB: AccountsHistory, StoragesHistory, TransactionHashNumbers
- MDBX (parallel subtxns): HeaderNumbers, BlockBodyIndices, TransactionBlocks,
  PlainAccountState, PlainStorageState, Bytecodes, HashedAccounts,
  HashedStorages, AccountsTrie, StoragesTrie, StageCheckpoints

Constraint: Reads don't see subtxn writes until commit - only safe in edge
mode where we don't read back after writes.

Tests: 145 pass (136 provider + 7 libmdbx + 2 reth-db)
2026-02-09 16:10:43 +00:00
joshieDo
e84c6fdb7b feat(provider): create subtxns only for tables written in save_blocks
- Add enable_parallel_writes_for_tables() method to DbTxMut trait
- Implement in Tx<RW> to create subtxns for specific table names
- save_blocks now creates subtxns for exactly 18 tables:
  - insert_block_mdbx_only: TransactionSenders, HeaderNumbers,
    BlockBodyIndices, TransactionBlocks, BlockOmmers, BlockWithdrawals
  - write_state: PlainAccountState, Bytecodes, PlainStorageState,
    StorageChangeSets, AccountChangeSets, Receipts
  - write_hashed_state: HashedAccounts, HashedStorages
  - write_trie_updates_sorted: AccountsTrie, StoragesTrie
  - update_history_indices: AccountsHistory, StoragesHistory
  - update_pipeline_stages: StageCheckpoints

This reduces overhead by not creating subtxns for unused tables.
2026-02-09 16:10:42 +00:00
joshieDo
0ba99298d4 feat(provider): always enable subtransactions in save_blocks
- Always call enable_parallel_writes() to create subtxns for all DBIs
- Subtransactions work in both sequential and parallel modes
- Parallel threads only spawned when sf_ctx.write_receipts (edge mode)
- Sequential mode still uses subtransactions, just single-threaded
- Subtransactions are committed serially before parent commit
2026-02-09 16:10:42 +00:00
joshieDo
b153f66ed4 feat(provider): enable parallel writes for edge mode
Remove the false && guard to enable parallel MDBX writes when:
- save_mode.with_state() is true
- sf_ctx.write_receipts is true (edge mode)
- enable_parallel_writes() succeeds

This enables concurrent writes to block metadata, plain state, hashed state,
and trie updates tables from separate threads within the same transaction.
2026-02-09 16:10:42 +00:00
joshieDo
7d66c98afb feat(provider): add parallel writes integration for save_blocks
- Add enable_parallel_writes, is_parallel_writes_enabled, commit_subtxns to DbTxMut trait
- Implement parallel writes path in save_blocks for edge mode
- Pre-compute merged hashed state and trie updates before parallel section
- Support parallel writes to: block metadata, plain state, hashed state, trie updates
- Currently disabled (TODO) pending full integration testing
- Add Sync bounds to impl blocks that use thread::scope

The parallel writes feature uses MDBX subtransactions to allow concurrent
writes to different tables from multiple threads within the same parent
transaction.
2026-02-09 16:10:42 +00:00
joshieDo
bef6030e4b feat(mdbx): implement parallel write subtransactions
- Add Transaction<RW>::enable_parallel_writes() to create subtxns per DBI
- Add SubTransaction wrapper with commit/abort methods
- Add Cursor::new_with_ptr() for parallel cursor creation
- Pre-touch DBIs in parent to prevent MAIN_DBI race conditions
- Add Tx<RW> high-level wrapper with automatic parallel routing
- Add comprehensive tests for both low-level and high-level APIs
2026-02-09 16:05:10 +00:00
joshieDo
9bebc00957 feat(mdbx): implement synchronized fallback and remove legacy APIs
- Remove mdbx_txn_reserve_pages() and mdbx_txn_create_subtx() legacy APIs
- Remove MDBX_suballoc_t struct and all suballoc references
- Implement synchronized fallback to parent allocation when subtxn_repnl exhausted
- Add subtxn_alloc_mutex for thread-safe parent EOF allocation
- Update all tests to use mdbx_txn_create_subtxns() batch API
- All 6 parallel write tests passing
2026-02-09 16:05:10 +00:00
joshieDo
4ca24ef2a9 feat(mdbx): GC-first allocation with no pre-reservation for new API
New batch API (mdbx_txn_create_subtxns):
- Distributes parent's repnl + loose_pages equally among subtxns
- NO EOF pre-reservation - subtxns use only pre-claimed GC pages
- Returns MAP_FULL if subtxn exhausts its pages (caller handles fallback)
- Removed pages_reserve from MDBX_subtxn_spec_t

Legacy API (mdbx_txn_reserve_pages + mdbx_txn_create_subtx):
- Still works with pre-reserved EOF pages via suballoc

Allocation priority:
1. subtxn_repnl (GC pages from new API)
2. suballoc (EOF pages from legacy API)
3. MAP_FULL if both exhausted
2026-02-09 16:05:10 +00:00
joshieDo
0507327a5e feat(mdbx): GC-first page allocation for parallel subtxns
Instead of always reserving EOF pages, parallel subtxns now:
1. Harvest available pages from parent's repnl (reclaimed GC pages)
   and loose_pages (freed within this txn)
2. Distribute harvested pages among subtxns proportionally
3. Only reserve EOF pages if GC pages are insufficient

Each subtxn has its own subtxn_repnl (pre-claimed page list).
Allocation order: subtxn_repnl -> suballoc (EOF range)

This reduces file growth by reusing freed pages before extending EOF.
2026-02-09 16:05:10 +00:00
joshieDo
c065b3990d refactor(mdbx): simplify parallel subtxns to WRITEMAP-only with batch API
BREAKING: Parallel subtxns now require WRITEMAP mode (reth's default).

Changes:
- Remove dirtylist/ioring/auxbuf allocation (unused in WRITEMAP)
- Add mdbx_txn_create_subtxns() batch API that creates all subtxns atomically
- Each subtxn is bound to a specific DBI via assigned_dbi field
- Cursor open rejects wrong DBI access with MDBX_EINVAL
- Duplicate DBI check in batch creation prevents conflicts
- Mark legacy APIs (mdbx_txn_reserve_pages, mdbx_txn_create_subtx) deprecated

The invariant 'one subtxn per DBI' is now enforced at:
1. Creation time: duplicate DBIs in specs rejected
2. Cursor open time: subtxn can only open cursor on its assigned DBI

This ensures no B-tree page conflicts between parallel writers.
2026-02-09 16:05:10 +00:00
joshieDo
efd6f06069 fix(mdbx): add WRITEMAP support for parallel subtransactions
Previously, parallel subtxns failed with WRITEMAP mode because dpl_alloc()
asserts that WRITEMAP is disabled. WRITEMAP mode writes directly to the
memory-mapped file and doesn't need a dirty page list.

Changes:
- Skip dpl_alloc() for parallel subtxns in WRITEMAP mode
- Initialize writemap_dirty_npages counter instead
- Merge counter (not dirtylist) at subtxn commit time
- Add test_parallel_subtx_writemap test to verify compatibility

This makes parallel writes compatible with reth's default WRITEMAP mode.
2026-02-09 16:05:10 +00:00
Georgios Konstantopoulos
570411e189 test(db): add parallel writes test for reth-db
Adds a test in reth-db that demonstrates parallel writes to two different
tables (simulating Headers and CanonicalHeaders) using the new parallel
subtransaction API.

The test uses pure FFI to create a non-WriteMap environment since WriteMap
mode doesn't support nested/subtransactions. It verifies that concurrent
writes to separate DBIs from different threads work correctly when using
the page reservation and parallel subtxn APIs.

Amp-Thread-ID: https://ampcode.com/threads/T-019c24b2-df74-73aa-af98-72d8a61d1595
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:05:10 +00:00
Georgios Konstantopoulos
96003e1bf6 fix(mdbx): only merge DBI_DIRTY state in subtxn commit
Previously, mdbx_subtx_commit() merged DBI metadata when either DBI_DIRTY
or DBI_FRESH was set. Since DBI_FRESH is inherited from parent when subtxn
is created, this caused subtxns to overwrite parent DBI metadata for DBIs
they didn't actually modify.

This broke parallel writes to different DBIs: committing subtxn2 would
overwrite subtxn1's DBI root with a stale snapshot.

Fix: Only merge DBIs where DBI_DIRTY is set (actual modifications).
Amp-Thread-ID: https://ampcode.com/threads/T-019c24b2-df74-73aa-af98-72d8a61d1595
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:05:10 +00:00
Georgios Konstantopoulos
ca17c74be8 fix(mdbx): fix parallel subtxn page handling for different DBIs
- Clear DBI_FRESH/DIRTY/CREAT flags when creating subtxns so only
  actually modified DBIs get merged back to parent on commit
- Handle pages from parent's dirty list in page_touch_modifable by
  adding them to subtxn's dirty list when touched
- Skip duplicate pages in mdbx_subtx_commit when merging dirty pages
  back to parent (pages already in parent's list from subtxn touch)
- Fix Rust test Send issue by returning usize instead of raw pointer

All 4 parallel_writes tests now pass, including the multi-DBI parallel
writes test that previously failed.

Amp-Thread-ID: https://ampcode.com/threads/T-019c24b2-df74-73aa-af98-72d8a61d1595
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:05:09 +00:00
Georgios Konstantopoulos
68335cf015 feat(mdbx): add parallel subtransaction support
Implements parallel write subtransactions for MDBX with:
- mdbx_txn_reserve_pages(): Reserve a range of page numbers
- mdbx_txn_create_subtx(): Create a subtransaction with its own page allocator
- mdbx_subtx_commit(): Merge subtxn dirty pages and dbi metadata into parent
- mdbx_subtx_abort(): Discard subtxn changes
- mdbx_txn_is_subtx(): Check if a txn is a parallel subtxn

Key implementation details:
- Each subtxn uses a bump allocator from its reserved page range
- Subtxns can read pages from parent's dirtylist via subparent chain
- On commit, dirty pages and B-tree metadata are merged to parent
- Sequential subtxn commits work correctly for same-dbi writes

Limitations:
- Concurrent writes to the same dbi are NOT supported (B-tree mods not composable)
- Subtxns must commit serially if they modify the same dbi

Amp-Thread-ID: https://ampcode.com/threads/T-019c248d-d2c3-702b-970b-153a9b5f1206
Co-authored-by: Amp <amp@ampcode.com>
2026-02-09 16:05:09 +00:00
372 changed files with 21217 additions and 10679 deletions

View File

@@ -1,5 +0,0 @@
---
reth-transaction-pool: patch
---
Renamed and documented validation methods for clarity. `validate_one_no_state` and `validate_one_against_state` are now public methods `validate_stateless` and `validate_stateful` with improved documentation explaining their respective validation phases.

View File

@@ -1,5 +0,0 @@
---
reth-network: minor
---
Added reason label to backed_off_peers metric. The metric now tracks backed off peers by reason (too_many_peers, graceful_close, connection_error) to improve observability.

View File

@@ -1,5 +0,0 @@
---
ef-tests: patch
---
Removed reth-stateless crate and stateless validation from ef-tests.

View File

@@ -1,6 +0,0 @@
---
reth-exex: patch
reth-exex-types: patch
---
Added configurable backfill thresholds to ExEx notifications stream and added regression tests for state provider parity between pipeline and backfill execution paths.

View File

@@ -1,4 +0,0 @@
---
---
Added WebSocket subscription integration tests for eth_subscribe.

View File

@@ -1,4 +0,0 @@
---
---
Improved nightly Docker build failure Slack notification with more detailed formatting and context.

View File

@@ -1,7 +0,0 @@
---
reth: patch
reth-cli-commands: patch
reth-node-core: patch
---
Removed experimental ress protocol support for stateless Ethereum nodes.

View File

@@ -1,5 +0,0 @@
---
reth-node-builder: patch
---
Removed biased select in engine service loop to allow fair scheduling of shutdown requests alongside event processing.

View File

@@ -1,5 +0,0 @@
---
reth-transaction-pool: patch
---
Fixed swapped arguments in `blob_tx_priority` function calls, correcting the parameter order to match the function signature.

View File

@@ -1,4 +0,0 @@
---
---
Improved documentation overview page with better structure and clarity.

View File

@@ -1,5 +0,0 @@
---
reth-node-events: patch
---
Updated consensus engine log message to be more accurate about received updates.

View File

@@ -1,9 +0,0 @@
---
reth-network-api: minor
reth-network-types: minor
reth-network: minor
reth-node-core: minor
reth: minor
---
Added optional ENR fork ID enforcement to filter out peers from incompatible networks during peer discovery, controlled by the `--enforce-enr-fork-id` CLI flag.

View File

@@ -1,5 +0,0 @@
---
reth-primitives: patch
---
Moved feature-referenced dependencies from dev-dependencies to optional dependencies to ensure they are available when their corresponding features are enabled.

View File

@@ -1,5 +0,0 @@
---
reth-transaction-pool: minor
---
Added `IntoIter: Send` bounds to `validate_transactions` and `validate_transactions_with_origin` in the `TransactionValidator` trait, avoiding unnecessary `Vec` collects. Simplified default `validate_transactions_with_origin` to delegate to `validate_transactions`.

View File

@@ -1,5 +0,0 @@
---
reth-provider: patch
---
Removed unused staging types from ProviderFactoryBuilder.

View File

@@ -1,5 +0,0 @@
---
reth-trie-sparse: minor
---
Removed `SerialSparseTrie` from the workspace, consolidating on `ParallelSparseTrie` as the single sparse trie implementation in `reth-trie-sparse`.

View File

@@ -1,5 +0,0 @@
---
reth-trie-sparse: patch
---
Fixed a bug where trie nodes could appear in both `updated_nodes` and `removed_nodes` simultaneously by removing entries from `removed_nodes` when a node is inserted as updated.

View File

@@ -1,4 +0,0 @@
---
---
Expanded CLI integration tests with subcommand help coverage, config TOML validation, genesis JSON validation, and send transaction round-trip test for dev mode.

View File

@@ -1,5 +0,0 @@
---
reth-network: minor
---
Added direction labels to `closed_sessions` and `pending_session_failures` metrics. Operators can now distinguish session closures and failures by direction (`active`, `incoming_pending`, `outgoing_pending` for closed sessions; `inbound`, `outbound` for pending session failures).

View File

@@ -1,4 +0,0 @@
---
---
Moved Kurtosis CI failure notifications to the hive Slack channel.

View File

@@ -1,7 +0,0 @@
---
reth-rpc-api: minor
reth-rpc-builder: patch
reth-rpc: minor
---
Added `subscribeFinalizedChainNotifications` RPC endpoint that buffers committed chain notifications and emits them once a new finalized block is received.

View File

@@ -1,5 +0,0 @@
---
reth-trie: patch
---
Fixed a potential panic in `ProofCalculator` by clearing internal computation state (`branch_stack`, `child_stack`, `branch_path`, etc.) after errors, preventing stale state from causing `usize` underflow panics when the calculator is reused. Added a test verifying correct behavior after simulated mid-computation errors.

2
.github/CODEOWNERS vendored
View File

@@ -38,7 +38,7 @@ crates/storage/libmdbx-rs/ @shekhirin
crates/storage/nippy-jar/ @joshieDo @shekhirin
crates/storage/provider/ @joshieDo @shekhirin @yongkangc
crates/storage/storage-api/ @joshieDo
crates/tasks/ @mattsse @DaniPopes
crates/tasks/ @mattsse
crates/tokio-util/ @mattsse
crates/tracing/ @mattsse @shekhirin
crates/tracing-otlp/ @mattsse @Rjected

View File

@@ -27,6 +27,7 @@ crates_to_check=(
reth-ethereum-forks
reth-ethereum-primitives
reth-ethereum-consensus
reth-stateless
)
any_failed=0

View File

@@ -63,7 +63,6 @@ exclude_crates=(
reth-provider # tokio
reth-prune # tokio
reth-prune-static-files # reth-provider
reth-tasks # tokio rt-multi-thread
reth-stages-api # reth-provider, reth-prune
reth-static-file # tokio
reth-transaction-pool # c-kzg

View File

@@ -15,7 +15,6 @@ permissions:
jobs:
update:
if: github.repository == 'paradigmxyz/reth'
uses: tempoxyz/ci/.github/workflows/cargo-update-pr.yml@main
secrets:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -31,7 +31,6 @@ on:
jobs:
build:
if: github.repository == 'paradigmxyz/reth'
name: Build Docker images
runs-on: ubuntu-24.04
permissions:
@@ -70,27 +69,18 @@ jobs:
# Add 'latest' tag for non-RC releases
if [[ ! "$VERSION" =~ -rc ]]; then
echo "ethereum_tags=${REGISTRY}/reth:${VERSION},${REGISTRY}/reth:latest" >> "$GITHUB_OUTPUT"
{
echo "ethereum_set<<EOF"
echo "ethereum.tags=${REGISTRY}/reth:${VERSION}"
echo "ethereum.tags=${REGISTRY}/reth:latest"
echo "EOF"
} >> "$GITHUB_OUTPUT"
else
echo "ethereum_tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:${VERSION}" >> "$GITHUB_OUTPUT"
fi
elif [[ "${{ github.event_name }}" == "schedule" ]] || [[ "${{ inputs.build_type }}" == "nightly" ]]; then
echo "targets=nightly" >> "$GITHUB_OUTPUT"
echo "ethereum_tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:nightly" >> "$GITHUB_OUTPUT"
else
# git-sha build
echo "targets=ethereum" >> "$GITHUB_OUTPUT"
echo "ethereum_tags=${REGISTRY}/reth:${{ github.sha }}" >> "$GITHUB_OUTPUT"
echo "ethereum_set=ethereum.tags=${REGISTRY}/reth:${{ github.sha }}" >> "$GITHUB_OUTPUT"
fi
- name: Build and push images
@@ -106,7 +96,7 @@ jobs:
targets: ${{ steps.params.outputs.targets }}
push: ${{ !(github.event_name == 'workflow_dispatch' && inputs.dry_run) }}
set: |
${{ steps.params.outputs.ethereum_set }}
ethereum.tags=${{ steps.params.outputs.ethereum_tags }}
- name: Verify image architectures
env:
@@ -126,18 +116,6 @@ jobs:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@v2
env:
SLACK_COLOR: danger
SLACK_ICON_EMOJI: ":rotating_light:"
SLACK_USERNAME: "GitHub Actions"
SLACK_TITLE: ":rotating_light: Nightly Docker Build Failed"
SLACK_MESSAGE: |
The scheduled nightly Docker build failed.
*Commit:* `${{ github.sha }}`
*Branch:* `${{ github.ref_name }}`
*Run:* <https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}|View logs>
*Action required:* Re-run the workflow or investigate the build failure.
SLACK_FOOTER: "paradigmxyz/reth · docker.yml"
MSG_MINIMAL: true
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -35,7 +35,6 @@ jobs:
- name: Run e2e tests
run: |
cargo nextest run \
--no-fail-fast \
--locked --features "asm-keccak" \
--workspace \
--exclude 'example-*' \
@@ -62,7 +61,6 @@ jobs:
- name: Run RocksDB e2e tests
run: |
cargo nextest run \
--no-fail-fast \
--locked --features "edge" \
-p reth-e2e-test-utils \
-E 'binary(rocksdb)'

View File

@@ -46,7 +46,6 @@ jobs:
- name: Run tests
run: |
cargo nextest run \
--no-fail-fast \
--locked --features "asm-keccak ${{ matrix.network }} ${{ matrix.storage == 'edge' && 'edge' || '' }}" \
--workspace --exclude ef-tests \
-E "kind(test) and not binary(e2e_testsuite)"
@@ -65,7 +64,7 @@ jobs:
era-files:
name: era1 file integration tests once a day
if: github.event_name == 'schedule' && github.repository == 'paradigmxyz/reth'
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@@ -77,4 +76,4 @@ jobs:
with:
cache-on-failure: true
- name: run era1 files integration tests
run: cargo nextest run --no-fail-fast --release --package reth-era --test it -- --ignored
run: cargo nextest run --release --package reth-era --test it -- --ignored

View File

@@ -20,7 +20,6 @@ concurrency:
jobs:
build-reth:
if: github.repository == 'paradigmxyz/reth'
uses: ./.github/workflows/docker-test.yml
with:
hive_target: kurtosis
@@ -66,4 +65,4 @@ jobs:
env:
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: "Failed run: https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}"
SLACK_WEBHOOK: ${{ secrets.SLACK_HIVE_WEBHOOK_URL }}
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -7,7 +7,6 @@ on:
jobs:
build:
if: github.repository == 'paradigmxyz/reth'
name: build reproducible binaries
runs-on: ${{ matrix.runner }}
strategy:

View File

@@ -38,7 +38,7 @@ jobs:
cache-on-failure: true
- name: Build reth
run: |
cargo install --path bin/reth
cargo install --features asm-keccak,jemalloc --path bin/reth
- name: Run headers stage
run: |
reth stage run headers --from ${{ env.FROM_BLOCK }} --to ${{ env.TO_BLOCK }} --commit --checkpoints

View File

@@ -9,7 +9,6 @@ on:
jobs:
close-issues:
if: github.repository == 'paradigmxyz/reth'
runs-on: ubuntu-latest
permissions:
issues: write

View File

@@ -17,7 +17,6 @@ concurrency:
jobs:
sync:
if: github.repository == 'paradigmxyz/reth'
name: sync (${{ matrix.chain.bin }})
runs-on: depot-ubuntu-latest
env:

View File

@@ -17,7 +17,6 @@ concurrency:
jobs:
sync:
if: github.repository == 'paradigmxyz/reth'
name: sync (${{ matrix.chain.bin }})
runs-on: depot-ubuntu-latest
env:

View File

@@ -49,7 +49,6 @@ jobs:
- name: Run tests
run: |
cargo nextest run \
--no-fail-fast \
--features "${{ matrix.features }} $EDGE_FEATURES" --locked \
${{ matrix.exclude_args }} --workspace \
--exclude ef-tests --no-tests=warn \
@@ -88,7 +87,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo nextest run --no-fail-fast --cargo-profile hivetests -p ef-tests --features "asm-keccak ef-tests"
- run: cargo nextest run --cargo-profile hivetests -p ef-tests --features "asm-keccak ef-tests"
doc:
name: doc tests

View File

@@ -381,7 +381,7 @@ cargo nextest run --workspace
cargo bench --bench bench_name
# Build optimized binary
cargo build --release
cargo build --release --features "jemalloc asm-keccak"
# Check compilation for all features
cargo check --workspace --all-features

829
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[workspace.package]
version = "1.11.3"
version = "1.10.2"
edition = "2024"
rust-version = "1.88"
license = "MIT OR Apache-2.0"
@@ -83,6 +83,8 @@ members = [
"crates/prune/db",
"crates/prune/prune",
"crates/prune/types",
"crates/ress/protocol",
"crates/ress/provider",
"crates/revm/",
"crates/rpc/ipc/",
"crates/rpc/rpc-api/",
@@ -99,6 +101,7 @@ members = [
"crates/stages/api/",
"crates/stages/stages/",
"crates/stages/types/",
"crates/stateless",
"crates/static-file/static-file",
"crates/static-file/types/",
"crates/storage/codecs/",
@@ -122,6 +125,7 @@ members = [
"crates/trie/db",
"crates/trie/parallel/",
"crates/trie/sparse",
"crates/trie/sparse-parallel/",
"crates/trie/trie",
"examples/beacon-api-sidecar-fetcher/",
"examples/beacon-api-sse/",
@@ -306,11 +310,6 @@ inherits = "release"
lto = "fat"
codegen-units = 1
[profile.maxperf-symbols]
inherits = "maxperf"
debug = "full"
strip = "none"
[profile.reproducible]
inherits = "release"
panic = "abort"
@@ -419,6 +418,7 @@ reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
reth-stages = { path = "crates/stages/stages" }
reth-stages-api = { path = "crates/stages/api" }
reth-stages-types = { path = "crates/stages/types", default-features = false }
reth-stateless = { path = "crates/stateless", default-features = false }
reth-static-file = { path = "crates/static-file/static-file" }
reth-static-file-types = { path = "crates/static-file/types", default-features = false }
reth-storage-api = { path = "crates/storage/storage-api", default-features = false }
@@ -434,7 +434,10 @@ reth-trie-common = { path = "crates/trie/common", default-features = false }
reth-trie-db = { path = "crates/trie/db" }
reth-trie-parallel = { path = "crates/trie/parallel" }
reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
reth-trie-sparse-parallel = { path = "crates/trie/sparse-parallel" }
reth-zstd-compressors = { path = "crates/storage/zstd-compressors", default-features = false }
reth-ress-protocol = { path = "crates/ress/protocol" }
reth-ress-provider = { path = "crates/ress/provider" }
# revm
revm = { version = "34.0.0", default-features = false }
@@ -448,57 +451,46 @@ op-revm = { version = "15.0.0", default-features = false }
revm-inspectors = "0.34.2"
# eth
alloy-dyn-abi = "1.5.6"
alloy-primitives = { version = "1.5.6", default-features = false, features = [
"map-foldhash",
] }
alloy-sol-types = { version = "1.5.6", default-features = false }
alloy-dyn-abi = "1.5.4"
alloy-primitives = { version = "1.5.4", default-features = false, features = ["map-foldhash"] }
alloy-sol-types = { version = "1.5.4", default-features = false }
alloy-chains = { version = "0.2.5", default-features = false }
alloy-eip2124 = { version = "0.2.0", default-features = false }
alloy-eip7928 = { version = "0.3.0", default-features = false }
alloy-evm = { version = "0.27.2", default-features = false }
alloy-rlp = { version = "0.3.13", default-features = false, features = [
"core-net",
] }
alloy-rlp = { version = "0.3.10", default-features = false, features = ["core-net"] }
alloy-trie = { version = "0.9.4", default-features = false }
alloy-hardforks = "0.4.5"
alloy-consensus = { version = "1.6.3", default-features = false }
alloy-contract = { version = "1.6.3", default-features = false }
alloy-eips = { version = "1.6.3", default-features = false }
alloy-genesis = { version = "1.6.3", default-features = false }
alloy-json-rpc = { version = "1.6.3", default-features = false }
alloy-network = { version = "1.6.3", default-features = false }
alloy-network-primitives = { version = "1.6.3", default-features = false }
alloy-provider = { version = "1.6.3", features = [
"reqwest",
"debug-api",
], default-features = false }
alloy-pubsub = { version = "1.6.3", default-features = false }
alloy-rpc-client = { version = "1.6.3", default-features = false }
alloy-rpc-types = { version = "1.6.3", features = [
"eth",
], default-features = false }
alloy-rpc-types-admin = { version = "1.6.3", default-features = false }
alloy-rpc-types-anvil = { version = "1.6.3", default-features = false }
alloy-rpc-types-beacon = { version = "1.6.3", default-features = false }
alloy-rpc-types-debug = { version = "1.6.3", default-features = false }
alloy-rpc-types-engine = { version = "1.6.3", default-features = false }
alloy-rpc-types-eth = { version = "1.6.3", default-features = false }
alloy-rpc-types-mev = { version = "1.6.3", default-features = false }
alloy-rpc-types-trace = { version = "1.6.3", default-features = false }
alloy-rpc-types-txpool = { version = "1.6.3", default-features = false }
alloy-serde = { version = "1.6.3", default-features = false }
alloy-signer = { version = "1.6.3", default-features = false }
alloy-signer-local = { version = "1.6.3", default-features = false }
alloy-transport = { version = "1.6.3" }
alloy-transport-http = { version = "1.6.3", features = [
"reqwest-rustls-tls",
], default-features = false }
alloy-transport-ipc = { version = "1.6.3", default-features = false }
alloy-transport-ws = { version = "1.6.3", default-features = false }
alloy-consensus = { version = "1.6.2", default-features = false }
alloy-contract = { version = "1.6.2", default-features = false }
alloy-eips = { version = "1.6.2", default-features = false }
alloy-genesis = { version = "1.6.2", default-features = false }
alloy-json-rpc = { version = "1.6.2", default-features = false }
alloy-network = { version = "1.6.2", default-features = false }
alloy-network-primitives = { version = "1.6.2", default-features = false }
alloy-provider = { version = "1.6.2", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.6.2", default-features = false }
alloy-rpc-client = { version = "1.6.2", default-features = false }
alloy-rpc-types = { version = "1.6.2", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.6.2", default-features = false }
alloy-rpc-types-anvil = { version = "1.6.2", default-features = false }
alloy-rpc-types-beacon = { version = "1.6.2", default-features = false }
alloy-rpc-types-debug = { version = "1.6.2", default-features = false }
alloy-rpc-types-engine = { version = "1.6.2", default-features = false }
alloy-rpc-types-eth = { version = "1.6.2", default-features = false }
alloy-rpc-types-mev = { version = "1.6.2", default-features = false }
alloy-rpc-types-trace = { version = "1.6.2", default-features = false }
alloy-rpc-types-txpool = { version = "1.6.2", default-features = false }
alloy-serde = { version = "1.6.2", default-features = false }
alloy-signer = { version = "1.6.2", default-features = false }
alloy-signer-local = { version = "1.6.2", default-features = false }
alloy-transport = { version = "1.6.2" }
alloy-transport-http = { version = "1.6.2", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.6.2", default-features = false }
alloy-transport-ws = { version = "1.6.2", default-features = false }
# op
alloy-op-evm = { version = "0.27.2", default-features = false }
@@ -515,10 +507,7 @@ either = { version = "1.15.0", default-features = false }
arrayvec = { version = "0.7.6", default-features = false }
aquamarine = "0.6"
auto_impl = "1"
backon = { version = "1.2", default-features = false, features = [
"std-blocking-sleep",
"tokio-sleep",
] }
backon = { version = "1.2", default-features = false, features = ["std-blocking-sleep", "tokio-sleep"] }
bincode = "1.3"
bitflags = "2.4"
boyer-moore-magiclen = "0.2.16"
@@ -540,13 +529,9 @@ itertools = { version = "0.14", default-features = false }
linked_hash_set = "0.1"
lz4 = "1.28.1"
modular-bitfield = "0.13.1"
notify = { version = "8.0.0", default-features = false, features = [
"macos_fsevent",
] }
notify = { version = "8.0.0", default-features = false, features = ["macos_fsevent"] }
nybbles = { version = "0.4.8", default-features = false }
once_cell = { version = "1.19", default-features = false, features = [
"critical-section",
] }
once_cell = { version = "1.19", default-features = false, features = ["critical-section"] }
parking_lot = "0.12"
paste = "1.0"
rand = "0.9"
@@ -565,9 +550,7 @@ strum_macros = "0.27"
syn = "2.0"
thiserror = { version = "2.0.0", default-features = false }
tar = "0.4.44"
tracing = { version = "0.1.0", default-features = false, features = [
"attributes",
] }
tracing = { version = "0.1.0", default-features = false }
tracing-appender = "0.2"
url = { version = "2.3", default-features = false }
zstd = "0.13"
@@ -605,11 +588,7 @@ futures-util = { version = "0.3", default-features = false }
hyper = "1.3"
hyper-util = "0.1.5"
pin-project = "1.0.12"
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
"rustls-tls-native-roots",
"stream",
] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots", "stream"] }
tracing-futures = "0.2"
tower = "0.5"
tower-http = "0.6"
@@ -634,10 +613,7 @@ proptest-arbitrary-interop = "0.1.0"
# crypto
enr = { version = "0.13", default-features = false }
k256 = { version = "0.13", default-features = false, features = ["ecdsa"] }
secp256k1 = { version = "0.30", default-features = false, features = [
"global-context",
"recovery",
] }
secp256k1 = { version = "0.30", default-features = false, features = ["global-context", "recovery"] }
# rand 8 for secp256k1
rand_08 = { package = "rand", version = "0.8" }

View File

@@ -51,15 +51,14 @@ RUN --mount=type=secret,id=DEPOT_TOKEN,env=SCCACHE_WEBDAV_TOKEN \
--mount=type=cache,target=/usr/local/cargo/registry,sharing=shared \
--mount=type=cache,target=/usr/local/cargo/git,sharing=shared \
--mount=type=cache,target=$SCCACHE_DIR,sharing=shared \
export RUSTC_WRAPPER=sccache SCCACHE_WEBDAV_ENDPOINT=https://cache.depot.dev SCCACHE_DIR=/sccache && \
sccache --start-server && \
if [ -n "$RUSTFLAGS" ]; then \
export RUSTFLAGS="$RUSTFLAGS"; \
elif [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
export RUSTFLAGS="-C target-cpu=x86-64-v3 -C target-feature=+pclmulqdq"; \
fi && \
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin $BINARY --manifest-path $MANIFEST_PATH/Cargo.toml && \
sccache --show-stats
cargo build --profile $BUILD_PROFILE --features "$FEATURES" --locked --bin $BINARY --manifest-path $MANIFEST_PATH/Cargo.toml
RUN sccache --show-stats || true
# Copy binary to a known location (ARG not resolved in COPY)
# Note: Custom profiles like maxperf/profiling output to target/<profile>/, not target/release/

View File

@@ -12,7 +12,12 @@ FULL_DB_TOOLS_DIR := $(shell pwd)/$(DB_TOOLS_DIR)/
CARGO_TARGET_DIR ?= target
# List of features to use when building. Can be overridden via the environment.
FEATURES ?=
# No jemalloc on Windows
ifeq ($(OS),Windows_NT)
FEATURES ?= asm-keccak min-debug-logs
else
FEATURES ?= jemalloc asm-keccak min-debug-logs
endif
# Cargo profile for builds. Default is for local builds, CI uses an override.
PROFILE ?= release
@@ -153,7 +158,7 @@ COV_FILE := lcov.info
.PHONY: test-unit
test-unit: ## Run unit tests.
cargo install cargo-nextest --locked
cargo nextest run --no-fail-fast $(UNIT_TEST_ARGS)
cargo nextest run $(UNIT_TEST_ARGS)
.PHONY: cov-unit
@@ -186,7 +191,7 @@ $(EEST_TESTS_DIR):
.PHONY: ef-tests
ef-tests: $(EF_TESTS_DIR) $(EEST_TESTS_DIR) ## Runs Legacy and EEST tests.
cargo nextest run --no-fail-fast -p ef-tests --release --features ef-tests
cargo nextest run -p ef-tests --release --features ef-tests
##@ reth-bench
@@ -233,15 +238,16 @@ update-book-cli: build-debug ## Update book cli documentation.
.PHONY: profiling
profiling: ## Builds `reth` with optimisations, but also symbols.
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --features jemalloc,asm-keccak
.PHONY: maxperf
maxperf: ## Builds `reth` with the most aggressive optimisations.
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --features jemalloc,asm-keccak
.PHONY: maxperf-no-asm
maxperf-no-asm: ## Builds `reth` with the most aggressive optimisations, minus the "asm-keccak" feature.
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --no-default-features --features jemalloc,min-debug-logs,otlp,otlp-logs,reth-revm/portable,js-tracer,keccak-cache-global,rocksdb
RUSTFLAGS="-C target-cpu=native" cargo build --profile maxperf --features jemalloc
fmt:
cargo +nightly fmt

View File

@@ -30,7 +30,7 @@ reth-bench-compare \
| `--draw` | Generate charts (needs Python/uv) | `false` | No |
| `--profile` | Enable CPU profiling (needs samply) | `false` | No |
| `-vvvv` | Debug logging | Info | No |
| `--features <FEATURES>` | Extra Rust features for both builds | - | No |
| `--features <FEATURES>` | Rust features for both builds | `jemalloc,asm-keccak` | No |
| `--rustflags <FLAGS>` | RUSTFLAGS for both builds | `-C target-cpu=native` | No |
| `--baseline-features <FEATURES>` | Features for baseline only | Inherits `--features` | No |
| `--feature-features <FEATURES>` | Features for feature only | Inherits `--features` | No |

View File

@@ -191,9 +191,10 @@ pub(crate) struct Args {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub reth_args: Vec<String>,
/// Comma-separated list of extra features to enable during reth compilation (applied to both
/// builds)
#[arg(long, value_name = "FEATURES", default_value = "")]
/// Comma-separated list of features to enable during reth compilation (applied to both builds)
///
/// Example: `jemalloc,asm-keccak`
#[arg(long, value_name = "FEATURES", default_value = "jemalloc,asm-keccak")]
pub features: String,
/// Comma-separated list of features to enable only for baseline build (overrides --features)
@@ -204,7 +205,7 @@ pub(crate) struct Args {
/// Comma-separated list of features to enable only for feature build (overrides --features)
///
/// Example: `--feature-features jemalloc-prof`
/// Example: `--feature-features jemalloc,asm-keccak`
#[arg(long, value_name = "FEATURES")]
pub feature_features: Option<String>,
@@ -276,8 +277,10 @@ impl Args {
/// Get the default RPC URL for a given chain
const fn get_default_rpc_url(chain: &Chain) -> &'static str {
match chain.id() {
27082 => "https://rpc.hoodi.ethpandaops.io", // hoodi
_ => "https://ethereum.reth.rs/rpc", // mainnet and fallback
8453 => "https://base.reth.rs/rpc", // base
84532 => "https://base-sepolia.rpc.ithaca.xyz", // base-sepolia
27082 => "https://rpc.hoodi.ethpandaops.io", // hoodi
_ => "https://ethereum.reth.rs/rpc", // mainnet and fallback
}
}

View File

@@ -32,7 +32,7 @@ Otherwise, running `make maxperf` at the root of the repo should be sufficient f
The `reth-bench new-payload-fcu` command is the most representative of ethereum mainnet live sync, alternating between sending `engine_newPayload` calls and `engine_forkchoiceUpdated` calls.
The `new-payload-fcu` command supports two optional waiting modes that can be used together or independently:
- `--wait-time <duration>`: Fixed sleep interval between blocks (e.g., `--wait-time 100ms` or `--wait-time 400` for 400ms)
- `--wait-time <duration>`: Fixed sleep interval between blocks (e.g., `--wait-time 100ms`)
- `--wait-for-persistence`: Waits for blocks to be persisted using the `reth_subscribePersistedBlock` subscription
When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold (2). This can be customized with `--persistence-threshold <N>`.
@@ -73,7 +73,7 @@ make profiling
If the purpose of the benchmark is to obtain `jemalloc` memory profiles that can then be analyzed by `jeprof`, it should be compiled with the `profiling` profile and the `jemalloc-prof` feature:
```bash
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --features "jemalloc-prof"
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --features "jemalloc-prof,asm-keccak"
```
> [!NOTE]
@@ -82,7 +82,7 @@ RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --features "jem
Finally, if the purpose of the benchmark is to profile the node when `snmalloc` is configured as the default allocator, it would be built with the following
command:
```bash
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --no-default-features --features "snmalloc-native,asm-keccak,min-debug-logs"
RUSTFLAGS="-C target-cpu=native" cargo build --profile profiling --no-default-features --features "snmalloc-native,asm-keccak"
```
### Run the Benchmark:

View File

@@ -192,15 +192,6 @@ impl Command {
parent_header = block.header;
parent_hash = block_hash;
blocks_processed += 1;
let progress = match mode {
RampMode::Blocks(total) => format!("{blocks_processed}/{total}"),
RampMode::TargetGasLimit(target) => {
let pct = (parent_header.gas_limit as f64 / target as f64 * 100.0).min(100.0);
format!("{pct:.1}%")
}
};
info!(target: "reth-bench", progress, block_number = parent_header.number, gas_limit = parent_header.gas_limit, "Block processed");
}
let final_gas_limit = parent_header.gas_limit;

View File

@@ -2,10 +2,7 @@
use crate::valid_payload::call_forkchoice_updated;
use eyre::Result;
use std::{
io::{BufReader, Read},
time::Duration,
};
use std::io::{BufReader, Read};
/// Read input from either a file path or stdin.
pub(crate) fn read_input(path: Option<&str>) -> Result<String> {
@@ -54,22 +51,6 @@ pub(crate) fn parse_gas_limit(s: &str) -> eyre::Result<u64> {
let base: u64 = num_str.trim().parse()?;
base.checked_mul(multiplier).ok_or_else(|| eyre::eyre!("value overflow"))
}
/// Parses a duration string, treating bare integers as milliseconds.
///
/// Accepts either a `humantime` duration string (e.g. `"100ms"`, `"2s"`) or a plain
/// integer which is interpreted as milliseconds (e.g. `"400"` → 400ms).
pub(crate) fn parse_duration(s: &str) -> eyre::Result<Duration> {
match humantime::parse_duration(s) {
Ok(d) => Ok(d),
Err(_) => {
let millis: u64 =
s.trim().parse().map_err(|_| eyre::eyre!("invalid duration: {s:?}"))?;
Ok(Duration::from_millis(millis))
}
}
}
use alloy_consensus::Header;
use alloy_eips::eip4844::kzg_to_versioned_hash;
use alloy_primitives::{Address, B256};
@@ -289,24 +270,4 @@ mod tests {
assert!(parse_gas_limit("G").is_err());
assert!(parse_gas_limit("-1G").is_err());
}
#[test]
fn test_parse_duration_with_unit() {
assert_eq!(parse_duration("100ms").unwrap(), Duration::from_millis(100));
assert_eq!(parse_duration("2s").unwrap(), Duration::from_secs(2));
assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
}
#[test]
fn test_parse_duration_bare_millis() {
assert_eq!(parse_duration("400").unwrap(), Duration::from_millis(400));
assert_eq!(parse_duration("0").unwrap(), Duration::from_millis(0));
assert_eq!(parse_duration("1000").unwrap(), Duration::from_millis(1000));
}
#[test]
fn test_parse_duration_errors() {
assert!(parse_duration("abc").is_err());
assert!(parse_duration("").is_err());
}
}

View File

@@ -12,7 +12,6 @@
use crate::{
bench::{
context::BenchContext,
helpers::parse_duration,
output::{
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
},
@@ -26,6 +25,7 @@ use alloy_provider::Provider;
use alloy_rpc_types_engine::ForkchoiceState;
use clap::Parser;
use eyre::{Context, OptionExt};
use humantime::parse_duration;
use reth_cli_runner::CliContext;
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
use reth_node_core::args::BenchmarkArgs;
@@ -40,9 +40,6 @@ pub struct Command {
rpc_url: String,
/// How long to wait after a forkchoice update before sending the next payload.
///
/// Accepts a duration string (e.g. `100ms`, `2s`) or a bare integer treated as
/// milliseconds (e.g. `400`).
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, verbatim_doc_comment)]
wait_time: Option<Duration>,
@@ -120,7 +117,7 @@ impl Command {
self.benchmark.ws_rpc_url.as_deref(),
&self.benchmark.engine_rpc_url,
)?;
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
let sub = setup_persistence_subscription(ws_url).await?;
Some(PersistenceWaiter::with_duration_and_subscription(
duration,
sub,
@@ -134,7 +131,7 @@ impl Command {
self.benchmark.ws_rpc_url.as_deref(),
&self.benchmark.engine_rpc_url,
)?;
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
let sub = setup_persistence_subscription(ws_url).await?;
Some(PersistenceWaiter::with_subscription(
sub,
self.persistence_threshold,
@@ -153,7 +150,6 @@ impl Command {
..
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
let total_blocks = benchmark_mode.total_blocks();
let buffer_size = self.rpc_block_buffer_size;
// Use a oneshot channel to propagate errors from the spawned task
@@ -207,7 +203,6 @@ impl Command {
});
let mut results = Vec::new();
let mut blocks_processed = 0u64;
let total_benchmark_duration = Instant::now();
let mut total_wait_time = Duration::ZERO;
@@ -251,13 +246,8 @@ impl Command {
// Exclude time spent waiting on the block prefetch channel from the benchmark duration.
// We want to measure engine throughput, not RPC fetch latency.
blocks_processed += 1;
let current_duration = total_benchmark_duration.elapsed() - total_wait_time;
let progress = match total_blocks {
Some(total) => format!("{blocks_processed}/{total}"),
None => format!("{blocks_processed}"),
};
info!(target: "reth-bench", progress, %combined_result);
info!(target: "reth-bench", %combined_result);
if let Some(w) = &mut waiter {
w.on_block(block_number).await?;

View File

@@ -52,7 +52,6 @@ impl Command {
..
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
let total_blocks = benchmark_mode.total_blocks();
let buffer_size = self.rpc_block_buffer_size;
// Use a oneshot channel to propagate errors from the spawned task
@@ -83,8 +82,8 @@ impl Command {
}
});
// put results in a summary vec so they can be printed at the end
let mut results = Vec::new();
let mut blocks_processed = 0u64;
let total_benchmark_duration = Instant::now();
let mut total_wait_time = Duration::ZERO;
@@ -106,12 +105,7 @@ impl Command {
call_new_payload(&auth_provider, version, params).await?;
let new_payload_result = NewPayloadResult { gas_used, latency: start.elapsed() };
blocks_processed += 1;
let progress = match total_blocks {
Some(total) => format!("{blocks_processed}/{total}"),
None => format!("{blocks_processed}"),
};
info!(target: "reth-bench", progress, %new_payload_result);
info!(target: "reth-bench", %new_payload_result);
// current duration since the start of the benchmark minus the time
// waiting for blocks

View File

@@ -154,18 +154,12 @@ impl PersistenceSubscription {
}
/// Establishes a websocket connection and subscribes to `reth_subscribePersistedBlock`.
///
/// The `keepalive_interval` is set to match `persistence_timeout` so that the `WebSocket`
/// connection is not dropped during long MDBX commits that block the server from responding
/// to pings.
pub(crate) async fn setup_persistence_subscription(
ws_url: Url,
persistence_timeout: Duration,
) -> eyre::Result<PersistenceSubscription> {
info!(target: "reth-bench", "Connecting to WebSocket at {} for persistence subscription", ws_url);
let ws_connect =
WsConnect::new(ws_url.to_string()).with_keepalive_interval(persistence_timeout);
let ws_connect = WsConnect::new(ws_url.to_string());
let client = RpcClient::connect_pubsub(ws_connect)
.await
.wrap_err("Failed to connect to WebSocket RPC endpoint")?;

View File

@@ -14,7 +14,6 @@
use crate::{
authenticated_transport::AuthenticatedTransportConnect,
bench::{
helpers::parse_duration,
output::{
write_benchmark_results, CombinedResult, GasRampPayloadFile, NewPayloadResult,
TotalGasOutput, TotalGasRow,
@@ -31,6 +30,7 @@ use alloy_rpc_client::ClientBuilder;
use alloy_rpc_types_engine::{ExecutionPayloadEnvelopeV4, ForkchoiceState, JwtSecret};
use clap::Parser;
use eyre::Context;
use humantime::parse_duration;
use reth_cli_runner::CliContext;
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
use reth_node_api::EngineApiMessageVersion;
@@ -78,9 +78,6 @@ pub struct Command {
output: Option<PathBuf>,
/// How long to wait after a forkchoice update before sending the next payload.
///
/// Accepts a duration string (e.g. `100ms`, `2s`) or a bare integer treated as
/// milliseconds (e.g. `400`).
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, verbatim_doc_comment)]
wait_time: Option<Duration>,
@@ -169,7 +166,7 @@ impl Command {
let mut waiter = match (self.wait_time, self.wait_for_persistence) {
(Some(duration), true) => {
let ws_url = derive_ws_rpc_url(self.ws_rpc_url.as_deref(), &self.engine_rpc_url)?;
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
let sub = setup_persistence_subscription(ws_url).await?;
Some(PersistenceWaiter::with_duration_and_subscription(
duration,
sub,
@@ -180,7 +177,7 @@ impl Command {
(Some(duration), false) => Some(PersistenceWaiter::with_duration(duration)),
(None, true) => {
let ws_url = derive_ws_rpc_url(self.ws_rpc_url.as_deref(), &self.engine_rpc_url)?;
let sub = setup_persistence_subscription(ws_url, self.persistence_timeout).await?;
let sub = setup_persistence_subscription(ws_url).await?;
Some(PersistenceWaiter::with_subscription(
sub,
self.persistence_threshold,
@@ -341,8 +338,7 @@ impl Command {
};
let current_duration = total_benchmark_duration.elapsed();
let progress = format!("{}/{}", i + 1, payloads.len());
info!(target: "reth-bench", progress, %combined_result);
info!(target: "reth-bench", %combined_result);
if let Some(w) = &mut waiter {
w.on_block(block_number).await?;

View File

@@ -20,19 +20,6 @@ impl BenchMode {
}
}
/// Returns the total number of blocks in the benchmark, if known.
///
/// For [`BenchMode::Range`] this is the length of the range.
/// For [`BenchMode::Continuous`] the total is unbounded, so `None` is returned.
pub const fn total_blocks(&self) -> Option<u64> {
match self {
Self::Continuous(_) => None,
Self::Range(range) => {
Some(range.end().saturating_sub(*range.start()).saturating_add(1))
}
}
}
/// Create a [`BenchMode`] from optional `from` and `to` fields.
pub fn new(from: Option<u64>, to: Option<u64>, latest_block: u64) -> Result<Self, eyre::Error> {
// If neither `--from` nor `--to` are provided, we will run the benchmark continuously,

View File

@@ -33,6 +33,7 @@ reth-chainspec.workspace = true
reth-primitives.workspace = true
reth-db = { workspace = true, features = ["mdbx"] }
reth-provider.workspace = true
reth-evm.workspace = true
reth-revm.workspace = true
reth-transaction-pool.workspace = true
reth-cli-runner.workspace = true
@@ -52,31 +53,32 @@ reth-payload-primitives.workspace = true
reth-node-api.workspace = true
reth-node-core.workspace = true
reth-ethereum-payload-builder.workspace = true
reth-ethereum-primitives.workspace = true
reth-node-ethereum.workspace = true
reth-node-builder.workspace = true
reth-node-metrics.workspace = true
reth-consensus.workspace = true
reth-tokio-util.workspace = true
reth-ress-protocol.workspace = true
reth-ress-provider.workspace = true
# alloy
alloy-primitives.workspace = true
alloy-rpc-types = { workspace = true, features = ["engine"] }
# tracing
tracing.workspace = true
# async
tokio = { workspace = true, features = ["sync", "macros", "time", "rt-multi-thread"] }
# misc
aquamarine.workspace = true
clap = { workspace = true, features = ["derive", "env"] }
eyre.workspace = true
[dev-dependencies]
alloy-node-bindings = "1.6.3"
alloy-provider = { workspace = true, features = ["reqwest"] }
alloy-rpc-types-eth.workspace = true
backon.workspace = true
serde_json.workspace = true
tempfile.workspace = true
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
toml.workspace = true
[features]
default = [
@@ -87,7 +89,6 @@ default = [
"js-tracer",
"keccak-cache-global",
"asm-keccak",
"min-debug-logs",
"rocksdb",
]
@@ -113,12 +114,10 @@ asm-keccak = [
"reth-primitives/asm-keccak",
"reth-ethereum-cli/asm-keccak",
"reth-node-ethereum/asm-keccak",
"alloy-primitives/asm-keccak",
]
keccak-cache-global = [
"reth-node-core/keccak-cache-global",
"reth-node-ethereum/keccak-cache-global",
"alloy-primitives/keccak-cache-global",
]
jemalloc = [
"reth-cli-util/jemalloc",

View File

@@ -51,9 +51,6 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg))]
// Used in feature flags only (`asm-keccak`, `keccak-cache-global`)
use alloy_primitives as _;
pub mod cli;
/// Re-exported utils.
@@ -208,9 +205,12 @@ pub mod rpc {
}
}
/// Ress subprotocol installation.
pub mod ress;
// re-export for convenience
#[doc(inline)]
pub use reth_cli_runner::{CliContext, CliRunner};
pub use reth_cli_runner::{tokio_runtime, CliContext, CliRunner};
// for rendering diagrams
use aquamarine as _;
@@ -218,4 +218,3 @@ use aquamarine as _;
// used in main
use clap as _;
use reth_cli_util as _;
use tracing as _;

View File

@@ -8,8 +8,9 @@ static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::ne
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
use clap::Parser;
use reth::cli::Cli;
use reth::{args::RessArgs, cli::Cli, ress::install_ress_subprotocol};
use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
use reth_node_builder::NodeHandle;
use reth_node_ethereum::EthereumNode;
use tracing::info;
@@ -21,12 +22,27 @@ fn main() {
unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
}
if let Err(err) = Cli::<EthereumChainSpecParser>::parse().run(async move |builder, _| {
info!(target: "reth::cli", "Launching node");
let handle = builder.node(EthereumNode::default()).launch_with_debug_capabilities().await?;
if let Err(err) =
Cli::<EthereumChainSpecParser, RessArgs>::parse().run(async move |builder, ress_args| {
info!(target: "reth::cli", "Launching node");
let NodeHandle { node, node_exit_future } =
builder.node(EthereumNode::default()).launch_with_debug_capabilities().await?;
handle.wait_for_node_exit().await
}) {
// Install ress subprotocol.
if ress_args.enabled {
install_ress_subprotocol(
ress_args,
node.provider,
node.evm_config,
node.network,
node.task_executor,
node.add_ons_handle.engine_events.new_listener(),
)?;
}
node_exit_future.await
})
{
eprintln!("Error: {err:?}");
std::process::exit(1);
}

67
bin/reth/src/ress.rs Normal file
View File

@@ -0,0 +1,67 @@
use reth_ethereum_primitives::EthPrimitives;
use reth_evm::ConfigureEvm;
use reth_network::{protocol::IntoRlpxSubProtocol, NetworkProtocols};
use reth_network_api::FullNetwork;
use reth_node_api::ConsensusEngineEvent;
use reth_node_core::args::RessArgs;
use reth_provider::providers::{BlockchainProvider, ProviderNodeTypes};
use reth_ress_protocol::{NodeType, ProtocolState, RessProtocolHandler};
use reth_ress_provider::{maintain_pending_state, PendingState, RethRessProtocolProvider};
use reth_tasks::TaskExecutor;
use reth_tokio_util::EventStream;
use tokio::sync::mpsc;
use tracing::*;
/// Install `ress` subprotocol if it's enabled.
pub fn install_ress_subprotocol<P, E, N>(
args: RessArgs,
provider: BlockchainProvider<P>,
evm_config: E,
network: N,
task_executor: TaskExecutor,
engine_events: EventStream<ConsensusEngineEvent<EthPrimitives>>,
) -> eyre::Result<()>
where
P: ProviderNodeTypes<Primitives = EthPrimitives>,
E: ConfigureEvm<Primitives = EthPrimitives> + Clone + 'static,
N: FullNetwork + NetworkProtocols,
{
info!(target: "reth::cli", "Installing ress subprotocol");
let pending_state = PendingState::default();
// Spawn maintenance task for pending state.
task_executor.spawn(maintain_pending_state(
engine_events,
provider.clone(),
pending_state.clone(),
));
let (tx, mut rx) = mpsc::unbounded_channel();
let provider = RethRessProtocolProvider::new(
provider,
evm_config,
Box::new(task_executor.clone()),
args.max_witness_window,
args.witness_max_parallel,
args.witness_cache_size,
pending_state,
)?;
network.add_rlpx_sub_protocol(
RessProtocolHandler {
provider,
node_type: NodeType::Stateful,
peers_handle: network.peers_handle().clone(),
max_active_connections: args.max_active_connections,
state: ProtocolState::new(tx),
}
.into_rlpx_sub_protocol(),
);
info!(target: "reth::cli", "Ress subprotocol support enabled");
task_executor.spawn(async move {
while let Some(event) = rx.recv().await {
trace!(target: "reth::ress", ?event, "Received ress event");
}
});
Ok(())
}

View File

@@ -1,255 +0,0 @@
#![allow(missing_docs)]
use std::process::Command;
const RETH: &str = env!("CARGO_BIN_EXE_reth");
// ── Helpers ──────────────────────────────────────────────────────────────────
/// Runs `reth <args>` and returns stdout, asserting exit code 0.
///
/// Tracing is suppressed via `RUST_LOG=off` so that log lines emitted during
/// binary startup don't pollute stdout-based assertions.
#[track_caller]
fn reth_ok(args: &[&str]) -> String {
let output = Command::new(RETH).env("RUST_LOG", "off").args(args).output().unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "args {args:?} failed.\nstdout: {stdout}\nstderr: {stderr}");
stdout.into_owned()
}
/// Spawns an isolated dev-mode reth node.
///
/// Discovery is disabled and peer limits are zeroed so the node is fully
/// isolated. Each call gets a unique temporary data directory so that
/// concurrent test runs never collide on the default `reth/dev/` path.
fn spawn_dev() -> (alloy_node_bindings::RethInstance, tempfile::TempDir) {
use alloy_node_bindings::Reth;
let datadir = tempfile::tempdir().expect("failed to create temp dir");
let instance = Reth::at(RETH)
.dev()
.disable_discovery()
.data_dir(datadir.path())
.args(["--max-outbound-peers", "0", "--max-inbound-peers", "0"])
.spawn();
// Return the TempDir alongside the instance so it lives as long as the node.
(instance, datadir)
}
// ── Original tests (from PR #22069) ──────────────────────────────────────────
#[test]
fn help() {
let stdout = reth_ok(&["--help"]);
assert!(stdout.contains("Usage"), "stdout: {stdout}");
assert!(stdout.contains("node"), "stdout: {stdout}");
}
#[test]
fn version() {
let stdout = reth_ok(&["--version"]);
assert!(stdout.to_lowercase().contains("reth"), "stdout: {stdout}");
}
#[test]
fn node_help() {
let stdout = reth_ok(&["node", "--help"]);
assert!(stdout.contains("--dev"), "stdout: {stdout}");
assert!(stdout.contains("--http"), "stdout: {stdout}");
}
#[test]
fn unknown_subcommand() {
let output = Command::new(RETH).arg("definitely-not-a-cmd").output().unwrap();
assert!(!output.status.success());
}
#[test]
fn unknown_flag() {
let output = Command::new(RETH).args(["node", "--no-such-flag"]).output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success());
assert!(stderr.contains("--no-such-flag"), "stderr: {stderr}");
}
#[tokio::test]
async fn dev_node_eth_syncing() {
use alloy_provider::{Provider, ProviderBuilder};
let (reth, _datadir) = spawn_dev();
let provider = ProviderBuilder::new().connect_http(reth.endpoint().parse().unwrap());
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let _syncing = provider.syncing().await.expect("eth_syncing failed");
}
// ── Subcommand --help coverage ───────────────────────────────────────────────
//
// Every registered subcommand must produce valid --help output. This catches
// clap wiring regressions (e.g. a missing field, a conflicting arg name, or a
// broken `help_message()` call) that would otherwise only surface when a user
// runs the command.
#[test]
fn init_help() {
let stdout = reth_ok(&["init", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn init_state_help() {
let stdout = reth_ok(&["init-state", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn import_help() {
let stdout = reth_ok(&["import", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn import_era_help() {
let stdout = reth_ok(&["import-era", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn export_era_help() {
let stdout = reth_ok(&["export-era", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn dump_genesis_help() {
let stdout = reth_ok(&["dump-genesis", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn db_help() {
let stdout = reth_ok(&["db", "--help"]);
assert!(stdout.contains("stats"), "stdout: {stdout}");
}
#[test]
fn stage_help() {
let stdout = reth_ok(&["stage", "--help"]);
assert!(stdout.contains("run"), "stdout: {stdout}");
}
#[test]
fn p2p_help() {
let stdout = reth_ok(&["p2p", "--help"]);
assert!(stdout.contains("header"), "stdout: {stdout}");
}
#[test]
fn config_help() {
let stdout = reth_ok(&["config", "--help"]);
assert!(stdout.contains("--default"), "stdout: {stdout}");
}
#[test]
fn prune_help() {
let stdout = reth_ok(&["prune", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn download_help() {
let stdout = reth_ok(&["download", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
#[test]
fn re_execute_help() {
let stdout = reth_ok(&["re-execute", "--help"]);
assert!(stdout.contains("--chain"), "stdout: {stdout}");
}
// ── `config --default` outputs valid TOML ────────────────────────────────────
#[test]
fn config_default_valid_toml() {
let stdout = reth_ok(&["config", "--default"]);
let parsed: toml::Value =
toml::from_str(&stdout).expect("config --default did not produce valid TOML");
// The default config must contain the [stages] table — this is the heart of
// the pipeline configuration and its absence would indicate a serialization
// regression.
assert!(parsed.get("stages").is_some(), "missing [stages] in config output");
}
// ── `dump-genesis` outputs valid JSON ────────────────────────────────────────
#[test]
fn dump_genesis_mainnet_valid_json() {
let stdout = reth_ok(&["dump-genesis"]);
let genesis: serde_json::Value =
serde_json::from_str(&stdout).expect("dump-genesis did not produce valid JSON");
assert!(genesis.get("nonce").is_some(), "missing nonce in genesis JSON");
assert!(genesis.get("alloc").is_some(), "missing alloc in genesis JSON");
}
#[test]
fn dump_genesis_sepolia_valid_json() {
let stdout = reth_ok(&["dump-genesis", "--chain", "sepolia"]);
let genesis: serde_json::Value = serde_json::from_str(&stdout)
.expect("dump-genesis --chain sepolia did not produce valid JSON");
assert!(genesis.get("alloc").is_some(), "missing alloc in sepolia genesis JSON");
}
// ── Dev node: send transaction round-trip ────────────────────────────────────
//
// Exercises the full pipeline: RPC submission → mempool → sealing → execution →
// receipt retrieval. Uses the pre-funded dev account so no genesis customization
// is required.
#[tokio::test]
async fn dev_node_send_tx_and_mine() {
use alloy_primitives::{Address, U256};
use alloy_provider::{Provider, ProviderBuilder};
use alloy_rpc_types_eth::TransactionRequest;
let (reth, _datadir) = spawn_dev();
let provider = ProviderBuilder::new().connect_http(reth.endpoint().parse().unwrap());
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
// Dev mode pre-funds the first dev account.
let accounts = provider.get_accounts().await.expect("eth_accounts failed");
assert!(!accounts.is_empty(), "dev node should expose at least one account");
let sender = accounts[0];
let recipient = Address::with_last_byte(0x42);
let tx = TransactionRequest::default().from(sender).to(recipient).value(U256::from(1_000_000));
let tx_hash = provider.send_transaction(tx).await.expect("eth_sendTransaction failed");
// In dev/instant-mine mode the node seals a block for each transaction, so
// the receipt becomes available almost immediately.
let receipt = tx_hash.get_receipt().await.expect("failed to get receipt");
assert!(receipt.status(), "transaction should have succeeded");
assert_eq!(receipt.to, Some(recipient));
assert!(receipt.block_number.unwrap() > 0, "receipt should be in a mined block");
// Verify the transfer actually mutated state.
let balance = provider.get_balance(recipient).await.expect("eth_getBalance failed");
assert_eq!(balance, U256::from(1_000_000));
}
const fn main() {}

View File

@@ -312,11 +312,6 @@ impl DeferredTrieData {
/// Given that invariant, circular wait dependencies are impossible.
#[instrument(level = "debug", target = "engine::tree::deferred_trie", skip_all)]
pub fn wait_cloned(&self) -> ComputedTrieData {
#[cfg(feature = "rayon")]
debug_assert!(
rayon::current_thread_index().is_none(),
"wait_cloned must not be called from a rayon worker thread"
);
let mut state = self.state.lock();
match &mut *state {
// If the deferred trie data is ready, return the cached result.

View File

@@ -1061,14 +1061,6 @@ mod tests {
) -> ProviderResult<Option<StorageValue>> {
Ok(None)
}
fn storage_by_hashed_key(
&self,
_address: Address,
_hashed_storage_key: StorageKey,
) -> ProviderResult<Option<StorageValue>> {
Ok(None)
}
}
impl BytecodeReader for MockStateProvider {

View File

@@ -223,26 +223,6 @@ impl<N: NodePrimitives> StateProvider for MemoryOverlayStateProviderRef<'_, N> {
self.historical.storage(address, storage_key)
}
fn storage_by_hashed_key(
&self,
address: Address,
hashed_storage_key: StorageKey,
) -> ProviderResult<Option<StorageValue>> {
let hashed_address = keccak256(address);
let state = &self.trie_input().state;
if let Some(hs) = state.storages.get(&hashed_address) {
if let Some(value) = hs.storage.get(&hashed_storage_key) {
return Ok(Some(*value));
}
if hs.wiped {
return Ok(Some(StorageValue::ZERO));
}
}
self.historical.storage_by_hashed_key(address, hashed_storage_key)
}
}
impl<N: NodePrimitives> BytecodeReader for MemoryOverlayStateProviderRef<'_, N> {

View File

@@ -66,8 +66,7 @@ pub trait RethCli: Sized {
F: FnOnce(Self, CliRunner) -> R,
{
let cli = Self::parse_args()?;
let runner = CliRunner::try_default_runtime()
.map_err(|e| Error::raw(clap::error::ErrorKind::Io, e))?;
let runner = CliRunner::try_default_runtime()?;
Ok(cli.with_runner(f, runner))
}

View File

@@ -134,4 +134,4 @@ arbitrary = [
]
rocksdb = ["reth-db-common/rocksdb", "reth-stages/rocksdb", "reth-provider/rocksdb", "reth-prune/rocksdb"]
edge = ["rocksdb"]
edge = ["rocksdb", "reth-db-common/edge", "reth-provider/edge"]

View File

@@ -19,7 +19,7 @@ use reth_node_builder::{
Node, NodeComponents, NodeComponentsBuilder, NodeTypes, NodeTypesWithDBAdapter,
};
use reth_node_core::{
args::{DatabaseArgs, DatadirArgs, StaticFilesArgs, StorageArgs},
args::{DatabaseArgs, DatadirArgs, RocksDbArgs, StaticFilesArgs, StorageArgs},
dirs::{ChainPath, DataDirPath},
};
use reth_provider::{
@@ -67,35 +67,70 @@ pub struct EnvironmentArgs<C: ChainSpecParser> {
#[command(flatten)]
pub static_files: StaticFilesArgs,
/// All `RocksDB` related arguments
#[command(flatten)]
pub rocksdb: RocksDbArgs,
/// Storage mode configuration (v2 vs v1/legacy)
#[command(flatten)]
pub storage: StorageArgs,
}
impl<C: ChainSpecParser> EnvironmentArgs<C> {
/// Returns the effective storage settings derived from `--storage.v2`.
/// Returns the effective storage settings derived from `--storage.v2`, static-file, and
/// `RocksDB` CLI args.
///
/// The base storage mode is determined by `--storage.v2`:
/// - When `--storage.v2` is set: uses [`StorageSettings::v2()`] defaults
/// - Otherwise: uses [`StorageSettings::base()`] defaults
/// - Otherwise: uses [`StorageSettings::v1()`] defaults
///
/// Individual `--static-files.*` and `--rocksdb.*` flags override the base when explicitly set.
pub fn storage_settings(&self) -> StorageSettings {
if self.storage.v2 {
StorageSettings::v2()
} else {
StorageSettings::base()
let mut s = if self.storage.v2 { StorageSettings::v2() } else { StorageSettings::base() };
// Apply static files overrides (only when explicitly set)
if let Some(v) = self.static_files.receipts {
s = s.with_receipts_in_static_files(v);
}
if let Some(v) = self.static_files.transaction_senders {
s = s.with_transaction_senders_in_static_files(v);
}
if let Some(v) = self.static_files.account_changesets {
s = s.with_account_changesets_in_static_files(v);
}
if let Some(v) = self.static_files.storage_changesets {
s = s.with_storage_changesets_in_static_files(v);
}
// Apply rocksdb overrides
// --rocksdb.all sets all rocksdb flags to true
if self.rocksdb.all {
s = s
.with_transaction_hash_numbers_in_rocksdb(true)
.with_storages_history_in_rocksdb(true)
.with_account_history_in_rocksdb(true);
}
// Individual rocksdb flags override --rocksdb.all when explicitly set
if let Some(v) = self.rocksdb.tx_hash {
s = s.with_transaction_hash_numbers_in_rocksdb(v);
}
if let Some(v) = self.rocksdb.storages_history {
s = s.with_storages_history_in_rocksdb(v);
}
if let Some(v) = self.rocksdb.account_history {
s = s.with_account_history_in_rocksdb(v);
}
s
}
/// Initializes environment according to [`AccessRights`] and returns an instance of
/// [`Environment`].
///
/// Internally builds a [`reth_tasks::Runtime`] attached to the current tokio handle for
/// parallel storage I/O.
pub fn init<N: CliNodeTypes>(&self, access: AccessRights) -> eyre::Result<Environment<N>>
where
C: ChainSpecParser<ChainSpec = N::ChainSpec>,
{
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
let data_dir = self.datadir.clone().resolve_datadir(self.chain.chain());
let db_path = data_dir.db();
let sf_path = data_dir.static_files();
@@ -151,7 +186,7 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
.build()?;
let provider_factory =
self.create_provider_factory(&config, db, sfp, rocksdb_provider, access, runtime)?;
self.create_provider_factory(&config, db, sfp, rocksdb_provider, access)?;
if access.is_read_write() {
debug!(target: "reth::cli", chain=%self.chain.chain(), genesis=?self.chain.genesis_hash(), "Initializing genesis");
init_genesis_with_settings(&provider_factory, self.storage_settings())?;
@@ -172,7 +207,6 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
static_file_provider: StaticFileProvider<N::Primitives>,
rocksdb_provider: RocksDBProvider,
access: AccessRights,
runtime: reth_tasks::Runtime,
) -> eyre::Result<ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>>>
where
C: ChainSpecParser<ChainSpec = N::ChainSpec>,
@@ -183,7 +217,6 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
self.chain.clone(),
static_file_provider,
rocksdb_provider,
runtime,
)?
.with_prune_modes(prune_modes.clone());

View File

@@ -5,7 +5,6 @@ use reth_codecs::Compact;
use reth_db_api::{cursor::DbDupCursorRO, database::Database, tables, transaction::DbTx};
use reth_db_common::DbTool;
use reth_node_builder::NodeTypesWithDB;
use reth_storage_api::StorageSettingsCache;
use std::time::{Duration, Instant};
use tracing::info;
@@ -17,22 +16,26 @@ const LOG_INTERVAL: Duration = Duration::from_secs(5);
pub struct Command {
/// The account address to check storage for
address: Address,
/// Use hashed state tables (HashedStorages) instead of plain state
#[arg(long)]
hashed: bool,
}
impl Command {
/// Execute `db account-storage` command
pub fn execute<N: NodeTypesWithDB>(self, tool: &DbTool<N>) -> eyre::Result<()> {
let address = self.address;
let use_hashed_state = tool.provider_factory.cached_storage_settings().use_hashed_state();
let use_hashed = self.hashed;
let hashed_address = keccak256(address);
let (slot_count, storage_size) = if use_hashed_state {
let hashed_address = keccak256(address);
tool.provider_factory.db_ref().view(|tx| {
let (slot_count, storage_size) = tool.provider_factory.db_ref().view(|tx| {
let mut count = 0usize;
let mut total_value_bytes = 0usize;
let mut last_log = Instant::now();
if use_hashed {
let mut cursor = tx.cursor_dup_read::<tables::HashedStorages>()?;
let mut count = 0usize;
let mut total_value_bytes = 0usize;
let mut last_log = Instant::now();
let walker = cursor.walk_dup(Some(hashed_address), None)?;
for entry in walker {
let (_, storage_entry) = entry?;
@@ -44,7 +47,7 @@ impl Command {
if last_log.elapsed() >= LOG_INTERVAL {
info!(
target: "reth::cli",
address = %address,
hashed_address = %hashed_address,
slots = count,
key = %storage_entry.key,
"Processing hashed storage slots"
@@ -52,25 +55,16 @@ impl Command {
last_log = Instant::now();
}
}
// HashedStorages uses 32-byte B256 key
let total_size = if count > 0 { 32 + total_value_bytes } else { 0 };
Ok::<_, eyre::Report>((count, total_size))
})??
} else {
tool.provider_factory.db_ref().view(|tx| {
} else {
let mut cursor = tx.cursor_dup_read::<tables::PlainStorageState>()?;
let mut count = 0usize;
let mut total_value_bytes = 0usize;
let mut last_log = Instant::now();
// Walk all storage entries for this address
let walker = cursor.walk_dup(Some(address), None)?;
for entry in walker {
let (_, storage_entry) = entry?;
count += 1;
let mut buf = Vec::new();
// StorageEntry encodes as: 32 bytes (key/subkey uncompressed) + compressed U256
let entry_len = storage_entry.to_compact(&mut buf);
total_value_bytes += entry_len;
@@ -85,26 +79,24 @@ impl Command {
last_log = Instant::now();
}
}
// Add 20 bytes for the Address key (stored once per account in dupsort)
// PlainStorageState uses 20-byte Address key
let total_size = if count > 0 { 20 + total_value_bytes } else { 0 };
Ok::<_, eyre::Report>((count, total_size))
})??
};
}
})??;
let hashed_address = keccak256(address);
let state_source = if use_hashed { "hashed" } else { "plain" };
println!("Account: {address}");
println!("Hashed address: {hashed_address}");
println!("State source: {state_source}");
println!("Storage slots: {slot_count}");
if use_hashed_state {
println!("Hashed storage size: {} (estimated)", human_bytes(storage_size as f64));
} else {
// Estimate hashed storage size: 32-byte B256 key instead of 20-byte Address
println!("Storage size: {} (estimated)", human_bytes(storage_size as f64));
if !use_hashed {
// When querying plain state, also estimate what hashed would be
let hashed_size_estimate = if slot_count > 0 { storage_size + 12 } else { 0 };
let total_estimate = storage_size + hashed_size_estimate;
println!("Plain storage size: {} (estimated)", human_bytes(storage_size as f64));
println!(
"Hashed storage size: {} (estimated)",
human_bytes(hashed_size_estimate as f64)
@@ -131,5 +123,17 @@ mod tests {
cmd.address,
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045".parse::<Address>().unwrap()
);
assert!(!cmd.hashed);
}
#[test]
fn parse_hashed_flag() {
let cmd = Command::try_parse_from([
"account-storage",
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"--hashed",
])
.unwrap();
assert!(cmd.hashed);
}
}

View File

@@ -1,61 +0,0 @@
use clap::Parser;
use reth_db::mdbx::{self, ffi};
use std::path::PathBuf;
/// Copies the MDBX database to a new location.
///
/// Equivalent to the standalone `mdbx_copy` tool but bundled into reth.
#[derive(Parser, Debug)]
pub struct Command {
/// Destination path for the database copy.
dest: PathBuf,
/// Compact the database while copying (reclaims free space).
#[arg(short, long)]
compact: bool,
/// Force dynamic size for the destination database.
#[arg(short = 'd', long)]
force_dynamic_size: bool,
/// Throttle to avoid MVCC pressure on writers.
#[arg(short = 'p', long)]
throttle_mvcc: bool,
}
impl Command {
/// Execute `db copy` command
pub fn execute(self, db: &mdbx::DatabaseEnv) -> eyre::Result<()> {
let mut flags: ffi::MDBX_copy_flags_t = ffi::MDBX_CP_DEFAULTS;
if self.compact {
flags |= ffi::MDBX_CP_COMPACT;
}
if self.force_dynamic_size {
flags |= ffi::MDBX_CP_FORCE_DYNAMIC_SIZE;
}
if self.throttle_mvcc {
flags |= ffi::MDBX_CP_THROTTLE_MVCC;
}
let dest = self
.dest
.to_str()
.ok_or_else(|| eyre::eyre!("destination path must be valid UTF-8"))?;
let dest_cstr = std::ffi::CString::new(dest)?;
println!("Copying database to {} ...", self.dest.display());
let rc = db.with_raw_env_ptr(|env_ptr| unsafe {
ffi::mdbx_env_copy(env_ptr, dest_cstr.as_ptr(), flags)
});
if rc != 0 {
eyre::bail!("mdbx_env_copy failed with error code {rc}: {}", unsafe {
std::ffi::CStr::from_ptr(ffi::mdbx_strerror(rc)).to_string_lossy()
});
}
println!("Done.");
Ok(())
}
}

View File

@@ -98,8 +98,7 @@ impl Command {
)?;
if let Some(entry) = entry {
let se: reth_primitives_traits::StorageEntry = entry.into();
println!("{}", serde_json::to_string_pretty(&se)?);
println!("{}", serde_json::to_string_pretty(&entry)?);
} else {
error!(target: "reth::cli", "No content for the given table key.");
}
@@ -107,14 +106,7 @@ impl Command {
}
let changesets = provider.storage_changeset(key.block_number())?;
let serializable: Vec<_> = changesets
.into_iter()
.map(|(addr, entry)| {
let se: reth_primitives_traits::StorageEntry = entry.into();
(addr, se)
})
.collect();
println!("{}", serde_json::to_string_pretty(&serializable)?);
println!("{}", serde_json::to_string_pretty(&changesets)?);
return Ok(());
}

View File

@@ -12,7 +12,6 @@ use std::{
mod account_storage;
mod checksum;
mod clear;
mod copy;
mod diff;
mod get;
mod list;
@@ -43,8 +42,6 @@ pub enum Subcommands {
List(list::Command),
/// Calculates the content checksum of a table or static file segment
Checksum(checksum::Command),
/// Copies the MDBX database to a new location (bundled mdbx_copy)
Copy(copy::Command),
/// Create a diff between two database tables or two entire databases.
Diff(diff::Command),
/// Gets the content of a table for the given key
@@ -73,23 +70,23 @@ pub enum Subcommands {
State(state::Command),
}
/// Initializes a provider factory with specified access rights, and then execute with the provided
/// command
macro_rules! db_exec {
($env:expr, $tool:ident, $N:ident, $access_rights:expr, $command:block) => {
let Environment { provider_factory, .. } = $env.init::<$N>($access_rights)?;
let $tool = DbTool::new(provider_factory)?;
$command;
};
}
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
/// Execute `db` command
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(
self,
ctx: CliContext,
) -> eyre::Result<()> {
/// Initializes a provider factory with specified access rights, and then executes the
/// provided command.
macro_rules! db_exec {
($env:expr, $tool:ident, $N:ident, $access_rights:expr, $command:block) => {
let Environment { provider_factory, .. } = $env.init::<$N>($access_rights)?;
let $tool = DbTool::new(provider_factory)?;
$command;
};
}
let data_dir = self.env.datadir.clone().resolve_datadir(self.env.chain.chain());
let db_path = data_dir.db();
let static_files_path = data_dir.static_files();
@@ -127,11 +124,6 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
command.execute(&tool)?;
});
}
Subcommands::Copy(command) => {
db_exec!(self.env, tool, N, AccessRights::RO, {
command.execute(tool.provider_factory.db_ref())?;
});
}
Subcommands::Diff(command) => {
db_exec!(self.env, tool, N, AccessRights::RO, {
command.execute(&tool)?;

View File

@@ -64,7 +64,7 @@ impl Command {
let executor = task_executor.clone();
let pprof_dump_dir = data_dir.pprof_dumps();
let handle = task_executor.spawn_critical_task("metrics server", async move {
let handle = task_executor.spawn_critical("metrics server", async move {
let config = MetricServerConfig::new(
listen_addr,
VersionInfo {

View File

@@ -39,12 +39,38 @@ enum Subcommands {
#[derive(Debug, Clone, Copy, Subcommand)]
#[clap(rename_all = "snake_case")]
pub enum SetCommand {
/// Enable or disable v2 storage layout
///
/// When enabled, uses static files for receipts/senders/changesets and RocksDB for
/// history indices and transaction hashes. When disabled, uses v1/legacy layout (everything in
/// MDBX).
V2 {
/// Store receipts in static files instead of the database
Receipts {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store transaction senders in static files instead of the database
TransactionSenders {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store account changesets in static files instead of the database
AccountChangesets {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store storage history in rocksdb instead of MDBX
StoragesHistory {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store transaction hash to number mapping in rocksdb instead of MDBX
TransactionHashNumbers {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store account history in rocksdb instead of MDBX
AccountHistory {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store storage changesets in static files instead of the database
StorageChangesets {
#[clap(action(ArgAction::Set))]
value: bool,
},
@@ -87,18 +113,74 @@ impl Command {
println!("No storage settings found, creating new settings.");
}
let mut settings @ StorageSettings { storage_v2: _ } =
settings.unwrap_or_else(StorageSettings::v1);
let mut settings @ StorageSettings {
receipts_in_static_files: _,
transaction_senders_in_static_files: _,
storages_history_in_rocksdb: _,
transaction_hash_numbers_in_rocksdb: _,
account_history_in_rocksdb: _,
account_changesets_in_static_files: _,
storage_changesets_in_static_files: _,
use_hashed_state: _,
} = settings.unwrap_or_else(StorageSettings::v1);
// Update the setting based on the key
match cmd {
SetCommand::V2 { value } => {
if settings.storage_v2 == value {
println!("storage_v2 is already set to {}", value);
SetCommand::Receipts { value } => {
if settings.receipts_in_static_files == value {
println!("receipts_in_static_files is already set to {}", value);
return Ok(());
}
settings.storage_v2 = value;
println!("Set storage_v2 = {}", value);
settings.receipts_in_static_files = value;
println!("Set receipts_in_static_files = {}", value);
}
SetCommand::TransactionSenders { value } => {
if settings.transaction_senders_in_static_files == value {
println!("transaction_senders_in_static_files is already set to {}", value);
return Ok(());
}
settings.transaction_senders_in_static_files = value;
println!("Set transaction_senders_in_static_files = {}", value);
}
SetCommand::AccountChangesets { value } => {
if settings.account_changesets_in_static_files == value {
println!("account_changesets_in_static_files is already set to {}", value);
return Ok(());
}
settings.account_changesets_in_static_files = value;
println!("Set account_changesets_in_static_files = {}", value);
}
SetCommand::StoragesHistory { value } => {
if settings.storages_history_in_rocksdb == value {
println!("storages_history_in_rocksdb is already set to {}", value);
return Ok(());
}
settings.storages_history_in_rocksdb = value;
println!("Set storages_history_in_rocksdb = {}", value);
}
SetCommand::TransactionHashNumbers { value } => {
if settings.transaction_hash_numbers_in_rocksdb == value {
println!("transaction_hash_numbers_in_rocksdb is already set to {}", value);
return Ok(());
}
settings.transaction_hash_numbers_in_rocksdb = value;
println!("Set transaction_hash_numbers_in_rocksdb = {}", value);
}
SetCommand::AccountHistory { value } => {
if settings.account_history_in_rocksdb == value {
println!("account_history_in_rocksdb is already set to {}", value);
return Ok(());
}
settings.account_history_in_rocksdb = value;
println!("Set account_history_in_rocksdb = {}", value);
}
SetCommand::StorageChangesets { value } => {
if settings.storage_changesets_in_static_files == value {
println!("storage_changesets_in_static_files is already set to {}", value);
return Ok(());
}
settings.storage_changesets_in_static_files = value;
println!("Set storage_changesets_in_static_files = {}", value);
}
}

View File

@@ -39,6 +39,10 @@ pub struct Command {
/// Output format (table, json, csv)
#[arg(long, short, default_value = "table")]
format: OutputFormat,
/// Use hashed state tables (HashedStorages) instead of plain state
#[arg(long)]
hashed: bool,
}
impl Command {
@@ -63,70 +67,75 @@ impl Command {
address: Address,
limit: usize,
) -> eyre::Result<()> {
let use_hashed_state = tool.provider_factory.cached_storage_settings().use_hashed_state();
let use_hashed = self.hashed;
let hashed_address = keccak256(address);
let entries = tool.provider_factory.db_ref().view(|tx| {
let (account, walker_entries) = if use_hashed_state {
let hashed_address = keccak256(address);
let account = tx.get::<tables::HashedAccounts>(hashed_address)?;
let mut cursor = tx.cursor_dup_read::<tables::HashedStorages>()?;
let walker = cursor.walk_dup(Some(hashed_address), None)?;
let mut entries = Vec::new();
let mut last_log = Instant::now();
for (idx, entry) in walker.enumerate() {
let (_, storage_entry) = entry?;
if storage_entry.value != U256::ZERO {
entries.push((storage_entry.key, storage_entry.value));
}
if entries.len() >= limit {
break;
}
if last_log.elapsed() >= LOG_INTERVAL {
info!(
target: "reth::cli",
address = %address,
slots_scanned = idx,
"Scanning storage slots"
);
last_log = Instant::now();
}
}
(account, entries)
let account = if use_hashed {
tx.get::<tables::HashedAccounts>(hashed_address)?
} else {
// Get account info
let account = tx.get::<tables::PlainAccountState>(address)?;
// Get storage entries
let mut cursor = tx.cursor_dup_read::<tables::PlainStorageState>()?;
let walker = cursor.walk_dup(Some(address), None)?;
let mut entries = Vec::new();
let mut last_log = Instant::now();
for (idx, entry) in walker.enumerate() {
let (_, storage_entry) = entry?;
if storage_entry.value != U256::ZERO {
entries.push((storage_entry.key, storage_entry.value));
}
if entries.len() >= limit {
break;
}
if last_log.elapsed() >= LOG_INTERVAL {
info!(
target: "reth::cli",
address = %address,
slots_scanned = idx,
"Scanning storage slots"
);
last_log = Instant::now();
}
}
(account, entries)
tx.get::<tables::PlainAccountState>(address)?
};
Ok::<_, eyre::Report>((account, walker_entries))
let mut entries = Vec::new();
let mut last_log = Instant::now();
if use_hashed {
let mut cursor = tx.cursor_dup_read::<tables::HashedStorages>()?;
let walker = cursor.walk_dup(Some(hashed_address), None)?;
for (idx, entry) in walker.enumerate() {
let (_, storage_entry) = entry?;
if storage_entry.value != U256::ZERO {
entries.push((storage_entry.key, storage_entry.value));
}
if entries.len() >= limit {
break;
}
if last_log.elapsed() >= LOG_INTERVAL {
info!(
target: "reth::cli",
hashed_address = %hashed_address,
slots_scanned = idx,
"Scanning hashed storage slots"
);
last_log = Instant::now();
}
}
} else {
let mut cursor = tx.cursor_dup_read::<tables::PlainStorageState>()?;
let walker = cursor.walk_dup(Some(address), None)?;
for (idx, entry) in walker.enumerate() {
let (_, storage_entry) = entry?;
if storage_entry.value != U256::ZERO {
entries.push((storage_entry.key, storage_entry.value));
}
if entries.len() >= limit {
break;
}
if last_log.elapsed() >= LOG_INTERVAL {
info!(
target: "reth::cli",
address = %address,
slots_scanned = idx,
"Scanning storage slots"
);
last_log = Instant::now();
}
}
}
Ok::<_, eyre::Report>((account, entries))
})??;
let (account, storage_entries) = entries;
self.print_results(address, None, account, &storage_entries);
self.print_results(address, None, account, &storage_entries, use_hashed);
Ok(())
}
@@ -145,7 +154,7 @@ impl Command {
// Check storage settings to determine where history is stored
let storage_settings = tool.provider_factory.cached_storage_settings();
let history_in_rocksdb = storage_settings.storage_v2;
let history_in_rocksdb = storage_settings.storages_history_in_rocksdb;
// For historical queries, enumerate keys from history indices only
// (not PlainStorageState, which reflects current state)
@@ -202,7 +211,7 @@ impl Command {
}
}
self.print_results(address, Some(block), account, &entries);
self.print_results(address, Some(block), account, &entries, false);
Ok(())
}
@@ -344,15 +353,22 @@ impl Command {
block: Option<BlockNumber>,
account: Option<reth_primitives_traits::Account>,
storage: &[(alloy_primitives::B256, U256)],
use_hashed: bool,
) {
let state_source = if use_hashed { "hashed" } else { "plain" };
match self.format {
OutputFormat::Table => {
println!("Account: {address}");
if use_hashed {
println!("Hashed address: {}", keccak256(address));
}
if let Some(b) = block {
println!("Block: {b}");
} else {
println!("Block: latest");
}
println!("State source: {state_source}");
println!();
if let Some(acc) = account {
@@ -366,9 +382,10 @@ impl Command {
}
println!();
let slot_header = if use_hashed { "Hashed Slot" } else { "Slot" };
println!("Storage ({} slots):", storage.len());
println!("{:-<130}", "");
println!("{:<66} | {:<64}", "Slot", "Value");
println!("{:<66} | {:<64}", slot_header, "Value");
println!("{:-<130}", "");
for (key, value) in storage {
println!("{key} | {value:#066x}");
@@ -377,7 +394,9 @@ impl Command {
OutputFormat::Json => {
let output = serde_json::json!({
"address": address.to_string(),
"hashed_address": if use_hashed { Some(keccak256(address).to_string()) } else { None },
"block": block,
"state_source": state_source,
"account": account.map(|a| serde_json::json!({
"nonce": a.nonce,
"balance": a.balance.to_string(),
@@ -435,5 +454,17 @@ mod tests {
let cmd = Command::try_parse_from(["state", "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"])
.unwrap();
assert_eq!(cmd.block, None);
assert!(!cmd.hashed);
}
#[test]
fn parse_state_args_hashed() {
let cmd = Command::try_parse_from([
"state",
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"--hashed",
])
.unwrap();
assert!(cmd.hashed);
}
}

View File

@@ -37,14 +37,6 @@ pub struct DownloadDefaults {
pub available_snapshots: Vec<Cow<'static, str>>,
/// Default base URL for snapshots
pub default_base_url: Cow<'static, str>,
/// Default base URL for chain-aware snapshots.
///
/// When set, the chain ID is appended to form the full URL: `{base_url}/{chain_id}`.
/// For example, given a base URL of `https://snapshots.example.com` and chain ID `1`,
/// the resulting URL would be `https://snapshots.example.com/1`.
///
/// Falls back to [`default_base_url`](Self::default_base_url) when `None`.
pub default_chain_aware_base_url: Option<Cow<'static, str>>,
/// Optional custom long help text that overrides the generated help
pub long_help: Option<String>,
}
@@ -68,7 +60,6 @@ impl DownloadDefaults {
Cow::Borrowed("https://publicnode.com/snapshots (full nodes & testnets)"),
],
default_base_url: Cow::Borrowed(MERKLE_BASE_URL),
default_chain_aware_base_url: None,
long_help: None,
}
}
@@ -93,11 +84,9 @@ impl DownloadDefaults {
}
help.push_str(
"\nIf no URL is provided, the latest archive snapshot for the selected chain\nwill be proposed for download from ",
);
help.push_str(
self.default_chain_aware_base_url.as_deref().unwrap_or(&self.default_base_url),
"\nIf no URL is provided, the latest mainnet archive snapshot\nwill be proposed for download from ",
);
help.push_str(self.default_base_url.as_ref());
help.push_str(
".\n\nLocal file:// URLs are also supported for extracting snapshots from disk.",
);
@@ -122,12 +111,6 @@ impl DownloadDefaults {
self
}
/// Set the default chain-aware base URL.
pub fn with_chain_aware_base_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
self.default_chain_aware_base_url = Some(url.into());
self
}
/// Builder: Set custom long help text, overriding the generated help
pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
self.long_help = Some(help.into());
@@ -159,7 +142,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> DownloadCo
let url = match self.url {
Some(url) => url,
None => {
let url = get_latest_snapshot_url(self.env.chain.chain().id()).await?;
let url = get_latest_snapshot_url().await?;
info!(target: "reth::cli", "Using default snapshot URL: {}", url);
url
}
@@ -526,12 +509,8 @@ async fn stream_and_extract(url: &str, target_dir: &Path) -> Result<()> {
}
// Builds default URL for latest mainnet archive snapshot using configured defaults
async fn get_latest_snapshot_url(chain_id: u64) -> Result<String> {
let defaults = DownloadDefaults::get_global();
let base_url = match &defaults.default_chain_aware_base_url {
Some(url) => format!("{url}/{chain_id}"),
None => defaults.default_base_url.to_string(),
};
async fn get_latest_snapshot_url() -> Result<String> {
let base_url = &DownloadDefaults::get_global().default_base_url;
let latest_url = format!("{base_url}/latest.txt");
let filename = Client::new()
.get(latest_url)

View File

@@ -10,8 +10,8 @@ use reth_node_builder::NodeBuilder;
use reth_node_core::{
args::{
DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, EraArgs, MetricArgs,
NetworkArgs, PayloadBuilderArgs, PruningArgs, RpcServerArgs, StaticFilesArgs, StorageArgs,
TxPoolArgs,
NetworkArgs, PayloadBuilderArgs, PruningArgs, RocksDbArgs, RpcServerArgs, StaticFilesArgs,
StorageArgs, TxPoolArgs,
},
node_config::NodeConfig,
version,
@@ -103,6 +103,10 @@ pub struct NodeCommand<C: ChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs
#[command(flatten)]
pub pruning: PruningArgs,
/// All `RocksDB` table routing arguments
#[command(flatten)]
pub rocksdb: RocksDbArgs,
/// Engine cli arguments
#[command(flatten, next_help_heading = "Engine")]
pub engine: EngineArgs,
@@ -115,8 +119,8 @@ pub struct NodeCommand<C: ChainSpecParser, Ext: clap::Args + fmt::Debug = NoArgs
#[command(flatten, next_help_heading = "Static Files")]
pub static_files: StaticFilesArgs,
/// All storage related arguments with --storage prefix
#[command(flatten, next_help_heading = "Storage")]
/// Storage mode configuration (v2 vs v1/legacy)
#[command(flatten)]
pub storage: StorageArgs,
/// Additional cli arguments
@@ -171,6 +175,7 @@ where
db,
dev,
pruning,
rocksdb,
engine,
era,
static_files,
@@ -178,6 +183,9 @@ where
ext,
} = self;
// Validate RocksDB arguments
rocksdb.validate()?;
// set up node config
let mut node_config = NodeConfig {
datadir,
@@ -193,6 +201,7 @@ where
db,
dev,
pruning,
rocksdb,
engine,
era,
static_files,

View File

@@ -76,7 +76,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> PruneComma
// Set up cancellation token for graceful shutdown on Ctrl+C
let cancellation = CancellationToken::new();
let cancellation_clone = cancellation.clone();
ctx.task_executor.spawn_critical_task("prune-ctrl-c", async move {
ctx.task_executor.spawn_critical("prune-ctrl-c", async move {
tokio::signal::ctrl_c().await.expect("failed to listen for ctrl-c");
cancellation_clone.cancel();
});

View File

@@ -9,10 +9,7 @@ use reth_db_api::{
transaction::{DbTx, DbTxMut},
};
use reth_db_common::{
init::{
insert_genesis_account_history, insert_genesis_header, insert_genesis_state,
insert_genesis_storage_history,
},
init::{insert_genesis_header, insert_genesis_history, insert_genesis_state},
DbTool,
};
use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
@@ -45,16 +42,12 @@ impl<C: ChainSpecParser> Command<C> {
let tool = DbTool::new(provider_factory)?;
let static_file_segments = match self.stage {
StageEnum::Headers => vec![StaticFileSegment::Headers],
StageEnum::Bodies => vec![StaticFileSegment::Transactions],
StageEnum::Execution => vec![
StaticFileSegment::Receipts,
StaticFileSegment::AccountChangeSets,
StaticFileSegment::StorageChangeSets,
],
StageEnum::Senders => vec![StaticFileSegment::TransactionSenders],
_ => vec![],
let static_file_segment = match self.stage {
StageEnum::Headers => Some(StaticFileSegment::Headers),
StageEnum::Bodies => Some(StaticFileSegment::Transactions),
StageEnum::Execution => Some(StaticFileSegment::Receipts),
StageEnum::Senders => Some(StaticFileSegment::TransactionSenders),
_ => None,
};
// Calling `StaticFileProviderRW::prune_*` will instruct the writer to prune rows only
@@ -62,33 +55,35 @@ impl<C: ChainSpecParser> Command<C> {
// deleting the jar files, otherwise if the task were to be interrupted after we
// have deleted them, BUT before we have committed the checkpoints to the database, we'd
// lose essential data.
let static_file_provider = tool.provider_factory.static_file_provider();
for segment in static_file_segments {
if let Some(highest_block) = static_file_provider.get_highest_static_file_block(segment)
if let Some(static_file_segment) = static_file_segment {
let static_file_provider = tool.provider_factory.static_file_provider();
if let Some(highest_block) =
static_file_provider.get_highest_static_file_block(static_file_segment)
{
let mut writer = static_file_provider.latest_writer(segment)?;
let mut writer = static_file_provider.latest_writer(static_file_segment)?;
match segment {
match static_file_segment {
StaticFileSegment::Headers => {
// Prune all headers leaving genesis intact.
writer.prune_headers(highest_block)?;
}
StaticFileSegment::Transactions => {
let to_delete = static_file_provider
.get_highest_static_file_tx(segment)
.get_highest_static_file_tx(static_file_segment)
.map(|tx_num| tx_num + 1)
.unwrap_or_default();
writer.prune_transactions(to_delete, 0)?;
}
StaticFileSegment::Receipts => {
let to_delete = static_file_provider
.get_highest_static_file_tx(segment)
.get_highest_static_file_tx(static_file_segment)
.map(|tx_num| tx_num + 1)
.unwrap_or_default();
writer.prune_receipts(to_delete, 0)?;
}
StaticFileSegment::TransactionSenders => {
let to_delete = static_file_provider
.get_highest_static_file_tx(segment)
.get_highest_static_file_tx(static_file_segment)
.map(|tx_num| tx_num + 1)
.unwrap_or_default();
writer.prune_transaction_senders(to_delete, 0)?;
@@ -133,15 +128,8 @@ impl<C: ChainSpecParser> Command<C> {
reset_stage_checkpoint(tx, StageId::SenderRecovery)?;
}
StageEnum::Execution => {
if provider_rw.cached_storage_settings().use_hashed_state() {
tx.clear::<tables::HashedAccounts>()?;
tx.clear::<tables::HashedStorages>()?;
reset_stage_checkpoint(tx, StageId::AccountHashing)?;
reset_stage_checkpoint(tx, StageId::StorageHashing)?;
} else {
tx.clear::<tables::PlainAccountState>()?;
tx.clear::<tables::PlainStorageState>()?;
}
tx.clear::<tables::PlainAccountState>()?;
tx.clear::<tables::PlainStorageState>()?;
tx.clear::<tables::AccountChangeSets>()?;
tx.clear::<tables::StorageChangeSets>()?;
tx.clear::<tables::Bytecodes>()?;
@@ -183,42 +171,29 @@ impl<C: ChainSpecParser> Command<C> {
None,
)?;
}
StageEnum::AccountHistory => {
StageEnum::AccountHistory | StageEnum::StorageHistory => {
let settings = provider_rw.cached_storage_settings();
let rocksdb = tool.provider_factory.rocksdb_provider();
if settings.storage_v2 {
if settings.account_history_in_rocksdb {
rocksdb.clear::<tables::AccountsHistory>()?;
} else {
tx.clear::<tables::AccountsHistory>()?;
}
reset_stage_checkpoint(tx, StageId::IndexAccountHistory)?;
insert_genesis_account_history(
&provider_rw,
self.env.chain.genesis().alloc.iter(),
)?;
}
StageEnum::StorageHistory => {
let settings = provider_rw.cached_storage_settings();
let rocksdb = tool.provider_factory.rocksdb_provider();
if settings.storage_v2 {
if settings.storages_history_in_rocksdb {
rocksdb.clear::<tables::StoragesHistory>()?;
} else {
tx.clear::<tables::StoragesHistory>()?;
}
reset_stage_checkpoint(tx, StageId::IndexAccountHistory)?;
reset_stage_checkpoint(tx, StageId::IndexStorageHistory)?;
insert_genesis_storage_history(
&provider_rw,
self.env.chain.genesis().alloc.iter(),
)?;
insert_genesis_history(&provider_rw, self.env.chain.genesis().alloc.iter())?;
}
StageEnum::TxLookup => {
if provider_rw.cached_storage_settings().storage_v2 {
if provider_rw.cached_storage_settings().transaction_hash_numbers_in_rocksdb {
tool.provider_factory
.rocksdb_provider()
.clear::<tables::TransactionHashNumbers>()?;

View File

@@ -37,14 +37,12 @@ where
unwind_and_copy(db_tool, from, tip_block_number, &output_db, evm_config.clone())?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
runtime,
)?,
to,
from,

View File

@@ -33,14 +33,12 @@ pub(crate) async fn dump_hashing_account_stage<N: ProviderNodeTypes<DB = Databas
unwind_and_copy(db_tool, from, tip_block_number, &output_db)?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
runtime,
)?,
to,
from,

View File

@@ -23,14 +23,12 @@ pub(crate) async fn dump_hashing_storage_stage<N: ProviderNodeTypes<DB = Databas
unwind_and_copy(db_tool, from, tip_block_number, &output_db)?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
runtime,
)?,
to,
from,

View File

@@ -57,14 +57,12 @@ where
unwind_and_copy(db_tool, (from, to), tip_block_number, &output_db, evm_config, consensus)?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
runtime,
)?,
to,
from,

View File

@@ -10,10 +10,9 @@
//! Entrypoint for running commands.
use reth_tasks::{PanickedTaskError, TaskExecutor};
use reth_tasks::{TaskExecutor, TaskManager};
use std::{future::Future, pin::pin, sync::mpsc, time::Duration};
use tokio::task::JoinHandle;
use tracing::{debug, error, info};
use tracing::{debug, error, trace};
/// Executes CLI commands.
///
@@ -21,24 +20,21 @@ use tracing::{debug, error, info};
#[derive(Debug)]
pub struct CliRunner {
config: CliRunnerConfig,
runtime: reth_tasks::Runtime,
tokio_runtime: tokio::runtime::Runtime,
}
impl CliRunner {
/// Attempts to create a new [`CliRunner`] using the default
/// [`Runtime`](reth_tasks::Runtime).
/// Attempts to create a new [`CliRunner`] using the default tokio
/// [`Runtime`](tokio::runtime::Runtime).
///
/// The default runtime is multi-threaded, with both I/O and time drivers enabled.
pub fn try_default_runtime() -> Result<Self, reth_tasks::RuntimeBuildError> {
Self::try_with_runtime_config(reth_tasks::RuntimeConfig::default())
/// The default tokio runtime is multi-threaded, with both I/O and time drivers enabled.
pub fn try_default_runtime() -> Result<Self, std::io::Error> {
Ok(Self { config: CliRunnerConfig::default(), tokio_runtime: tokio_runtime()? })
}
/// Creates a new [`CliRunner`] with the given [`RuntimeConfig`](reth_tasks::RuntimeConfig).
pub fn try_with_runtime_config(
config: reth_tasks::RuntimeConfig,
) -> Result<Self, reth_tasks::RuntimeBuildError> {
let runtime = reth_tasks::RuntimeBuilder::new(config).build()?;
Ok(Self { config: CliRunnerConfig::default(), runtime })
/// Create a new [`CliRunner`] from a provided tokio [`Runtime`](tokio::runtime::Runtime).
pub const fn from_runtime(tokio_runtime: tokio::runtime::Runtime) -> Self {
Self { config: CliRunnerConfig::new(), tokio_runtime }
}
/// Sets the [`CliRunnerConfig`] for this runner.
@@ -52,7 +48,7 @@ impl CliRunner {
where
F: Future<Output = T>,
{
self.runtime.handle().block_on(fut)
self.tokio_runtime.block_on(fut)
}
/// Executes the given _async_ command on the tokio runtime until the command future resolves or
@@ -68,11 +64,12 @@ impl CliRunner {
F: Future<Output = Result<(), E>>,
E: Send + Sync + From<std::io::Error> + From<reth_tasks::PanickedTaskError> + 'static,
{
let (context, task_manager_handle) = cli_context(&self.runtime);
let AsyncCliRunner { context, mut task_manager, tokio_runtime } =
AsyncCliRunner::new(self.tokio_runtime);
// Executes the command until it finished or ctrl-c was fired
let command_res = self.runtime.handle().block_on(run_to_completion_or_panic(
task_manager_handle,
let command_res = tokio_runtime.block_on(run_to_completion_or_panic(
&mut task_manager,
run_until_ctrl_c(command(context)),
));
@@ -80,13 +77,13 @@ impl CliRunner {
error!(target: "reth::cli", "shutting down due to error");
} else {
debug!(target: "reth::cli", "shutting down gracefully");
// after the command has finished or exit signal was received we shutdown the
// runtime which fires the shutdown signal to all tasks spawned via the task
// after the command has finished or exit signal was received we shutdown the task
// manager which fires the shutdown signal to all tasks spawned via the task
// executor and awaiting on tasks spawned with graceful shutdown
self.runtime.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
task_manager.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
}
runtime_shutdown(self.runtime, true);
tokio_shutdown(tokio_runtime, true);
command_res
}
@@ -102,16 +99,17 @@ impl CliRunner {
F: Future<Output = Result<(), E>> + Send + 'static,
E: Send + Sync + From<std::io::Error> + From<reth_tasks::PanickedTaskError> + 'static,
{
let (context, task_manager_handle) = cli_context(&self.runtime);
let AsyncCliRunner { context, mut task_manager, tokio_runtime } =
AsyncCliRunner::new(self.tokio_runtime);
// Spawn the command on the blocking thread pool
let handle = self.runtime.handle().clone();
let handle2 = handle.clone();
let command_handle = handle.spawn_blocking(move || handle2.block_on(command(context)));
let handle = tokio_runtime.handle().clone();
let command_handle =
tokio_runtime.handle().spawn_blocking(move || handle.block_on(command(context)));
// Wait for the command to complete or ctrl-c
let command_res = self.runtime.handle().block_on(run_to_completion_or_panic(
task_manager_handle,
let command_res = tokio_runtime.block_on(run_to_completion_or_panic(
&mut task_manager,
run_until_ctrl_c(
async move { command_handle.await.expect("Failed to join blocking task") },
),
@@ -121,10 +119,10 @@ impl CliRunner {
error!(target: "reth::cli", "shutting down due to error");
} else {
debug!(target: "reth::cli", "shutting down gracefully");
self.runtime.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
task_manager.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
}
runtime_shutdown(self.runtime, true);
tokio_shutdown(tokio_runtime, true);
command_res
}
@@ -135,40 +133,48 @@ impl CliRunner {
F: Future<Output = Result<(), E>>,
E: Send + Sync + From<std::io::Error> + 'static,
{
self.runtime.handle().block_on(run_until_ctrl_c(fut))?;
self.tokio_runtime.block_on(run_until_ctrl_c(fut))?;
Ok(())
}
/// Executes a regular future as a spawned blocking task until completion or until external
/// signal received.
///
/// See [`Runtime::spawn_blocking`](tokio::runtime::Runtime::spawn_blocking).
/// See [`Runtime::spawn_blocking`](tokio::runtime::Runtime::spawn_blocking) .
pub fn run_blocking_until_ctrl_c<F, E>(self, fut: F) -> Result<(), E>
where
F: Future<Output = Result<(), E>> + Send + 'static,
E: Send + Sync + From<std::io::Error> + 'static,
{
let handle = self.runtime.handle().clone();
let handle2 = handle.clone();
let fut = handle.spawn_blocking(move || handle2.block_on(fut));
self.runtime
.handle()
let tokio_runtime = self.tokio_runtime;
let handle = tokio_runtime.handle().clone();
let fut = tokio_runtime.handle().spawn_blocking(move || handle.block_on(fut));
tokio_runtime
.block_on(run_until_ctrl_c(async move { fut.await.expect("Failed to join task") }))?;
runtime_shutdown(self.runtime, false);
tokio_shutdown(tokio_runtime, false);
Ok(())
}
}
/// Extracts the task manager handle from the runtime and creates the [`CliContext`].
fn cli_context(
runtime: &reth_tasks::Runtime,
) -> (CliContext, JoinHandle<Result<(), PanickedTaskError>>) {
let handle =
runtime.take_task_manager_handle().expect("Runtime must contain a TaskManager handle");
let context = CliContext { task_executor: runtime.clone() };
(context, handle)
/// [`CliRunner`] configuration when executing commands asynchronously
struct AsyncCliRunner {
context: CliContext,
task_manager: TaskManager,
tokio_runtime: tokio::runtime::Runtime,
}
// === impl AsyncCliRunner ===
impl AsyncCliRunner {
/// Given a tokio [`Runtime`](tokio::runtime::Runtime), creates additional context required to
/// execute commands asynchronously.
fn new(tokio_runtime: tokio::runtime::Runtime) -> Self {
let task_manager = TaskManager::new(tokio_runtime.handle().clone());
let task_executor = task_manager.executor();
Self { context: CliContext { task_executor }, task_manager, tokio_runtime }
}
}
/// Additional context provided by the [`CliRunner`] when executing commands
@@ -210,25 +216,37 @@ impl CliRunnerConfig {
}
}
/// Creates a new default tokio multi-thread [Runtime](tokio::runtime::Runtime) with all features
/// enabled
pub fn tokio_runtime() -> Result<tokio::runtime::Runtime, std::io::Error> {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
// Keep the threads alive for at least the block time (12 seconds) plus buffer.
// This prevents the costly process of spawning new threads on every
// new block, and instead reuses the existing threads.
.thread_keep_alive(Duration::from_secs(15))
.thread_name("tokio-rt")
.build()
}
/// Runs the given future to completion or until a critical task panicked.
///
/// Returns the error if a task panicked, or the given future returned an error.
async fn run_to_completion_or_panic<F, E>(
task_manager_handle: JoinHandle<Result<(), PanickedTaskError>>,
fut: F,
) -> Result<(), E>
async fn run_to_completion_or_panic<F, E>(tasks: &mut TaskManager, fut: F) -> Result<(), E>
where
F: Future<Output = Result<(), E>>,
E: Send + Sync + From<reth_tasks::PanickedTaskError> + 'static,
{
let fut = pin!(fut);
tokio::select! {
task_manager_result = task_manager_handle => {
if let Ok(Err(panicked_error)) = task_manager_result {
return Err(panicked_error.into());
}
},
res = fut => res?,
{
let fut = pin!(fut);
tokio::select! {
task_manager_result = tasks => {
if let Err(panicked_error) = task_manager_result {
return Err(panicked_error.into());
}
},
res = fut => res?,
}
}
Ok(())
}
@@ -253,10 +271,10 @@ where
tokio::select! {
_ = ctrl_c => {
info!(target: "reth::cli", "Received ctrl-c");
trace!(target: "reth::cli", "Received ctrl-c");
},
_ = sigterm => {
info!(target: "reth::cli", "Received SIGTERM");
trace!(target: "reth::cli", "Received SIGTERM");
},
res = fut => res?,
}
@@ -269,7 +287,7 @@ where
tokio::select! {
_ = ctrl_c => {
info!(target: "reth::cli", "Received ctrl-c");
trace!(target: "reth::cli", "Received ctrl-c");
},
res = fut => res?,
}
@@ -278,17 +296,17 @@ where
Ok(())
}
/// Default timeout for waiting on the tokio runtime to shut down.
const DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
/// Shut down the given [`Runtime`](reth_tasks::Runtime), and wait for it if `wait` is set.
/// Shut down the given Tokio runtime, and wait for it if `wait` is set.
///
/// Dropping the runtime on the current thread could block due to tokio pool teardown.
/// Instead, we drop it on a separate thread and optionally wait for completion.
fn runtime_shutdown(rt: reth_tasks::Runtime, wait: bool) {
/// `drop(tokio_runtime)` would block the current thread until its pools
/// (including blocking pool) are shutdown. Since we want to exit as soon as possible, drop
/// it on a separate thread and wait for up to 5 seconds for this operation to
/// complete.
fn tokio_shutdown(rt: tokio::runtime::Runtime, wait: bool) {
// Shutdown the runtime on a separate thread
let (tx, rx) = mpsc::channel();
std::thread::Builder::new()
.name("rt-shutdown".to_string())
.name("tokio-shutdown".to_string())
.spawn(move || {
drop(rt);
let _ = tx.send(());
@@ -296,8 +314,8 @@ fn runtime_shutdown(rt: reth_tasks::Runtime, wait: bool) {
.unwrap();
if wait {
let _ = rx.recv_timeout(DEFAULT_RUNTIME_SHUTDOWN_TIMEOUT).inspect_err(|err| {
tracing::warn!(target: "reth::cli", %err, "runtime shutdown timed out");
let _ = rx.recv_timeout(Duration::from_secs(5)).inspect_err(|err| {
debug!(target: "reth::cli", %err, "tokio runtime shutdown timed out");
});
}
}

View File

@@ -11,6 +11,7 @@ use reth_node_builder::{
PayloadTypes,
};
use reth_provider::providers::{BlockchainProvider, NodeTypesForProvider};
use reth_tasks::TaskManager;
use std::sync::Arc;
use wallet::Wallet;
@@ -49,7 +50,7 @@ pub async fn setup<N>(
chain_spec: Arc<N::ChainSpec>,
is_dev: bool,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)>
) -> eyre::Result<(Vec<NodeHelperType<N>>, TaskManager, Wallet)>
where
N: NodeBuilderHelper,
{
@@ -68,6 +69,7 @@ pub async fn setup_engine<N>(
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
TaskManager,
Wallet,
)>
where
@@ -94,6 +96,7 @@ pub async fn setup_engine_with_connection<N>(
connect_nodes: bool,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
TaskManager,
Wallet,
)>
where

View File

@@ -14,7 +14,7 @@ use reth_node_core::args::{DiscoveryArgs, NetworkArgs, RpcServerArgs};
use reth_primitives_traits::AlloyBlockHeader;
use reth_provider::providers::BlockchainProvider;
use reth_rpc_server_types::RpcModuleSelection;
use reth_tasks::Runtime;
use reth_tasks::TaskManager;
use std::sync::Arc;
use tracing::{span, Instrument, Level};
@@ -110,9 +110,11 @@ where
self,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
TaskManager,
Wallet,
)> {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
let tasks = TaskManager::current();
let exec = tasks.executor();
let network_config = NetworkArgs {
discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
@@ -151,7 +153,7 @@ where
let span = span!(Level::INFO, "node", idx);
let node = N::default();
let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config)
.testing_node(runtime.clone())
.testing_node(exec.clone())
.with_types_and_provider::<N, BlockchainProvider<_>>()
.with_components(node.components_builder())
.with_add_ons(node.add_ons())
@@ -195,7 +197,7 @@ where
}
}
Ok((nodes, Wallet::default().with_chain_id(self.chain_spec.chain().into())))
Ok((nodes, tasks, Wallet::default().with_chain_id(self.chain_spec.chain().into())))
}
}

View File

@@ -15,7 +15,7 @@ use reth_provider::{
};
use reth_rpc_server_types::RpcModuleSelection;
use reth_stages_types::StageId;
use reth_tasks::Runtime;
use reth_tasks::TaskManager;
use std::{path::Path, sync::Arc};
use tempfile::TempDir;
use tracing::{debug, info, span, Level};
@@ -24,6 +24,8 @@ use tracing::{debug, info, span, Level};
pub struct ChainImportResult {
/// The nodes that were created
pub nodes: Vec<NodeHelperType<EthereumNode>>,
/// The task manager
pub task_manager: TaskManager,
/// The wallet for testing
pub wallet: Wallet,
/// Temporary directories that must be kept alive for the duration of the test
@@ -66,7 +68,8 @@ pub async fn setup_engine_with_chain_import(
+ Copy
+ 'static,
) -> eyre::Result<ChainImportResult> {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
let tasks = TaskManager::current();
let exec = tasks.executor();
let network_config = NetworkArgs {
discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
@@ -126,7 +129,6 @@ pub async fn setup_engine_with_chain_import(
.with_default_tables()
.build()
.unwrap(),
reth_tasks::Runtime::test(),
)?;
// Initialize genesis if needed
@@ -219,7 +221,7 @@ pub async fn setup_engine_with_chain_import(
let node = EthereumNode::default();
let NodeHandle { node, node_exit_future: _ } = NodeBuilder::new(node_config.clone())
.testing_node_with_datadir(runtime.clone(), datadir.clone())
.testing_node_with_datadir(exec.clone(), datadir.clone())
.with_types_and_provider::<EthereumNode, BlockchainProvider<_>>()
.with_components(node.components_builder())
.with_add_ons(node.add_ons())
@@ -241,6 +243,7 @@ pub async fn setup_engine_with_chain_import(
Ok(ChainImportResult {
nodes,
task_manager: tasks,
wallet: crate::Wallet::default().with_chain_id(chain_spec.chain.id()),
_temp_dirs: temp_dirs,
})
@@ -330,7 +333,6 @@ mod tests {
.with_default_tables()
.build()
.unwrap(),
reth_tasks::Runtime::test(),
)
.expect("failed to create provider factory");
@@ -395,7 +397,6 @@ mod tests {
.with_default_tables()
.build()
.unwrap(),
reth_tasks::Runtime::test(),
)
.expect("failed to create provider factory");
@@ -496,7 +497,6 @@ mod tests {
.with_default_tables()
.build()
.unwrap(),
reth_tasks::Runtime::test(),
)
.expect("failed to create provider factory");

View File

@@ -1,6 +1,6 @@
//! Test setup utilities for configuring the initial state.
use crate::{testsuite::Environment, E2ETestSetupBuilder, NodeBuilderHelper};
use crate::{setup_engine_with_connection, testsuite::Environment, NodeBuilderHelper};
use alloy_eips::BlockNumberOrTag;
use alloy_primitives::B256;
use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes};
@@ -38,8 +38,6 @@ pub struct Setup<I> {
shutdown_tx: Option<mpsc::Sender<()>>,
/// Is this setup in dev mode
pub is_dev: bool,
/// Whether to use v2 storage mode (hashed keys, static file changesets, rocksdb history)
pub storage_v2: bool,
/// Tracks instance generic.
_phantom: PhantomData<I>,
/// Holds the import result to keep nodes alive when using imported chain
@@ -60,7 +58,6 @@ impl<I> Default for Setup<I> {
tree_config: TreeConfig::default(),
shutdown_tx: None,
is_dev: true,
storage_v2: false,
_phantom: Default::default(),
import_result_holder: None,
import_rlp_path: None,
@@ -129,12 +126,6 @@ where
self
}
/// Enable v2 storage mode (hashed keys, static file changesets, rocksdb history)
pub const fn with_storage_v2(mut self) -> Self {
self.storage_v2 = true;
self
}
/// Apply setup using pre-imported chain data from RLP file
pub async fn apply_with_import<N>(
&mut self,
@@ -203,32 +194,23 @@ where
self.shutdown_tx = Some(shutdown_tx);
let is_dev = self.is_dev;
let storage_v2 = self.storage_v2;
let node_count = self.network.node_count;
let tree_config = self.tree_config.clone();
let attributes_generator = Self::create_static_attributes_generator::<N>();
let mut builder = E2ETestSetupBuilder::<N, _>::new(
let result = setup_engine_with_connection::<N>(
node_count,
Arc::<N::ChainSpec>::new((*chain_spec).clone().into()),
is_dev,
self.tree_config.clone(),
attributes_generator,
self.network.connect_nodes,
)
.with_tree_config_modifier(move |base| {
tree_config.clone().with_cross_block_cache_size(base.cross_block_cache_size())
})
.with_node_config_modifier(move |config| config.set_dev(is_dev))
.with_connect_nodes(self.network.connect_nodes);
if storage_v2 {
builder = builder.with_storage_v2();
}
let result = builder.build().await;
.await;
let mut node_clients = Vec::new();
match result {
Ok((nodes, _wallet)) => {
Ok((nodes, executor, _wallet)) => {
// create HTTP clients for each node's RPC and Engine API endpoints
for node in &nodes {
node_clients.push(node.to_node_client()?);
@@ -236,11 +218,12 @@ where
// spawn a separate task just to handle the shutdown
tokio::spawn(async move {
// keep nodes in scope to ensure they're not dropped
// keep nodes and executor in scope to ensure they're not dropped
let _nodes = nodes;
let _executor = executor;
// Wait for shutdown signal
let _ = shutdown_rx.recv().await;
// nodes will be dropped here when the test completes
// nodes and executor will be dropped here when the test completes
});
}
Err(e) => {

View File

@@ -370,14 +370,15 @@ async fn test_setup_builder_with_custom_tree_config() -> Result<()> {
.build(),
);
let (nodes, _wallet) = E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, |_| {
EthPayloadBuilderAttributes::default()
})
.with_tree_config_modifier(|config| {
config.with_persistence_threshold(0).with_memory_block_buffer_target(5)
})
.build()
.await?;
let (nodes, _tasks, _wallet) =
E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, |_| {
EthPayloadBuilderAttributes::default()
})
.with_tree_config_modifier(|config| {
config.with_persistence_threshold(0).with_memory_block_buffer_target(5)
})
.build()
.await?;
assert_eq!(nodes.len(), 1);

View File

@@ -10,6 +10,7 @@ use jsonrpsee::core::client::ClientT;
use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET};
use reth_db::tables;
use reth_e2e_test_utils::{transaction::TransactionTestContext, wallet, E2ETestSetupBuilder};
use reth_node_core::args::RocksDbArgs;
use reth_node_ethereum::EthereumNode;
use reth_payload_builder::EthPayloadBuilderAttributes;
use reth_provider::RocksDBProviderFactory;
@@ -95,6 +96,22 @@ fn test_attributes_generator(timestamp: u64) -> EthPayloadBuilderAttributes {
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
}
/// Verifies that `RocksDB` CLI defaults are `None` (deferred to storage mode).
#[test]
fn test_rocksdb_defaults_are_none() {
let args = RocksDbArgs::default();
assert!(args.tx_hash.is_none(), "tx_hash default should be None (deferred to --storage.v2)");
assert!(
args.storages_history.is_none(),
"storages_history default should be None (deferred to --storage.v2)"
);
assert!(
args.account_history.is_none(),
"account_history default should be None (deferred to --storage.v2)"
);
}
/// Smoke test: node boots with `RocksDB` routing enabled.
#[tokio::test]
async fn test_rocksdb_node_startup() -> Result<()> {
@@ -102,7 +119,7 @@ async fn test_rocksdb_node_startup() -> Result<()> {
let chain_spec = test_chain_spec();
let (nodes, _wallet) =
let (nodes, _tasks, _wallet) =
E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, test_attributes_generator)
.with_storage_v2()
.build()
@@ -130,7 +147,7 @@ async fn test_rocksdb_block_mining() -> Result<()> {
let chain_spec = test_chain_spec();
let chain_id = chain_spec.chain().id();
let (mut nodes, _wallet) =
let (mut nodes, _tasks, _wallet) =
E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, test_attributes_generator)
.with_storage_v2()
.build()
@@ -184,7 +201,7 @@ async fn test_rocksdb_transaction_queries() -> Result<()> {
let chain_spec = test_chain_spec();
let chain_id = chain_spec.chain().id();
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
let (mut nodes, _tasks, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
1,
chain_spec.clone(),
test_attributes_generator,
@@ -251,7 +268,7 @@ async fn test_rocksdb_multi_tx_same_block() -> Result<()> {
let chain_spec = test_chain_spec();
let chain_id = chain_spec.chain().id();
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
let (mut nodes, _tasks, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
1,
chain_spec.clone(),
test_attributes_generator,
@@ -319,7 +336,7 @@ async fn test_rocksdb_txs_across_blocks() -> Result<()> {
let chain_spec = test_chain_spec();
let chain_id = chain_spec.chain().id();
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
let (mut nodes, _tasks, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
1,
chain_spec.clone(),
test_attributes_generator,
@@ -404,7 +421,7 @@ async fn test_rocksdb_pending_tx_not_in_storage() -> Result<()> {
let chain_spec = test_chain_spec();
let chain_id = chain_spec.chain().id();
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
let (mut nodes, _tasks, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
1,
chain_spec.clone(),
test_attributes_generator,
@@ -460,7 +477,7 @@ async fn test_rocksdb_pending_tx_not_in_storage() -> Result<()> {
///
/// This test exercises `unwind_trie_state_from` which previously failed with
/// `UnsortedInput` errors because it read changesets directly from MDBX tables
/// instead of using storage-aware methods that check `is_v2()`.
/// instead of using storage-aware methods that check `storage_changesets_in_static_files`.
#[tokio::test]
async fn test_rocksdb_reorg_unwind() -> Result<()> {
reth_tracing::init_test_tracing();
@@ -468,7 +485,7 @@ async fn test_rocksdb_reorg_unwind() -> Result<()> {
let chain_spec = test_chain_spec();
let chain_id = chain_spec.chain().id();
let (mut nodes, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
let (mut nodes, _tasks, _) = E2ETestSetupBuilder::<EthereumNode, _>::new(
1,
chain_spec.clone(),
test_attributes_generator,

View File

@@ -1,7 +1,6 @@
//! Engine tree configuration.
use alloy_eips::merge::EPOCH_SLOTS;
use core::time::Duration;
/// Triggers persistence when the number of canonical blocks in memory exceeds this threshold.
pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
@@ -65,9 +64,6 @@ pub const DEFAULT_SPARSE_TRIE_PRUNE_DEPTH: usize = 4;
/// Storage tries beyond this limit are cleared (but allocations preserved).
pub const DEFAULT_SPARSE_TRIE_MAX_STORAGE_TRIES: usize = 100;
/// Default timeout for the state root task before spawning a sequential fallback.
pub const DEFAULT_STATE_ROOT_TASK_TIMEOUT: Duration = Duration::from_secs(1);
const DEFAULT_BLOCK_BUFFER_LIMIT: u32 = EPOCH_SLOTS as u32 * 2;
const DEFAULT_MAX_INVALID_HEADER_CACHE_LENGTH: u32 = 256;
const DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE: usize = 4;
@@ -179,13 +175,6 @@ pub struct TreeConfig {
sparse_trie_prune_depth: usize,
/// Maximum number of storage tries to retain after pruning.
sparse_trie_max_storage_tries: usize,
/// Whether to fully disable sparse trie cache pruning between blocks.
disable_sparse_trie_cache_pruning: bool,
/// Timeout for the state root task before spawning a sequential fallback computation.
/// If `Some`, after waiting this duration for the state root task, a sequential state root
/// computation is spawned in parallel and whichever finishes first is used.
/// If `None`, the timeout fallback is disabled.
state_root_task_timeout: Option<Duration>,
}
impl Default for TreeConfig {
@@ -218,8 +207,6 @@ impl Default for TreeConfig {
disable_trie_cache: false,
sparse_trie_prune_depth: DEFAULT_SPARSE_TRIE_PRUNE_DEPTH,
sparse_trie_max_storage_tries: DEFAULT_SPARSE_TRIE_MAX_STORAGE_TRIES,
disable_sparse_trie_cache_pruning: false,
state_root_task_timeout: Some(DEFAULT_STATE_ROOT_TASK_TIMEOUT),
}
}
}
@@ -254,7 +241,6 @@ impl TreeConfig {
disable_cache_metrics: bool,
sparse_trie_prune_depth: usize,
sparse_trie_max_storage_tries: usize,
state_root_task_timeout: Option<Duration>,
) -> Self {
Self {
persistence_threshold,
@@ -284,8 +270,6 @@ impl TreeConfig {
disable_trie_cache: false,
sparse_trie_prune_depth,
sparse_trie_max_storage_tries,
disable_sparse_trie_cache_pruning: false,
state_root_task_timeout,
}
}
@@ -634,26 +618,4 @@ impl TreeConfig {
self.sparse_trie_max_storage_tries = max_tries;
self
}
/// Returns whether sparse trie cache pruning is disabled.
pub const fn disable_sparse_trie_cache_pruning(&self) -> bool {
self.disable_sparse_trie_cache_pruning
}
/// Setter for whether to disable sparse trie cache pruning.
pub const fn with_disable_sparse_trie_cache_pruning(mut self, value: bool) -> Self {
self.disable_sparse_trie_cache_pruning = value;
self
}
/// Returns the state root task timeout.
pub const fn state_root_task_timeout(&self) -> Option<Duration> {
self.state_root_task_timeout
}
/// Setter for state root task timeout.
pub const fn with_state_root_task_timeout(mut self, timeout: Option<Duration>) -> Self {
self.state_root_task_timeout = timeout;
self
}
}

View File

@@ -23,7 +23,7 @@ use serde::{de::DeserializeOwned, Serialize};
// Re-export [`ExecutionPayload`] moved to `reth_payload_primitives`
#[cfg(feature = "std")]
pub use reth_evm::{ConfigureEngineEvm, ConvertTx, ExecutableTxIterator, ExecutableTxTuple};
pub use reth_evm::{ConfigureEngineEvm, ExecutableTxIterator, ExecutableTxTuple};
pub use reth_payload_primitives::ExecutionPayload;
mod error;

View File

@@ -20,7 +20,7 @@ use reth_node_types::{BlockTy, NodeTypes};
use reth_payload_builder::PayloadBuilderHandle;
use reth_provider::{
providers::{BlockchainProvider, ProviderNodeTypes},
ProviderFactory, StorageSettingsCache,
ProviderFactory,
};
use reth_prune::PrunerWithFactory;
use reth_stages_api::{MetricEventsSender, Pipeline};
@@ -94,7 +94,6 @@ where
if chain_spec.is_optimism() { EngineApiKind::OpStack } else { EngineApiKind::Ethereum };
let downloader = BasicBlockDownloader::new(client, consensus.clone());
let use_hashed_state = provider.cached_storage_settings().use_hashed_state();
let persistence_handle =
PersistenceHandle::<N::Primitives>::spawn_service(provider, pruner, sync_metrics_tx);
@@ -112,7 +111,6 @@ where
engine_kind,
evm_config,
changeset_cache,
use_hashed_state,
);
let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree);
@@ -203,7 +201,6 @@ mod tests {
TreeConfig::default(),
Box::new(NoopInvalidBlockHook::default()),
changeset_cache.clone(),
reth_tasks::Runtime::test(),
);
let (sync_metrics_tx, _sync_metrics_rx) = unbounded_channel();

View File

@@ -32,6 +32,7 @@ reth-stages-api.workspace = true
reth-tasks.workspace = true
reth-trie-parallel.workspace = true
reth-trie-sparse = { workspace = true, features = ["std", "metrics"] }
reth-trie-sparse-parallel = { workspace = true, features = ["std"] }
reth-trie.workspace = true
reth-trie-common.workspace = true
reth-trie-db.workspace = true
@@ -141,15 +142,7 @@ test-utils = [
"reth-ethereum-primitives/test-utils",
"reth-node-ethereum/test-utils",
"reth-evm-ethereum/test-utils",
"reth-tasks/test-utils",
]
rocksdb = [
"reth-provider/rocksdb",
"reth-prune/rocksdb",
"reth-stages?/rocksdb",
"reth-e2e-test-utils/rocksdb",
]
edge = ["rocksdb"]
[[test]]
name = "e2e_testsuite"

View File

@@ -12,7 +12,8 @@ use rand::Rng;
use reth_chainspec::ChainSpec;
use reth_db_common::init::init_genesis;
use reth_engine_tree::tree::{
precompile_cache::PrecompileCacheMap, PayloadProcessor, StateProviderBuilder, TreeConfig,
executor::WorkloadExecutor, precompile_cache::PrecompileCacheMap, PayloadProcessor,
StateProviderBuilder, TreeConfig,
};
use reth_ethereum_primitives::TransactionSigned;
use reth_evm::OnStateHook;
@@ -218,7 +219,7 @@ fn bench_state_root(c: &mut Criterion) {
setup_provider(&factory, &state_updates).expect("failed to setup provider");
let payload_processor = PayloadProcessor::new(
reth_tasks::Runtime::test(),
WorkloadExecutor::default(),
EthEvmConfig::new(factory.chain_spec()),
&TreeConfig::default(),
PrecompileCacheMap::default(),

View File

@@ -138,7 +138,7 @@ impl<N: ProviderNodeTypes> PipelineSync<N> {
let (tx, rx) = oneshot::channel();
let pipeline = pipeline.take().expect("exists");
self.pipeline_task_spawner.spawn_critical_blocking_task(
self.pipeline_task_spawner.spawn_critical_blocking(
"pipeline task",
Box::pin(async move {
let result = pipeline.run_as_fut(Some(target)).await;

View File

@@ -76,16 +76,8 @@ impl CacheConfig for EpochCacheConfig {
type FixedCache<K, V, H = DefaultHashBuilder> = fixed_cache::Cache<K, V, H, EpochCacheConfig>;
/// A wrapper of a state provider and a shared cache.
///
/// The const generic `PREWARM` controls whether every cache miss is populated. This is only
/// relevant for pre-warm transaction execution with the intention to pre-populate the cache with
/// data for regular block execution. During regular block execution the cache doesn't need to be
/// populated because the actual EVM database [`State`](revm::database::State) also caches
/// internally during block execution and the cache is then updated after the block with the entire
/// [`BundleState`] output of that block which contains all accessed accounts, code, storage. See
/// also [`ExecutionCache::insert_state`].
#[derive(Debug)]
pub struct CachedStateProvider<S, const PREWARM: bool = false> {
pub struct CachedStateProvider<S> {
/// The state provider
state_provider: S,
@@ -94,9 +86,15 @@ pub struct CachedStateProvider<S, const PREWARM: bool = false> {
/// Metrics for the cached state provider
metrics: CachedStateMetrics,
/// If prewarm enabled we populate every cache miss
prewarm: bool,
}
impl<S> CachedStateProvider<S> {
impl<S> CachedStateProvider<S>
where
S: StateProvider,
{
/// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and
/// [`CachedStateMetrics`].
pub const fn new(
@@ -104,18 +102,27 @@ impl<S> CachedStateProvider<S> {
caches: ExecutionCache,
metrics: CachedStateMetrics,
) -> Self {
Self { state_provider, caches, metrics }
Self { state_provider, caches, metrics, prewarm: false }
}
}
impl<S> CachedStateProvider<S, true> {
/// Creates a new [`CachedStateProvider`] with prewarming enabled.
pub const fn new_prewarm(
state_provider: S,
caches: ExecutionCache,
metrics: CachedStateMetrics,
) -> Self {
Self { state_provider, caches, metrics }
impl<S> CachedStateProvider<S> {
/// Enables pre-warm mode so that every cache miss is populated.
///
/// This is only relevant for pre-warm transaction execution with the intention to pre-populate
/// the cache with data for regular block execution. During regular block execution the
/// cache doesn't need to be populated because the actual EVM database
/// [`State`](revm::database::State) also caches internally during block execution and the cache
/// is then updated after the block with the entire [`BundleState`] output of that block which
/// contains all accessed accounts,code,storage. See also [`ExecutionCache::insert_state`].
pub const fn prewarm(mut self) -> Self {
self.prewarm = true;
self
}
/// Returns whether this provider should pre-warm cache misses.
const fn is_prewarm(&self) -> bool {
self.prewarm
}
}
@@ -300,9 +307,9 @@ impl<K: PartialEq, V> StatsHandler<K, V> for CacheStatsHandler {
}
}
impl<S: AccountReader, const PREWARM: bool> AccountReader for CachedStateProvider<S, PREWARM> {
impl<S: AccountReader> AccountReader for CachedStateProvider<S> {
fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
if PREWARM {
if self.is_prewarm() {
match self.caches.get_or_try_insert_account_with(*address, || {
self.state_provider.basic_account(address)
})? {
@@ -327,13 +334,13 @@ pub enum CachedStatus<T> {
Cached(T),
}
impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvider<S, PREWARM> {
impl<S: StateProvider> StateProvider for CachedStateProvider<S> {
fn storage(
&self,
account: Address,
storage_key: StorageKey,
) -> ProviderResult<Option<StorageValue>> {
if PREWARM {
if self.is_prewarm() {
match self.caches.get_or_try_insert_storage_with(account, storage_key, || {
self.state_provider.storage(account, storage_key).map(Option::unwrap_or_default)
})? {
@@ -351,19 +358,11 @@ impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvide
self.state_provider.storage(account, storage_key)
}
}
fn storage_by_hashed_key(
&self,
address: Address,
hashed_storage_key: StorageKey,
) -> ProviderResult<Option<StorageValue>> {
self.state_provider.storage_by_hashed_key(address, hashed_storage_key)
}
}
impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvider<S, PREWARM> {
impl<S: BytecodeReader> BytecodeReader for CachedStateProvider<S> {
fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
if PREWARM {
if self.is_prewarm() {
match self.caches.get_or_try_insert_code_with(*code_hash, || {
self.state_provider.bytecode_by_hash(code_hash)
})? {
@@ -379,9 +378,7 @@ impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvi
}
}
impl<S: StateRootProvider, const PREWARM: bool> StateRootProvider
for CachedStateProvider<S, PREWARM>
{
impl<S: StateRootProvider> StateRootProvider for CachedStateProvider<S> {
fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult<B256> {
self.state_provider.state_root(hashed_state)
}
@@ -405,9 +402,7 @@ impl<S: StateRootProvider, const PREWARM: bool> StateRootProvider
}
}
impl<S: StateProofProvider, const PREWARM: bool> StateProofProvider
for CachedStateProvider<S, PREWARM>
{
impl<S: StateProofProvider> StateProofProvider for CachedStateProvider<S> {
fn proof(
&self,
input: TrieInput,
@@ -434,9 +429,7 @@ impl<S: StateProofProvider, const PREWARM: bool> StateProofProvider
}
}
impl<S: StorageRootProvider, const PREWARM: bool> StorageRootProvider
for CachedStateProvider<S, PREWARM>
{
impl<S: StorageRootProvider> StorageRootProvider for CachedStateProvider<S> {
fn storage_root(
&self,
address: Address,
@@ -464,7 +457,7 @@ impl<S: StorageRootProvider, const PREWARM: bool> StorageRootProvider
}
}
impl<S: BlockHashReader, const PREWARM: bool> BlockHashReader for CachedStateProvider<S, PREWARM> {
impl<S: BlockHashReader> BlockHashReader for CachedStateProvider<S> {
fn block_hash(&self, number: alloy_primitives::BlockNumber) -> ProviderResult<Option<B256>> {
self.state_provider.block_hash(number)
}
@@ -478,9 +471,7 @@ impl<S: BlockHashReader, const PREWARM: bool> BlockHashReader for CachedStatePro
}
}
impl<S: HashedPostStateProvider, const PREWARM: bool> HashedPostStateProvider
for CachedStateProvider<S, PREWARM>
{
impl<S: HashedPostStateProvider> HashedPostStateProvider for CachedStateProvider<S> {
fn hashed_post_state(&self, bundle_state: &reth_revm::db::BundleState) -> HashedPostState {
self.state_provider.hashed_post_state(bundle_state)
}
@@ -845,10 +836,8 @@ impl SavedCache {
self.caches.update_metrics(&self.metrics);
}
/// Clears all caches, resetting them to empty state,
/// and updates the hash of the block this cache belongs to.
pub(crate) fn clear_with_hash(&mut self, hash: B256) {
self.hash = hash;
/// Clears all caches, resetting them to empty state.
pub(crate) fn clear(&self) {
self.caches.clear();
}
}

View File

@@ -199,17 +199,6 @@ impl<S: StateProvider> StateProvider for InstrumentedStateProvider<S> {
self.record_storage_fetch(start.elapsed());
res
}
fn storage_by_hashed_key(
&self,
address: Address,
hashed_storage_key: StorageKey,
) -> ProviderResult<Option<StorageValue>> {
let start = Instant::now();
let res = self.state_provider.storage_by_hashed_key(address, hashed_storage_key);
self.record_storage_fetch(start.elapsed());
res
}
}
impl<S: BytecodeReader> BytecodeReader for InstrumentedStateProvider<S> {

View File

@@ -8,18 +8,9 @@ use reth_metrics::{
metrics::{Counter, Gauge, Histogram},
Metrics,
};
use reth_primitives_traits::constants::gas_units::MEGAGAS;
use reth_trie::updates::TrieUpdates;
use std::time::{Duration, Instant};
/// Upper bounds for each gas bucket. The last bucket is a catch-all for
/// everything above the final threshold: <5M, 5-10M, 10-20M, 20-30M, 30-40M, >40M.
const GAS_BUCKET_THRESHOLDS: [u64; 5] =
[5 * MEGAGAS, 10 * MEGAGAS, 20 * MEGAGAS, 30 * MEGAGAS, 40 * MEGAGAS];
/// Total number of gas buckets (thresholds + 1 catch-all).
const NUM_GAS_BUCKETS: usize = GAS_BUCKET_THRESHOLDS.len() + 1;
/// Metrics for the `EngineApi`.
#[derive(Debug, Default)]
pub struct EngineApiMetrics {
@@ -99,9 +90,8 @@ impl EngineApiMetrics {
pub struct TreeMetrics {
/// The highest block number in the canonical chain
pub canonical_chain_height: Gauge,
/// Metrics for reorgs.
#[metric(skip)]
pub reorgs: ReorgMetrics,
/// The number of reorgs
pub reorgs: Counter,
/// The latest reorg depth
pub latest_reorg_depth: Gauge,
/// The current safe block height (this is required by optimism)
@@ -110,27 +100,6 @@ pub struct TreeMetrics {
pub finalized_block_height: Gauge,
}
/// Metrics for reorgs.
#[derive(Debug)]
pub struct ReorgMetrics {
/// The number of head block reorgs
pub head: Counter,
/// The number of safe block reorgs
pub safe: Counter,
/// The number of finalized block reorgs
pub finalized: Counter,
}
impl Default for ReorgMetrics {
fn default() -> Self {
Self {
head: metrics::counter!("blockchain_tree_reorgs", "commitment" => "head"),
safe: metrics::counter!("blockchain_tree_reorgs", "commitment" => "safe"),
finalized: metrics::counter!("blockchain_tree_reorgs", "commitment" => "finalized"),
}
}
}
/// Metrics for the `EngineApi`.
#[derive(Metrics)]
#[metrics(scope = "consensus.engine.beacon")]
@@ -244,65 +213,6 @@ impl ForkchoiceUpdatedMetrics {
}
}
/// Per-gas-bucket newPayload metrics, initialized once via [`Self::new_with_labels`].
#[derive(Clone, Metrics)]
#[metrics(scope = "consensus.engine.beacon")]
pub(crate) struct NewPayloadGasBucketMetrics {
/// Latency for new payload calls in this gas bucket.
pub(crate) new_payload_gas_bucket_latency: Histogram,
/// Gas per second for new payload calls in this gas bucket.
pub(crate) new_payload_gas_bucket_gas_per_second: Histogram,
}
/// Holds pre-initialized [`NewPayloadGasBucketMetrics`] instances, one per gas bucket.
#[derive(Debug)]
pub(crate) struct GasBucketMetrics {
buckets: [NewPayloadGasBucketMetrics; NUM_GAS_BUCKETS],
}
impl Default for GasBucketMetrics {
fn default() -> Self {
Self {
buckets: std::array::from_fn(|i| {
let label = Self::bucket_label(i);
NewPayloadGasBucketMetrics::new_with_labels(&[("gas_bucket", label)])
}),
}
}
}
impl GasBucketMetrics {
fn record(&self, gas_used: u64, elapsed: Duration) {
let idx = Self::bucket_index(gas_used);
self.buckets[idx].new_payload_gas_bucket_latency.record(elapsed);
self.buckets[idx]
.new_payload_gas_bucket_gas_per_second
.record(gas_used as f64 / elapsed.as_secs_f64());
}
fn bucket_index(gas_used: u64) -> usize {
GAS_BUCKET_THRESHOLDS
.iter()
.position(|&threshold| gas_used < threshold)
.unwrap_or(GAS_BUCKET_THRESHOLDS.len())
}
/// Returns a human-readable label like `<5M`, `5-10M`, … `>40M`.
fn bucket_label(index: usize) -> String {
if index == 0 {
let hi = GAS_BUCKET_THRESHOLDS[0] / MEGAGAS;
format!("<{hi}M")
} else if index < GAS_BUCKET_THRESHOLDS.len() {
let lo = GAS_BUCKET_THRESHOLDS[index - 1] / MEGAGAS;
let hi = GAS_BUCKET_THRESHOLDS[index] / MEGAGAS;
format!("{lo}-{hi}M")
} else {
let lo = GAS_BUCKET_THRESHOLDS[GAS_BUCKET_THRESHOLDS.len() - 1] / MEGAGAS;
format!(">{lo}M")
}
}
}
/// Metrics for engine newPayload responses.
#[derive(Metrics)]
#[metrics(scope = "consensus.engine.beacon")]
@@ -313,9 +223,6 @@ pub(crate) struct NewPayloadStatusMetrics {
/// Start time of the latest new payload call.
#[metric(skip)]
pub(crate) latest_start_at: Option<Instant>,
/// Gas-bucket-labeled latency and gas/s histograms.
#[metric(skip)]
pub(crate) gas_bucket: GasBucketMetrics,
/// The total count of new payload messages received.
pub(crate) new_payload_messages: Counter,
/// The total count of new payload messages that we responded to with
@@ -392,7 +299,6 @@ impl NewPayloadStatusMetrics {
self.new_payload_messages.increment(1);
self.new_payload_latency.record(elapsed);
self.new_payload_last.set(elapsed);
self.gas_bucket.record(gas_used, elapsed);
if let Some(latest_forkchoice_updated_at) = latest_forkchoice_updated_at.take() {
self.forkchoice_updated_new_payload_time_diff
.record(start - latest_forkchoice_updated_at);
@@ -437,8 +343,6 @@ pub struct BlockValidationMetrics {
pub state_root_parallel_fallback_total: Counter,
/// Total number of times the state root task failed but the fallback succeeded.
pub state_root_task_fallback_success_total: Counter,
/// Total number of times the state root task timed out and a sequential fallback was spawned.
pub state_root_task_timeout_total: Counter,
/// Latest state root duration, ie the time spent blocked waiting for the state root.
pub state_root_duration: Gauge,
/// Histogram for state root duration ie the time spent blocked waiting for the state root

View File

@@ -32,13 +32,12 @@ use reth_provider::{
BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader,
DatabaseProviderFactory, HashedPostStateProvider, ProviderError, StageCheckpointReader,
StateProviderBox, StateProviderFactory, StateReader, StorageChangeSetReader,
StorageSettingsCache, TransactionVariant,
TransactionVariant,
};
use reth_revm::database::StateProviderDatabase;
use reth_stages_api::ControlFlow;
use reth_tasks::spawn_os_thread;
use reth_trie_db::ChangesetCache;
use revm::interpreter::debug_unreachable;
use state::TreeState;
use std::{fmt::Debug, ops, sync::Arc, time::Instant};
@@ -271,9 +270,6 @@ where
evm_config: C,
/// Changeset cache for in-memory trie changesets
changeset_cache: ChangesetCache,
/// Whether the node uses hashed state as canonical storage (v2 mode).
/// Cached at construction to avoid threading `StorageSettingsCache` bounds everywhere.
use_hashed_state: bool,
}
impl<N, P: Debug, T: PayloadTypes + Debug, V: Debug, C> std::fmt::Debug
@@ -299,7 +295,6 @@ where
.field("engine_kind", &self.engine_kind)
.field("evm_config", &self.evm_config)
.field("changeset_cache", &self.changeset_cache)
.field("use_hashed_state", &self.use_hashed_state)
.finish()
}
}
@@ -317,8 +312,7 @@ where
P::Provider: BlockReader<Block = N::Block, Header = N::BlockHeader>
+ StageCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ StorageSettingsCache,
+ StorageChangeSetReader,
C: ConfigureEvm<Primitives = N> + 'static,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
V: EngineValidator<T>,
@@ -339,7 +333,6 @@ where
engine_kind: EngineApiKind,
evm_config: C,
changeset_cache: ChangesetCache,
use_hashed_state: bool,
) -> Self {
let (incoming_tx, incoming) = crossbeam_channel::unbounded();
@@ -361,7 +354,6 @@ where
engine_kind,
evm_config,
changeset_cache,
use_hashed_state,
}
}
@@ -382,7 +374,6 @@ where
kind: EngineApiKind,
evm_config: C,
changeset_cache: ChangesetCache,
use_hashed_state: bool,
) -> (Sender<FromEngine<EngineApiRequest<T, N>, N::Block>>, UnboundedReceiver<EngineApiEvent<N>>)
{
let best_block_number = provider.best_block_number().unwrap_or(0);
@@ -415,7 +406,6 @@ where
kind,
evm_config,
changeset_cache,
use_hashed_state,
);
let incoming = task.incoming_tx.clone();
spawn_os_thread("engine", || task.run());
@@ -1411,7 +1401,7 @@ where
// Spawn a background task to trigger computation so it's ready when the next payload
// arrives.
if let Some(overlay) = self.state.tree_state.prepare_canonical_overlay() {
tokio::task::spawn_blocking(move || {
rayon::spawn(move || {
let _ = overlay.get();
});
}
@@ -1519,7 +1509,7 @@ where
.engine
.failed_forkchoice_updated_response_deliveries
.increment(1);
warn!(target: "engine::tree", ?state, elapsed=?start.elapsed(), "Failed to deliver forkchoiceUpdated response, receiver dropped (request cancelled): {err:?}");
error!(target: "engine::tree", ?state, elapsed=?start.elapsed(), "Failed to send event: {err:?}");
}
}
BeaconEngineMessage::NewPayload { payload, tx } => {
@@ -1543,7 +1533,7 @@ where
BeaconOnNewPayloadError::Internal(Box::new(e))
}))
{
warn!(target: "engine::tree", payload=?num_hash, elapsed=?start.elapsed(), "Failed to deliver newPayload response, receiver dropped (request cancelled): {err:?}");
error!(target: "engine::tree", payload=?num_hash, elapsed=?start.elapsed(), "Failed to send event: {err:?}");
self.metrics
.engine
.failed_new_payload_response_deliveries
@@ -2386,14 +2376,9 @@ where
let old_first = old.first().map(|first| first.recovered_block().num_hash());
trace!(target: "engine::tree", ?new_first, ?old_first, "Reorg detected, new and old first blocks");
self.update_reorg_metrics(old.len(), old_first);
self.update_reorg_metrics(old.len());
self.reinsert_reorged_blocks(new.clone());
// When use_hashed_state is enabled, skip reinserting the old chain — the
// bundle state references plain state reverts which don't exist.
if !self.use_hashed_state {
self.reinsert_reorged_blocks(old.clone());
}
self.reinsert_reorged_blocks(old.clone());
}
// update the tracked in-memory state with the new chain
@@ -2413,23 +2398,9 @@ where
));
}
/// This updates metrics based on the given reorg length and first reorged block number.
fn update_reorg_metrics(&self, old_chain_length: usize, first_reorged_block: Option<NumHash>) {
if let Some(first_reorged_block) = first_reorged_block.map(|block| block.number) {
if let Some(finalized) = self.canonical_in_memory_state.get_finalized_num_hash() &&
first_reorged_block <= finalized.number
{
self.metrics.tree.reorgs.finalized.increment(1);
} else if let Some(safe) = self.canonical_in_memory_state.get_safe_num_hash() &&
first_reorged_block <= safe.number
{
self.metrics.tree.reorgs.safe.increment(1);
} else {
self.metrics.tree.reorgs.head.increment(1);
}
} else {
debug_unreachable!("Reorged chain doesn't have any blocks");
}
/// This updates metrics based on the given reorg length.
fn update_reorg_metrics(&self, old_chain_length: usize) {
self.metrics.tree.reorgs.increment(1);
self.metrics.tree.latest_reorg_depth.set(old_chain_length as f64);
}

View File

@@ -0,0 +1,47 @@
//! Executor for mixed I/O and CPU workloads.
use reth_trie_parallel::root::get_tokio_runtime_handle;
use tokio::{runtime::Handle, task::JoinHandle};
/// An executor for mixed I/O and CPU workloads.
///
/// This type uses tokio to spawn blocking tasks and will reuse an existing tokio
/// runtime if available or create its own.
#[derive(Debug, Clone)]
pub struct WorkloadExecutor {
inner: WorkloadExecutorInner,
}
impl Default for WorkloadExecutor {
fn default() -> Self {
Self { inner: WorkloadExecutorInner::new() }
}
}
impl WorkloadExecutor {
/// Returns the handle to the tokio runtime
pub(super) const fn handle(&self) -> &Handle {
&self.inner.handle
}
/// Runs the provided function on an executor dedicated to blocking operations.
#[track_caller]
pub fn spawn_blocking<F, R>(&self, func: F) -> JoinHandle<R>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
self.inner.handle.spawn_blocking(func)
}
}
#[derive(Debug, Clone)]
struct WorkloadExecutorInner {
handle: Handle,
}
impl WorkloadExecutorInner {
fn new() -> Self {
Self { handle: get_tokio_runtime_handle() }
}
}

View File

@@ -11,10 +11,11 @@ use crate::tree::{
StateProviderBuilder, TreeConfig,
};
use alloy_eip7928::BlockAccessList;
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal};
use alloy_eips::eip1898::BlockWithParent;
use alloy_evm::block::StateChangeSource;
use alloy_primitives::B256;
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
use executor::WorkloadExecutor;
use metrics::{Counter, Histogram};
use multiproof::{SparseTrieUpdate, *};
use parking_lot::RwLock;
@@ -23,8 +24,8 @@ use rayon::prelude::*;
use reth_evm::{
block::ExecutableTxParts,
execute::{ExecutableTxFor, WithTxEnv},
ConfigureEvm, ConvertTx, EvmEnvFor, ExecutableTxIterator, ExecutableTxTuple, OnStateHook,
SpecFor, TxEnvFor,
ConfigureEvm, EvmEnvFor, ExecutableTxIterator, ExecutableTxTuple, OnStateHook, SpecFor,
TxEnvFor,
};
use reth_metrics::Metrics;
use reth_primitives_traits::NodePrimitives;
@@ -33,15 +34,13 @@ use reth_provider::{
StateProviderFactory, StateReader,
};
use reth_revm::{db::BundleState, state::EvmState};
use reth_tasks::Runtime;
use reth_trie::{hashed_cursor::HashedCursorFactory, trie_cursor::TrieCursorFactory};
use reth_trie_parallel::{
proof_task::{ProofTaskCtx, ProofWorkerHandle},
root::ParallelStateRootError,
};
use reth_trie_sparse::{
ParallelSparseTrie, ParallelismThresholds, RevealableSparseTrie, SparseStateTrie,
};
use reth_trie_sparse::{RevealableSparseTrie, SparseStateTrie};
use reth_trie_sparse_parallel::{ParallelSparseTrie, ParallelismThresholds};
use std::{
collections::BTreeMap,
ops::Not,
@@ -50,11 +49,12 @@ use std::{
mpsc::{self, channel},
Arc,
},
time::{Duration, Instant},
time::Instant,
};
use tracing::{debug, debug_span, instrument, warn, Span};
pub mod bal;
pub mod executor;
pub mod multiproof;
mod preserved_sparse_trie;
pub mod prewarm;
@@ -94,9 +94,6 @@ pub const SPARSE_TRIE_MAX_NODES_SHRINK_CAPACITY: usize = 1_000_000;
/// 144MB.
pub const SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY: usize = 1_000_000;
/// Blocks with fewer transactions than this skip prewarming, since the fixed overhead of spawning
/// prewarm workers exceeds the execution time saved.
pub const SMALL_BLOCK_TX_THRESHOLD: usize = 5;
/// Type alias for [`PayloadHandle`] returned by payload processor spawn methods.
type IteratorPayloadHandle<Evm, I, N> = PayloadHandle<
WithTxEnv<TxEnvFor<Evm>, <I as ExecutableTxIterator<Evm>>::Recovered>,
@@ -111,7 +108,7 @@ where
Evm: ConfigureEvm,
{
/// The executor used by to spawn tasks.
executor: Runtime,
executor: WorkloadExecutor,
/// The most recent cache used for execution.
execution_cache: PayloadExecutionCache,
/// Metrics for trie operations
@@ -138,8 +135,6 @@ where
sparse_trie_prune_depth: usize,
/// Maximum storage tries to retain after pruning.
sparse_trie_max_storage_tries: usize,
/// Whether sparse trie cache pruning is fully disabled.
disable_sparse_trie_cache_pruning: bool,
/// Whether to disable cache metrics recording.
disable_cache_metrics: bool,
}
@@ -150,13 +145,13 @@ where
Evm: ConfigureEvm<Primitives = N>,
{
/// Returns a reference to the workload executor driving payload tasks.
pub const fn executor(&self) -> &Runtime {
pub const fn executor(&self) -> &WorkloadExecutor {
&self.executor
}
/// Creates a new payload processor.
pub fn new(
executor: Runtime,
executor: WorkloadExecutor,
evm_config: Evm,
config: &TreeConfig,
precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
@@ -175,7 +170,6 @@ where
prewarm_max_concurrency: config.prewarm_max_concurrency(),
sparse_trie_prune_depth: config.sparse_trie_prune_depth(),
sparse_trie_max_storage_tries: config.sparse_trie_max_storage_tries(),
disable_sparse_trie_cache_pruning: config.disable_sparse_trie_cache_pruning(),
disable_cache_metrics: config.disable_cache_metrics(),
}
}
@@ -241,8 +235,7 @@ where
+ 'static,
{
// start preparing transactions immediately
let (prewarm_rx, execution_rx) =
self.spawn_tx_iterator(transactions, env.transaction_count);
let (prewarm_rx, execution_rx) = self.spawn_tx_iterator(transactions);
let span = Span::current();
let (to_sparse_trie, sparse_trie_rx) = channel();
@@ -259,13 +252,15 @@ where
// When BAL is present, use BAL prewarming and send BAL to multiproof
debug!(target: "engine::tree::payload_processor", "BAL present, using BAL prewarming");
// The prewarm task converts the BAL to HashedPostState and sends it on
// to_multi_proof after slot prefetching completes.
// Send BAL message immediately to MultiProofTask
let _ = to_multi_proof.send(MultiProofMessage::BlockAccessList(Arc::clone(&bal)));
// Spawn with BAL prewarming
self.spawn_caching_with(
env,
prewarm_rx,
provider_builder.clone(),
Some(to_multi_proof.clone()),
None, // Don't send proof targets when BAL is present
Some(bal),
v2_proofs_enabled,
)
@@ -283,7 +278,15 @@ where
// Create and spawn the storage proof task
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
let proof_handle = ProofWorkerHandle::new(&self.executor, task_ctx, v2_proofs_enabled);
let storage_worker_count = config.storage_worker_count();
let account_worker_count = config.account_worker_count();
let proof_handle = ProofWorkerHandle::new(
self.executor.handle().clone(),
task_ctx,
storage_worker_count,
account_worker_count,
v2_proofs_enabled,
);
if config.disable_trie_cache() {
let multi_proof_task = MultiProofTask::new(
@@ -349,8 +352,7 @@ where
where
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
{
let (prewarm_rx, execution_rx) =
self.spawn_tx_iterator(transactions, env.transaction_count);
let (prewarm_rx, execution_rx) = self.spawn_tx_iterator(transactions);
// This path doesn't use multiproof, so V2 proofs flag doesn't matter
let prewarm_handle =
self.spawn_caching_with(env, prewarm_rx, provider_builder, None, bal, false);
@@ -363,23 +365,11 @@ where
}
}
/// Transaction count threshold below which sequential signature recovery is used.
///
/// For blocks with fewer than this many transactions, the rayon parallel iterator overhead
/// (work-stealing setup, channel-based reorder) exceeds the cost of sequential ECDSA
/// recovery. Inspired by Nethermind's `RecoverSignature` which uses sequential `foreach`
/// for small blocks.
const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
/// Spawns a task advancing transaction env iterator and streaming updates through a channel.
///
/// For blocks with fewer than [`Self::SMALL_BLOCK_TX_THRESHOLD`] transactions, uses
/// sequential iteration to avoid rayon overhead.
#[expect(clippy::type_complexity)]
fn spawn_tx_iterator<I: ExecutableTxIterator<Evm>>(
&self,
transactions: I,
transaction_count: usize,
) -> (
mpsc::Receiver<WithTxEnv<TxEnvFor<Evm>, I::Recovered>>,
mpsc::Receiver<Result<WithTxEnv<TxEnvFor<Evm>, I::Recovered>, I::Error>>,
@@ -388,51 +378,22 @@ where
let (prewarm_tx, prewarm_rx) = mpsc::channel();
let (execute_tx, execute_rx) = mpsc::channel();
if transaction_count == 0 {
// Empty block — nothing to do.
} else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
// Sequential path for small blocks — avoids rayon work-stealing setup and
// channel-based reorder overhead when it costs more than the ECDSA recovery itself.
debug!(
target: "engine::tree::payload_processor",
transaction_count,
"using sequential sig recovery for small block"
);
self.executor.spawn_blocking(move || {
let (transactions, convert) = transactions.into_parts();
for (idx, tx) in transactions.into_iter().enumerate() {
let tx = convert.convert(tx);
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
if let Ok(tx) = &tx {
let _ = prewarm_tx.send(tx.clone());
}
let _ = ooo_tx.send((idx, tx));
// Spawn a task that `convert`s all transactions in parallel and sends them out-of-order.
rayon::spawn(move || {
let (transactions, convert) = transactions.into();
transactions.into_par_iter().enumerate().for_each_with(ooo_tx, |ooo_tx, (idx, tx)| {
let tx = convert(tx);
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
// Only send Ok(_) variants to prewarming task.
if let Ok(tx) = &tx {
let _ = prewarm_tx.send(tx.clone());
}
let _ = ooo_tx.send((idx, tx));
});
} else {
// Parallel path — spawn on rayon for parallel signature recovery.
rayon::spawn(move || {
let (transactions, convert) = transactions.into_parts();
transactions.into_par_iter().enumerate().for_each_with(
ooo_tx,
|ooo_tx, (idx, tx)| {
let tx = convert.convert(tx);
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
// Only send Ok(_) variants to prewarming task.
if let Ok(tx) = &tx {
let _ = prewarm_tx.send(tx.clone());
}
let _ = ooo_tx.send((idx, tx));
},
);
});
}
});
// Spawn a task that processes out-of-order transactions from the task above and sends them
// to the execution task in order.
@@ -444,8 +405,8 @@ where
let _ = execute_tx.send(tx);
next_for_execution += 1;
while let Some(entry) = queue.first_entry()
&& *entry.key() == next_for_execution
while let Some(entry) = queue.first_entry() &&
*entry.key() == next_for_execution
{
let _ = execute_tx.send(entry.remove());
next_for_execution += 1;
@@ -463,7 +424,7 @@ where
fn spawn_caching_with<P>(
&self,
env: ExecutionEnv<Evm>,
transactions: mpsc::Receiver<impl ExecutableTxFor<Evm> + Clone + Send + 'static>,
mut transactions: mpsc::Receiver<impl ExecutableTxFor<Evm> + Clone + Send + 'static>,
provider_builder: StateProviderBuilder<N, P>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
bal: Option<Arc<BlockAccessList>>,
@@ -472,8 +433,11 @@ where
where
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
{
let skip_prewarm =
self.disable_transaction_prewarming || env.transaction_count < SMALL_BLOCK_TX_THRESHOLD;
if self.disable_transaction_prewarming {
// if no transactions should be executed we clear them but still spawn the task for
// caching updates
transactions = mpsc::channel().1;
}
let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
@@ -502,9 +466,7 @@ where
{
let to_prewarm_task = to_prewarm_task.clone();
self.executor.spawn_blocking(move || {
let mode = if skip_prewarm {
PrewarmMode::Skipped
} else if let Some(bal) = bal {
let mode = if let Some(bal) = bal {
PrewarmMode::BlockAccessList(bal)
} else {
PrewarmMode::Transactions(transactions)
@@ -553,7 +515,6 @@ where
let disable_trie_cache = config.disable_trie_cache();
let prune_depth = self.sparse_trie_prune_depth;
let max_storage_tries = self.sparse_trie_max_storage_tries;
let disable_cache_pruning = self.disable_sparse_trie_cache_pruning;
let chunk_size =
config.multiproof_chunking_enabled().then_some(config.multiproof_chunk_size());
let executor = self.executor.clone();
@@ -650,7 +611,6 @@ where
max_storage_tries,
SPARSE_TRIE_MAX_NODES_SHRINK_CAPACITY,
SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY,
disable_cache_pruning,
);
trie_metrics
.into_trie_for_reuse_duration_histogram
@@ -764,18 +724,6 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
.map_err(|_| ParallelStateRootError::Other("sparse trie task dropped".to_string()))?
}
/// Takes the state root receiver out of the handle for use with custom waiting logic
/// (e.g., timeout-based waiting).
///
/// # Panics
///
/// If payload processing was started without background tasks.
pub const fn take_state_root_rx(
&mut self,
) -> mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>> {
self.state_root.take().expect("state_root is None")
}
/// Returns a state hook to be used to send state updates to this task.
///
/// If a multiproof task is spawned the hook will notify it about new states.
@@ -926,7 +874,7 @@ impl PayloadExecutionCache {
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
pub(crate) fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
let start = Instant::now();
let mut cache = self.inner.write();
let cache = self.inner.read();
let elapsed = start.elapsed();
self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
@@ -934,7 +882,7 @@ impl PayloadExecutionCache {
warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
}
if let Some(c) = cache.as_mut() {
if let Some(c) = cache.as_ref() {
let cached_hash = c.executed_block_hash();
// Check that the cache hash matches the parent hash of the current block. It won't
// match in case it's a fork block.
@@ -955,13 +903,13 @@ impl PayloadExecutionCache {
);
if available {
// If the has is available (no other threads are using it), but has a mismatching
// parent hash, we can just clear it and keep using without re-creating from
// scratch.
if !hash_matches {
// Fork block: clear and update the hash on the ORIGINAL before cloning.
// This prevents the canonical chain from matching on the stale hash
// and picking up polluted data if the fork block fails.
c.clear_with_hash(parent_hash);
c.clear();
}
return Some(c.clone());
return Some(c.clone())
} else if hash_matches {
self.metrics.execution_cache_in_use.increment(1);
}
@@ -972,25 +920,10 @@ impl PayloadExecutionCache {
None
}
/// Waits until the execution cache becomes available for use.
///
/// This acquires a write lock to ensure exclusive access, then immediately releases it.
/// This is useful for synchronization before starting payload processing.
///
/// Returns the time spent waiting for the lock.
pub fn wait_for_availability(&self) -> Duration {
let start = Instant::now();
// Acquire write lock to wait for any current holders to finish
let _guard = self.inner.write();
let elapsed = start.elapsed();
if elapsed.as_millis() > 5 {
debug!(
target: "engine::tree::payload_processor",
blocked_for=?elapsed,
"Waited for execution cache to become available"
);
}
elapsed
/// Clears the tracked cache
#[expect(unused)]
pub(crate) fn clear(&self) {
self.inner.write().take();
}
/// Updates the cache with a closure that has exclusive access to the guard.
@@ -1043,9 +976,6 @@ pub struct ExecutionEnv<Evm: ConfigureEvm> {
/// Used to determine parallel worker count for prewarming.
/// A value of 0 indicates the count is unknown.
pub transaction_count: usize,
/// Withdrawals included in the block.
/// Used to generate prefetch targets for withdrawal addresses.
pub withdrawals: Option<Vec<Withdrawal>>,
}
impl<Evm: ConfigureEvm> Default for ExecutionEnv<Evm>
@@ -1059,7 +989,6 @@ where
parent_hash: Default::default(),
parent_state_root: Default::default(),
transaction_count: 0,
withdrawals: None,
}
}
}
@@ -1069,7 +998,9 @@ mod tests {
use super::PayloadExecutionCache;
use crate::tree::{
cached_state::{CachedStateMetrics, ExecutionCache, SavedCache},
payload_processor::{evm_state_to_hashed_post_state, PayloadProcessor},
payload_processor::{
evm_state_to_hashed_post_state, executor::WorkloadExecutor, PayloadProcessor,
},
precompile_cache::PrecompileCacheMap,
StateProviderBuilder, TreeConfig,
};
@@ -1143,18 +1074,10 @@ mod tests {
execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(hash)));
// When the parent hash doesn't match (fork block), the cache is cleared,
// hash updated on the original, and clone returned for reuse
// When the parent hash doesn't match, the cache is cleared and returned for reuse
let different_hash = B256::from([4u8; 32]);
let cache = execution_cache.get_cache_for(different_hash);
assert!(cache.is_some(), "cache should be returned for reuse after clearing");
drop(cache);
// The stored cache now has the fork block's parent hash.
// Canonical chain looking for original hash sees a mismatch → clears and reuses.
let original = execution_cache.get_cache_for(hash);
assert!(original.is_some(), "canonical chain gets cache back via mismatch+clear");
assert!(cache.is_some(), "cache should be returned for reuse after clearing")
}
#[test]
@@ -1179,7 +1102,7 @@ mod tests {
#[test]
fn on_inserted_executed_block_populates_cache() {
let payload_processor = PayloadProcessor::new(
reth_tasks::Runtime::test(),
WorkloadExecutor::default(),
EthEvmConfig::new(Arc::new(ChainSpec::default())),
&TreeConfig::default(),
PrecompileCacheMap::default(),
@@ -1208,7 +1131,7 @@ mod tests {
#[test]
fn on_inserted_executed_block_skips_on_parent_mismatch() {
let payload_processor = PayloadProcessor::new(
reth_tasks::Runtime::test(),
WorkloadExecutor::default(),
EthEvmConfig::new(Arc::new(ChainSpec::default())),
&TreeConfig::default(),
PrecompileCacheMap::default(),
@@ -1343,7 +1266,7 @@ mod tests {
}
let mut payload_processor = PayloadProcessor::new(
reth_tasks::Runtime::test(),
WorkloadExecutor::default(),
EthEvmConfig::new(factory.chain_spec()),
&TreeConfig::default(),
PrecompileCacheMap::default(),
@@ -1378,61 +1301,4 @@ mod tests {
"State root mismatch: task={root_from_task}, base={root_from_regular}"
);
}
/// Tests the full prewarm lifecycle for a fork block:
///
/// 1. Cache is at canonical block 4.
/// 2. Fork block (parent = block 2) checks out the cache via `get_cache_for`, simulating what
/// `PrewarmCacheTask` does when it receives a `SavedCache`.
/// 3. Prewarm populates the shared cache with fork-specific state.
/// 4. While the prewarm clone is alive, the cache is unavailable (`usage_guard` > 1).
/// 5. Prewarm drops without calling `save_cache` (fork block was invalid).
/// 6. Canonical block 5 (parent = block 4) must get a cache with correct hash and no stale fork
/// data.
#[test]
fn fork_prewarm_dropped_without_save_does_not_corrupt_cache() {
let execution_cache = PayloadExecutionCache::default();
// Canonical chain at block 4.
let block4_hash = B256::from([4u8; 32]);
execution_cache.update_with_guard(|slot| *slot = Some(make_saved_cache(block4_hash)));
// Fork block arrives with parent = block 2. Prewarm task checks out the cache.
// This simulates PrewarmCacheTask receiving a SavedCache clone from get_cache_for.
let fork_parent = B256::from([2u8; 32]);
let prewarm_cache = execution_cache.get_cache_for(fork_parent);
assert!(prewarm_cache.is_some(), "prewarm should obtain cache for fork block");
let prewarm_cache = prewarm_cache.unwrap();
assert_eq!(prewarm_cache.executed_block_hash(), fork_parent);
// Prewarm populates cache with fork-specific state (ancestor data for block 2).
// Since ExecutionCache uses Arc<Inner>, this data is shared with the stored original.
let fork_addr = Address::from([0xBB; 20]);
let fork_key = B256::from([0xCC; 32]);
prewarm_cache.cache().insert_storage(fork_addr, fork_key, Some(U256::from(999)));
// While prewarm holds the clone, the usage_guard count > 1 → cache is in use.
let during_prewarm = execution_cache.get_cache_for(block4_hash);
assert!(
during_prewarm.is_none(),
"cache must be unavailable while prewarm holds a reference"
);
// Fork block fails — prewarm task drops without calling save_cache/update_with_guard.
drop(prewarm_cache);
// Canonical block 5 arrives (parent = block 4).
// Stored hash = fork_parent (our fix), so get_cache_for sees a mismatch,
// clears the stale fork data, and returns a cache with hash = block4_hash.
let block5_cache = execution_cache.get_cache_for(block4_hash);
assert!(
block5_cache.is_some(),
"canonical chain must get cache after fork prewarm is dropped"
);
assert_eq!(
block5_cache.as_ref().unwrap().executed_block_hash(),
block4_hash,
"cache must carry the canonical parent hash, not the fork parent"
);
}
}

View File

@@ -115,8 +115,6 @@ pub enum MultiProofMessage {
/// The state update that was used to calculate the proof
state: HashedPostState,
},
/// Pre-hashed state update from BAL conversion that can be applied directly without proofs.
HashedStateUpdate(HashedPostState),
/// Block Access List (EIP-7928; BAL) containing complete state changes for the block.
///
/// When received, the task generates a single state update from the BAL and processes it.
@@ -1191,11 +1189,6 @@ impl MultiProofTask {
}
false
}
MultiProofMessage::HashedStateUpdate(hashed_state) => {
batch_metrics.state_update_proofs_requested +=
self.on_hashed_state_update(Source::BlockAccessList, hashed_state);
false
}
}
}
@@ -1541,18 +1534,23 @@ mod tests {
providers::OverlayStateProviderFactory, test_utils::create_test_provider_factory,
BlockNumReader, BlockReader, ChangeSetReader, DatabaseProviderFactory, LatestStateProvider,
PruneCheckpointReader, StageCheckpointReader, StateProviderBox, StorageChangeSetReader,
StorageSettingsCache,
};
use reth_trie::MultiProof;
use reth_trie_db::ChangesetCache;
use reth_trie_parallel::proof_task::{ProofTaskCtx, ProofWorkerHandle};
use revm_primitives::{B256, U256};
use std::sync::{Arc, OnceLock};
use tokio::runtime::{Handle, Runtime};
/// Get a test runtime, creating it if necessary
fn get_test_runtime() -> &'static reth_tasks::Runtime {
static TEST_RT: OnceLock<reth_tasks::Runtime> = OnceLock::new();
TEST_RT.get_or_init(reth_tasks::Runtime::test)
/// Get a handle to the test runtime, creating it if necessary
fn get_test_runtime_handle() -> Handle {
static TEST_RT: OnceLock<Runtime> = OnceLock::new();
TEST_RT
.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()
})
.handle()
.clone()
}
fn create_test_state_root_task<F>(factory: F) -> MultiProofTask
@@ -1563,17 +1561,16 @@ mod tests {
+ PruneCheckpointReader
+ ChangeSetReader
+ StorageChangeSetReader
+ StorageSettingsCache
+ BlockNumReader,
> + Clone
+ Send
+ 'static,
{
let runtime = get_test_runtime();
let rt_handle = get_test_runtime_handle();
let changeset_cache = ChangesetCache::new();
let overlay_factory = OverlayStateProviderFactory::new(factory, changeset_cache);
let task_ctx = ProofTaskCtx::new(overlay_factory);
let proof_handle = ProofWorkerHandle::new(runtime, task_ctx, false);
let proof_handle = ProofWorkerHandle::new(rt_handle, task_ctx, 1, 1, false);
let (to_sparse_trie, _receiver) = std::sync::mpsc::channel();
let (tx, rx) = crossbeam_channel::unbounded();
@@ -1583,10 +1580,7 @@ mod tests {
fn create_cached_provider<F>(factory: F) -> CachedStateProvider<StateProviderBox>
where
F: DatabaseProviderFactory<
Provider: BlockReader
+ StageCheckpointReader
+ PruneCheckpointReader
+ reth_provider::StorageSettingsCache,
Provider: BlockReader + StageCheckpointReader + PruneCheckpointReader,
> + Clone
+ Send
+ 'static,

View File

@@ -3,11 +3,12 @@
use alloy_primitives::B256;
use parking_lot::Mutex;
use reth_trie_sparse::SparseStateTrie;
use reth_trie_sparse_parallel::ParallelSparseTrie;
use std::sync::Arc;
use tracing::debug;
/// Type alias for the sparse trie type used in preservation.
pub(super) type SparseTrie = SparseStateTrie;
pub(super) type SparseTrie = SparseStateTrie<ParallelSparseTrie, ParallelSparseTrie>;
/// Shared handle to a preserved sparse trie that can be reused across payload validations.
///

View File

@@ -14,7 +14,8 @@
use crate::tree::{
cached_state::{CachedStateProvider, SavedCache},
payload_processor::{
bal::{self, total_slots, BALSlotIter},
bal::{total_slots, BALSlotIter},
executor::WorkloadExecutor,
multiproof::{MultiProofMessage, VersionedMultiProofTargets},
PayloadExecutionCache,
},
@@ -23,7 +24,6 @@ use crate::tree::{
};
use alloy_consensus::transaction::TxHashRef;
use alloy_eip7928::BlockAccessList;
use alloy_eips::eip4895::Withdrawal;
use alloy_evm::Database;
use alloy_primitives::{keccak256, map::B256Set, B256};
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
@@ -36,7 +36,6 @@ use reth_provider::{
StateReader,
};
use reth_revm::{database::StateProviderDatabase, state::EvmState};
use reth_tasks::Runtime;
use reth_trie::MultiProofTargets;
use std::{
ops::Range,
@@ -49,16 +48,13 @@ use std::{
};
use tracing::{debug, debug_span, instrument, trace, warn, Span};
/// Determines the prewarming mode: transaction-based, BAL-based, or skipped.
/// Determines the prewarming mode: transaction-based or BAL-based.
#[derive(Debug)]
pub enum PrewarmMode<Tx> {
/// Prewarm by executing transactions from a stream.
Transactions(Receiver<Tx>),
/// Prewarm by prefetching slots from a Block Access List.
BlockAccessList(Arc<BlockAccessList>),
/// Transaction prewarming is skipped (e.g. small blocks where the overhead exceeds the
/// benefit). No workers are spawned.
Skipped,
}
/// A wrapper for transactions that includes their index in the block.
@@ -81,7 +77,7 @@ where
Evm: ConfigureEvm<Primitives = N>,
{
/// The executor used to spawn execution tasks.
executor: Runtime,
executor: WorkloadExecutor,
/// Shared execution cache.
execution_cache: PayloadExecutionCache,
/// Context provided to execution tasks
@@ -104,7 +100,7 @@ where
{
/// Initializes the task with the given transactions pending execution
pub fn new(
executor: Runtime,
executor: WorkloadExecutor,
execution_cache: PayloadExecutionCache,
ctx: PrewarmContext<N, P, Evm>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
@@ -167,7 +163,7 @@ where
};
// Spawn workers
let tx_sender = ctx.clone().spawn_workers(workers_needed, &executor, to_multi_proof.clone(), done_tx.clone());
let tx_sender = ctx.clone().spawn_workers(workers_needed, &executor, to_multi_proof, done_tx.clone());
// Distribute transactions to workers
let mut tx_index = 0usize;
@@ -191,16 +187,6 @@ where
tx_index += 1;
}
// Send withdrawal prefetch targets after all transactions have been distributed
if let Some(to_multi_proof) = to_multi_proof
&& let Some(withdrawals) = &ctx.env.withdrawals
&& !withdrawals.is_empty()
{
let targets =
multiproof_targets_from_withdrawals(withdrawals, ctx.v2_proofs_enabled);
let _ = to_multi_proof.send(MultiProofMessage::PrefetchProofs(targets));
}
// drop sender and wait for all tasks to finish
drop(done_tx);
drop(tx_sender);
@@ -290,7 +276,6 @@ where
target: "engine::tree::payload_processor::prewarm",
"Skipping BAL prewarm - no cache available"
);
self.send_bal_hashed_state(&bal);
let _ =
actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
return;
@@ -306,7 +291,7 @@ where
);
if total_slots == 0 {
self.send_bal_hashed_state(&bal);
// No slots to prefetch, signal completion immediately
let _ =
actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
return;
@@ -351,51 +336,10 @@ where
"All BAL prewarm workers completed"
);
// Convert BAL to HashedPostState and send to multiproof task
self.send_bal_hashed_state(&bal);
// Signal that execution has finished
let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
}
/// Converts the BAL to [`HashedPostState`](reth_trie::HashedPostState) and sends it to the
/// multiproof task.
fn send_bal_hashed_state(&self, bal: &BlockAccessList) {
let Some(to_multi_proof) = &self.to_multi_proof else { return };
let provider = match self.ctx.provider.build() {
Ok(provider) => provider,
Err(err) => {
warn!(
target: "engine::tree::payload_processor::prewarm",
?err,
"Failed to build provider for BAL hashed state conversion"
);
return;
}
};
match bal::bal_to_hashed_post_state(bal, &provider) {
Ok(hashed_state) => {
debug!(
target: "engine::tree::payload_processor::prewarm",
accounts = hashed_state.accounts.len(),
storages = hashed_state.storages.len(),
"Converted BAL to hashed post state"
);
let _ = to_multi_proof.send(MultiProofMessage::HashedStateUpdate(hashed_state));
let _ = to_multi_proof.send(MultiProofMessage::FinishedStateUpdates);
}
Err(err) => {
warn!(
target: "engine::tree::payload_processor::prewarm",
?err,
"Failed to convert BAL to hashed state"
);
}
}
}
/// Executes the task.
///
/// This will execute the transactions until all transactions have been processed or the task
@@ -419,10 +363,6 @@ where
PrewarmMode::BlockAccessList(bal) => {
self.run_bal_prewarm(bal, actions_tx);
}
PrewarmMode::Skipped => {
let _ = actions_tx
.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
}
}
let mut final_execution_outcome = None;
@@ -535,8 +475,11 @@ where
if let Some(saved_cache) = saved_cache {
let caches = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
state_provider =
Box::new(CachedStateProvider::new_prewarm(state_provider, caches, cache_metrics));
state_provider = Box::new(
CachedStateProvider::new(state_provider, caches, cache_metrics)
// ensure we pre-warm the cache
.prewarm(),
);
}
let state_provider = StateProviderDatabase::new(state_provider);
@@ -594,11 +537,18 @@ where
return
};
while let Ok(IndexedTransaction { index, tx }) = txs.recv() {
let _enter = debug_span!(
while let Ok(IndexedTransaction { index, tx }) = {
let _enter = debug_span!(target: "engine::tree::payload_processor::prewarm", "recv tx")
.entered();
txs.recv()
} {
let enter = debug_span!(
target: "engine::tree::payload_processor::prewarm",
"prewarm tx",
index,
tx_hash = %tx.tx().tx_hash(),
is_success = tracing::field::Empty,
gas_used = tracing::field::Empty,
)
.entered();
@@ -629,6 +579,12 @@ where
};
metrics.execution_duration.record(start.elapsed());
// record some basic information about the transactions
enter.record("gas_used", res.result.gas_used());
enter.record("is_success", res.result.is_success());
drop(enter);
// If the task was cancelled, stop execution, and exit.
if terminate_execution.load(Ordering::Relaxed) {
break
@@ -637,12 +593,16 @@ where
// Only send outcome for transactions after the first txn
// as the main execution will be just as fast
if index > 0 {
let _enter =
debug_span!(target: "engine::tree::payload_processor::prewarm", "prewarm outcome", index, tx_hash=%tx.tx().tx_hash())
.entered();
let (targets, storage_targets) =
multiproof_targets_from_state(res.state, v2_proofs_enabled);
metrics.prefetch_storage_targets.record(storage_targets as f64);
if let Some(to_multi_proof) = &to_multi_proof {
let _ = to_multi_proof.send(MultiProofMessage::PrefetchProofs(targets));
}
drop(_enter);
}
metrics.total_runtime.record(start.elapsed());
@@ -658,7 +618,7 @@ where
fn spawn_workers<Tx>(
self,
workers_needed: usize,
task_executor: &Runtime,
task_executor: &WorkloadExecutor,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
done_tx: Sender<()>,
) -> CrossbeamSender<IndexedTransaction<Tx>>
@@ -695,7 +655,7 @@ where
fn spawn_bal_worker(
&self,
idx: usize,
executor: &Runtime,
executor: &WorkloadExecutor,
bal: Arc<BlockAccessList>,
range: Range<usize>,
done_tx: Sender<()>,
@@ -871,27 +831,6 @@ fn multiproof_targets_v2_from_state(state: EvmState) -> (VersionedMultiProofTarg
(VersionedMultiProofTargets::V2(targets), storage_target_count)
}
/// Returns [`VersionedMultiProofTargets`] for withdrawal addresses.
///
/// Withdrawals only modify account balances (no storage), so the targets contain
/// only account-level entries with empty storage sets.
fn multiproof_targets_from_withdrawals(
withdrawals: &[Withdrawal],
v2_enabled: bool,
) -> VersionedMultiProofTargets {
use reth_trie_parallel::targets_v2::MultiProofTargetsV2;
if v2_enabled {
VersionedMultiProofTargets::V2(MultiProofTargetsV2 {
account_targets: withdrawals.iter().map(|w| keccak256(w.address).into()).collect(),
..Default::default()
})
} else {
VersionedMultiProofTargets::Legacy(
withdrawals.iter().map(|w| (keccak256(w.address), Default::default())).collect(),
)
}
}
/// The events the pre-warm task can handle.
///
/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the main

View File

@@ -1,5 +1,6 @@
//! Sparse Trie task related functionality.
use super::executor::WorkloadExecutor;
use crate::tree::{
multiproof::{
dispatch_with_chunking, evm_state_to_hashed_post_state, MultiProofMessage,
@@ -10,9 +11,8 @@ use crate::tree::{
use alloy_primitives::B256;
use alloy_rlp::{Decodable, Encodable};
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
use rayon::iter::ParallelIterator;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use reth_primitives_traits::{Account, ParallelBridgeBuffered};
use reth_tasks::Runtime;
use reth_trie::{
proof_v2::Target, updates::TrieUpdates, DecodedMultiProofV2, HashedPostState, Nibbles,
TrieAccount, EMPTY_ROOT_HASH, TRIE_ACCOUNT_RLP_MAX_SIZE,
@@ -28,7 +28,7 @@ use reth_trie_parallel::{
use reth_trie_sparse::{
errors::{SparseStateTrieResult, SparseTrieErrorKind, SparseTrieResult},
provider::{TrieNodeProvider, TrieNodeProviderFactory},
DeferredDrops, LeafUpdate, ParallelSparseTrie, SparseStateTrie, SparseTrie,
DeferredDrops, LeafUpdate, SerialSparseTrie, SparseStateTrie, SparseTrie, SparseTrieExt,
};
use revm_primitives::{hash_map::Entry, B256Map};
use smallvec::SmallVec;
@@ -44,8 +44,8 @@ where
BPF: TrieNodeProviderFactory + Send + Sync,
BPF::AccountNodeProvider: TrieNodeProvider + Send + Sync,
BPF::StorageNodeProvider: TrieNodeProvider + Send + Sync,
A: SparseTrie + Send + Sync + Default,
S: SparseTrie + Send + Sync + Default + Clone,
A: SparseTrie + SparseTrieExt + Send + Sync + Default,
S: SparseTrie + SparseTrieExt + Send + Sync + Default + Clone,
{
Cleared(SparseTrieTask<BPF, A, S>),
Cached(SparseTrieCacheTask<A, S>),
@@ -56,8 +56,8 @@ where
BPF: TrieNodeProviderFactory + Send + Sync + Clone,
BPF::AccountNodeProvider: TrieNodeProvider + Send + Sync,
BPF::StorageNodeProvider: TrieNodeProvider + Send + Sync,
A: SparseTrie + Send + Sync + Default,
S: SparseTrie + Send + Sync + Default + Clone,
A: SparseTrie + SparseTrieExt + Send + Sync + Default,
S: SparseTrie + SparseTrieExt + Send + Sync + Default + Clone,
{
pub(super) fn run(&mut self) -> Result<StateRootComputeOutcome, ParallelStateRootError> {
match self {
@@ -72,7 +72,6 @@ where
max_storage_tries: usize,
max_nodes_capacity: usize,
max_values_capacity: usize,
disable_pruning: bool,
) -> (SparseStateTrie<A, S>, DeferredDrops) {
match self {
Self::Cleared(task) => task.into_cleared_trie(max_nodes_capacity, max_values_capacity),
@@ -81,7 +80,6 @@ where
max_storage_tries,
max_nodes_capacity,
max_values_capacity,
disable_pruning,
),
}
}
@@ -99,7 +97,7 @@ where
}
/// A task responsible for populating the sparse trie.
pub(super) struct SparseTrieTask<BPF, A = ParallelSparseTrie, S = ParallelSparseTrie>
pub(super) struct SparseTrieTask<BPF, A = SerialSparseTrie, S = SerialSparseTrie>
where
BPF: TrieNodeProviderFactory + Send + Sync,
BPF::AccountNodeProvider: TrieNodeProvider + Send + Sync,
@@ -119,8 +117,8 @@ where
BPF: TrieNodeProviderFactory + Send + Sync + Clone,
BPF::AccountNodeProvider: TrieNodeProvider + Send + Sync,
BPF::StorageNodeProvider: TrieNodeProvider + Send + Sync,
A: SparseTrie + Send + Sync + Default,
S: SparseTrie + Send + Sync + Default + Clone,
A: SparseTrie + SparseTrieExt + Send + Sync + Default,
S: SparseTrie + SparseTrieExt + Send + Sync + Default + Clone,
{
/// Creates a new sparse trie task with the given trie.
pub(super) const fn new(
@@ -214,7 +212,7 @@ where
const MAX_PENDING_UPDATES: usize = 100;
/// Sparse trie task implementation that uses in-memory sparse trie data to schedule proof fetching.
pub(super) struct SparseTrieCacheTask<A = ParallelSparseTrie, S = ParallelSparseTrie> {
pub(super) struct SparseTrieCacheTask<A = SerialSparseTrie, S = SerialSparseTrie> {
/// Sender for proof results.
proof_result_tx: CrossbeamSender<ProofResultMessage>,
/// Receiver for proof results directly from workers.
@@ -279,12 +277,12 @@ pub(super) struct SparseTrieCacheTask<A = ParallelSparseTrie, S = ParallelSparse
impl<A, S> SparseTrieCacheTask<A, S>
where
A: SparseTrie + Default,
S: SparseTrie + Default + Clone,
A: SparseTrieExt + Default,
S: SparseTrieExt + Default + Clone,
{
/// Creates a new sparse trie, pre-populating with an existing [`SparseStateTrie`].
pub(super) fn new_with_trie(
executor: &Runtime,
executor: &WorkloadExecutor,
updates: CrossbeamReceiver<MultiProofMessage>,
proof_worker_handle: ProofWorkerHandle,
metrics: MultiProofTaskMetrics,
@@ -345,9 +343,6 @@ where
MultiProofMessage::EmptyProof { .. } | MultiProofMessage::BlockAccessList(_) => {
continue
}
MultiProofMessage::HashedStateUpdate(state) => {
SparseTrieTaskMessage::HashedState(state)
}
};
if hashed_state_tx.send(msg).is_err() {
break;
@@ -358,23 +353,16 @@ where
/// Prunes and shrinks the trie for reuse in the next payload built on top of this one.
///
/// Should be called after the state root result has been sent.
///
/// When `disable_pruning` is true, the trie is preserved without any node pruning,
/// storage trie eviction, or capacity shrinking, keeping the full cache intact for
/// benchmarking purposes.
pub(super) fn into_trie_for_reuse(
self,
prune_depth: usize,
max_storage_tries: usize,
max_nodes_capacity: usize,
max_values_capacity: usize,
disable_pruning: bool,
) -> (SparseStateTrie<A, S>, DeferredDrops) {
let Self { mut trie, .. } = self;
if !disable_pruning {
trie.prune(prune_depth, max_storage_tries);
trie.shrink_to(max_nodes_capacity, max_values_capacity);
}
trie.prune(prune_depth, max_storage_tries);
trie.shrink_to(max_nodes_capacity, max_values_capacity);
let deferred = trie.take_deferred_drops();
(trie, deferred)
}
@@ -416,9 +404,7 @@ where
let update = match message {
Ok(m) => m,
Err(_) => {
return Err(ParallelStateRootError::Other(
"updates channel disconnected before state root calculation".to_string(),
))
break
}
};
@@ -497,7 +483,7 @@ where
}
#[instrument(
level = "trace",
level = "debug",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
@@ -529,7 +515,7 @@ where
/// Processes a hashed state update and encodes all state changes as trie updates.
#[instrument(
level = "trace",
level = "debug",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
@@ -689,11 +675,6 @@ where
/// Invokes `update_leaves` for the accounts trie and collects any new targets.
///
/// Returns whether any updates were drained (applied to the trie).
#[instrument(
level = "debug",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
fn process_account_leaf_updates(&mut self, new: bool) -> SparseTrieResult<bool> {
let account_updates =
if new { &mut self.new_account_updates } else { &mut self.account_updates };
@@ -737,50 +718,53 @@ where
return Ok(());
}
let span = debug_span!("compute_storage_roots").entered();
self
let span = tracing::Span::current();
let roots = self
.trie
.storage_tries_mut()
.iter_mut()
.filter(|(address, trie)| {
self.storage_updates.get(*address).is_some_and(|updates| updates.is_empty()) &&
!trie.is_root_cached()
.par_iter_mut()
.filter(|(address, _)| {
self.storage_updates.get(*address).is_some_and(|updates| updates.is_empty())
})
.par_bridge_buffered()
.for_each(|(address, trie)| {
.map(|(address, trie)| {
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage root", ?address).entered();
trie.root().expect("updates are drained, trie should be revealed by now");
});
drop(span);
let root =
trie.root().expect("updates are drained, trie should be revealed by now");
(address, root)
})
.collect::<Vec<_>>();
for (addr, storage_root) in roots {
// If the storage root is known and we have a pending update for this account, encode it
// into a proper update.
if let Entry::Occupied(entry) = self.pending_account_updates.entry(*addr) &&
entry.get().is_some()
{
let account = entry.remove().expect("just checked, should be Some");
let encoded = if account.is_none_or(|account| account.is_empty()) &&
storage_root == EMPTY_ROOT_HASH
{
Vec::new()
} else {
self.account_rlp_buf.clear();
account
.unwrap_or_default()
.into_trie_account(storage_root)
.encode(&mut self.account_rlp_buf);
self.account_rlp_buf.clone()
};
self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
}
}
loop {
let span = debug_span!("promote_updates", promoted = tracing::field::Empty).entered();
// Now handle pending account updates that can be upgraded to a proper update.
let account_rlp_buf = &mut self.account_rlp_buf;
let mut num_promoted = 0;
self.pending_account_updates.retain(|addr, account| {
if let Some(updates) = self.storage_updates.get(addr) {
if !updates.is_empty() {
// If account has pending storage updates, it is still pending.
return true;
} else if let Some(account) = account.take() {
let storage_root = self.trie.storage_root(addr).expect("updates are drained, storage trie should be revealed by now");
let encoded = if account.is_none_or(|account| account.is_empty()) &&
storage_root == EMPTY_ROOT_HASH
{
Vec::new()
} else {
account_rlp_buf.clear();
account
.unwrap_or_default()
.into_trie_account(storage_root)
.encode(account_rlp_buf);
account_rlp_buf.clone()
};
self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
num_promoted += 1;
return false;
}
// If account has pending storage updates, it is still pending.
if self.storage_updates.get(addr).is_some_and(|updates| !updates.is_empty()) {
return true;
}
// Get the current account state either from the trie or from latest account update.
@@ -815,18 +799,15 @@ where
account_rlp_buf.clone()
};
self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
num_promoted += 1;
false
});
span.record("promoted", num_promoted);
drop(span);
// Only exit when no new updates are processed.
//
// We need to keep iterating if any updates are being drained because that might
// indicate that more pending account updates can be promoted.
if num_promoted == 0 || !self.process_account_leaf_updates(false)? {
if !self.process_account_leaf_updates(false)? {
break
}
}
@@ -1043,59 +1024,3 @@ where
Ok(elapsed)
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{keccak256, Address, U256};
use reth_trie_sparse::ParallelSparseTrie;
#[test]
fn test_run_hashing_task_hashed_state_update_forwards() {
let (updates_tx, updates_rx) = crossbeam_channel::unbounded();
let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
let address = keccak256(Address::random());
let slot = keccak256(U256::from(42).to_be_bytes::<32>());
let value = U256::from(999);
let mut hashed_state = HashedPostState::default();
hashed_state.accounts.insert(
address,
Some(Account { balance: U256::from(100), nonce: 1, bytecode_hash: None }),
);
let mut storage = reth_trie::HashedStorage::new(false);
storage.storage.insert(slot, value);
hashed_state.storages.insert(address, storage);
let expected_state = hashed_state.clone();
let handle = std::thread::spawn(move || {
SparseTrieCacheTask::<ParallelSparseTrie, ParallelSparseTrie>::run_hashing_task(
updates_rx,
hashed_state_tx,
);
});
updates_tx.send(MultiProofMessage::HashedStateUpdate(hashed_state)).unwrap();
updates_tx.send(MultiProofMessage::FinishedStateUpdates).unwrap();
drop(updates_tx);
let SparseTrieTaskMessage::HashedState(received) = hashed_state_rx.recv().unwrap() else {
panic!("expected HashedState message");
};
let account = received.accounts.get(&address).unwrap().unwrap();
assert_eq!(account.balance, expected_state.accounts[&address].unwrap().balance);
assert_eq!(account.nonce, expected_state.accounts[&address].unwrap().nonce);
let storage = received.storages.get(&address).unwrap();
assert_eq!(*storage.storage.get(&slot).unwrap(), value);
let second = hashed_state_rx.recv().unwrap();
assert!(matches!(second, SparseTrieTaskMessage::FinishedStateUpdates));
assert!(hashed_state_rx.recv().is_err());
handle.join().unwrap();
}
}

Some files were not shown because too many files have changed in this diff Show More