Compare commits

..

280 Commits

Author SHA1 Message Date
yongkangc
5121ad2244 fix: revert slow path optimization that caused regression
The slow path optimization attempted to reuse ancestor cached overlays,
but those overlays were built with a DIFFERENT anchor_hash. Since the
slow path is triggered precisely because the anchor changed (after
persist/reorg), reusing overlays from the old anchor produces incorrect
results.

Reverted to the original slow path that rebuilds from each ancestor's
per-block state changes. The Arc::make_mut tracking metrics are kept.
2026-01-06 23:29:13 +00:00
yongkangc
f073e6ec49 fix: address clippy doc-markdown lints 2026-01-06 23:29:13 +00:00
yongkangc
dcde41a6b4 perf: optimize slow path to reuse ancestor cached overlays
Instead of iterating all ancestors from scratch in merge_ancestors_into_overlay,
we now find the most recent ancestor that has a cached anchored_trie_input and
use that as the base overlay. This reduces O(N) work to O(N - cached_depth).

This optimization is particularly helpful at persist/reorg boundaries where
the slow path is triggered. If any ancestor in the chain already has a cached
overlay, we skip rebuilding everything before it.

Also adds Arc::make_mut clone tracking metrics to help identify when CoW
clones are triggered (when strong_count > 1).
2026-01-06 23:29:13 +00:00
yongkangc
517d5ad6be perf: avoid O(N) prefix_sets clone on overlay reuse
The fast path overlay reuse was cloning the entire TrieInputSorted,
including prefix_sets (a B256Map<PrefixSetMut>) which caused O(N)
HashMap clones per block.

prefix_sets does not need to accumulate across blocks - it only needs
the current block's changes. Ancestors' trie changes are already
embodied in the nodes and state overlays. The incremental state root
algorithms use prefix_sets to identify which branches changed since
the last root computation.

Now we only Arc::clone the nodes and state (O(1)) and use a fresh
empty prefix_sets, which the caller extends with only the current
block's changes. This matches the existing pattern in merkle_changesets.rs.

Amp-Thread-ID: https://ampcode.com/threads/T-019b9306-9706-7736-8571-4a8d4acb814c
2026-01-06 23:29:13 +00:00
yongkangc
0a08af0288 fix: use rfind instead of filter().next_back() per clippy lint 2026-01-06 23:29:13 +00:00
yongkangc
9ca0850121 fix: address clippy lints (redundant clone, doc-markdown, double-ended-iterator-last) 2026-01-06 23:29:13 +00:00
yongkangc
3b38fe6bfb style: apply rustfmt formatting 2026-01-06 23:29:13 +00:00
yongkangc
d74914d86d perf(chain-state): fix O(N²) complexity in deferred trie overlay computation
**Summary**
Eliminate O(N²) complexity in sort_and_build_trie_input() by reusing the
parent block's already-merged trie overlay instead of re-merging all
ancestors for every block.

**Problem**
Previously, each block iterated through ALL ancestors calling wait_cloned()
on each to merge their state into the overlay. For block N, this meant N-1
wait_cloned() calls, leading to O(N²) total calls for N blocks. This became
a severe bottleneck under load when blocks arrive faster than the async
task can complete (sync fallback path).

**Solution**
- Fast path (O(1)): When parent's anchor_hash matches, clone its overlay
  which already contains all ancestors merged
- Slow path (O(N)): When anchor changes (after persist/reorg), rebuild from
  all ancestors - but this only happens at persist boundaries, not per block

**Changes**
- Modified sort_and_build_trie_input() to check if parent's overlay can be
  reused before falling back to full ancestor merge
- Added merge_ancestors_into_overlay() helper for the slow path
- Added metrics: deferred_trie_overlay_reused and deferred_trie_overlay_rebuilt
- Added comprehensive tests for fast path, slow path, chain building, and
  O(N) performance verification
- Documented invariants on AnchoredTrieInput for correctness guarantees

**Performance Impact**
- Before: 79,800 wait_cloned() calls for 400 blocks (O(N²))
- After: 400 wait_cloned() calls for 400 blocks (O(N))

Amp-Thread-ID: https://ampcode.com/threads/T-019b9262-f4d6-71c4-8fb0-36a245ad4c28
2026-01-06 23:29:13 +00:00
Snezhkko
5fa1b99bb6 docs: clarify TreeRootEntry::content unsigned format (#20790) 2026-01-06 22:10:05 +00:00
Alexey Shekhirin
d52b337127 fix(engine): do not create another cache for multiproof task (#20755) 2026-01-06 20:52:06 +00:00
Richard Janis Goldschmidt
342a795ebe chore: relax = requirement on cc dependency (#20788) 2026-01-06 18:09:40 +00:00
Matthias Seitz
485eb2e8d5 perf(trie): add clone_into_sorted for TrieUpdates and StorageTrieUpdates (#20784)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-06 15:11:27 +00:00
fig
63842264f3 perf(engine): parellelize multiproof_targets_from_state (#20669) 2026-01-06 14:03:09 +00:00
ethfanWilliam
e1d984035f perf: handle RPC errors instead of panicking (#20768) 2026-01-06 13:22:56 +00:00
Satoshi Nakamoto
d5fd0c04fc docs: fix doc comment errors (#20776) 2026-01-06 13:22:36 +00:00
かりんとう
8c5ff4b2fd perf: preallocate capacity for filter chunk results (#20783) 2026-01-06 13:21:30 +00:00
andrewshab
0ad5574115 chore(chain-state): remove needless collect in test assertions (#20778) 2026-01-06 13:19:55 +00:00
bigbear
485f5b36ce fix(transaction-pool): finalized block number should never decrease (#20781) 2026-01-06 13:16:22 +00:00
yyhrnk
d488a7d130 docs: align net JSON-RPC docs with implementation (#20782) 2026-01-06 13:11:56 +00:00
かりんとう
7bc3c95f05 perf: use parallel signature recovery in debug_trace_raw_block (#20780)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-06 13:06:06 +00:00
Hwangjae Lee
a64ac7c1c7 fix(consensus): prevent infinite reconnection loop in RpcBlockProvider when channel is closed (#20772)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-06 11:37:15 +00:00
Micke
9773e6233d perf(engine): prevent duplicate block insertion in BlockBuffer (#20487)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-06 10:51:55 +00:00
Ekaterina Endofer
1fd7a88e2e fix(era): correct error messages in CompressedBody and CompressedReceipts (#20695) 2026-01-06 10:16:51 +00:00
dependabot[bot]
dea27a55a8 chore(deps): bump taiki-e/cache-cargo-install-action from 2 to 3 (#20760)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 10:02:29 +00:00
ethfanWilliam
5f8d7ddd21 chore: make error handling consistent (#20769) 2026-01-06 09:54:32 +00:00
YK
44452359b9 fix(net): delay BlockRangeUpdate to avoid immediate sending after connection (#20765) 2026-01-06 09:48:30 +00:00
Hwangjae Lee
c1ef67df70 docs(payload): fix typos and incorrect references in comments (#20771)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-06 09:42:37 +00:00
Hwangjae Lee
0c6688d056 chore(consensus): fix typo in RpcBlockProvider log message (#20773)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-06 09:38:58 +00:00
YK
0b71c21986 ci(hive): revert to self-hosted Reth runner group (#20764) 2026-01-06 09:38:35 +00:00
VolodymyrBg
4d1c2c4939 refactor(ethereum): cache RLP lengths in ethereum payload builder (#20758) 2026-01-05 20:00:26 +00:00
NaCl-Ezpz
39b2dc8f4f chore: era decompression bounds (#20423)
Co-authored-by: NaCl <nacl@gaysex.local>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-05 19:50:41 +00:00
Karl Yu
e9e940919a feat: make metrics layer configurable (#20703) 2026-01-05 19:30:42 +00:00
ethfanWilliam
b6f95866cc feat(primitives-traits): add set_timestamp to test utils (#20756) 2026-01-05 19:20:09 +00:00
DaniPopes
fa05d19f1b fix(bench-compare): add backward compat for old CSV format (#20754) 2026-01-05 17:58:20 +00:00
bobtajson
981d1da41a chore(chain-state): remove needless collect in test assertions (#20736) 2026-01-05 17:22:58 +00:00
andrewshab
5ded234131 docs: update NetworkInner struct definition in network.md (#20752) 2026-01-05 17:09:23 +00:00
Hwangjae Lee
cfeaedd389 docs(net): fix typos in comments (#20751)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-05 17:07:33 +00:00
Mablr
7779d484a3 feat(optimism): Flashblock Receipts Stream (#20061)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-05 16:58:05 +00:00
cui
790a73cd2a chore: update todo (#20693) 2026-01-05 15:13:07 +00:00
cui
39e2c5167a feat: remove todo (#20692) 2026-01-05 15:03:46 +00:00
Satoshi Nakamoto
0f1bec0ad1 docs(network): sync struct definitions with sour (#20747) 2026-01-05 15:02:01 +00:00
cui
17c1365368 perf: prealloc vector (#20713)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 13:57:24 +00:00
cui
a7841919d9 perf: prealloc vector (#20716)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 13:56:28 +00:00
cui
0dbbb3ff37 perf: prealloc B256Map (#20720)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 13:54:10 +00:00
cui
96ff33120e perf: prealloc vec (#20721)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 13:53:17 +00:00
cui
f920ffd5f9 refactor: simplify code (#20722)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 13:52:48 +00:00
GarmashAlex
da1d7e542f refactor(rpc): remove unused BlockTransactionsResponseSender (#20696) 2026-01-05 13:52:01 +00:00
Satoshi Nakamoto
186208fef9 docs: fix doc comment errors (#20746) 2026-01-05 13:07:30 +00:00
cui
5265079654 perf: avoid one vec alloc (#20717)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 12:40:03 +00:00
cui
9ca5cffaee chore: update alloy (#20709)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 12:05:59 +00:00
Satoshi Nakamoto
b51ce5c155 docs(network): sync request handler structs with source (#20726) 2026-01-05 11:56:07 +00:00
andrewshab
8e9e595799 docs: update db.md BodyStage unwind implementation (#20727) 2026-01-05 11:54:57 +00:00
Satoshi Nakamoto
b77898c00d docs: fix doc comment errors (#20728) 2026-01-05 11:53:35 +00:00
cui
58b0125784 refactor: optimize check whether all blobs ready (#20711)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 11:53:06 +00:00
cui
e8cc91ebc2 fix: inclusive range off-by-one (#20729)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 11:39:38 +00:00
cui
59486a64d4 fix: to block should not sub one (#20730)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-05 11:35:22 +00:00
Hwangjae Lee
b1263d4651 docs(evm): fix typos and remove stale TODO (#20742)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-05 11:25:42 +00:00
kurahin
a79432ffc6 docs: fix discv5 multiaddr peer id conversion comment (#20743) 2026-01-05 11:22:32 +00:00
Karl Yu
480029a678 feat: optimize send_raw_transaction_sync receipts fetching (#20689) 2026-01-05 11:22:04 +00:00
DaniPopes
66f3453b3c feat(reth-bench-compare): add per-build features and rustflags args (#20744) 2026-01-05 11:11:23 +00:00
github-actions[bot]
3d4efdb271 chore(deps): weekly cargo update (#20735)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-01-04 11:31:03 +00:00
Doohyun Cho
5ac9184ba6 perf(era-utils): replace Box<dyn Fn> with function pointer (#20701) 2026-01-03 10:46:42 +00:00
Rej Ect
0e6efdb91c chore: bump license year to 2026 (#20704) 2026-01-03 10:45:34 +00:00
zhygis
986e07f21a feat(cli): make Cli extensible with custom subcommands (#20710)
Co-authored-by: Amp <amp@ampcode.com>
2026-01-03 10:41:56 +00:00
Sophia Raye
5307da4794 docs(eth-wire): sync code examples with source (#20724) 2026-01-03 11:45:07 +01:00
Karl Yu
0c69e294c3 chore: optimize evm_env if header is available (#20691) 2025-12-31 13:45:35 +00:00
かりんとう
dc931f5669 chore: use chain_id() method instead of direct field access in prometheus setup (#20687) 2025-12-31 08:53:44 +00:00
Hwangjae Lee
9cfe5c7363 fix(ipc): trim leading whitespace in StreamCodec decode (#20615)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2025-12-31 08:51:56 +00:00
fig
454b060d5a chore(tree): use with_capacity at collect_blocks_for_canonical_unwind() (#20682) 2025-12-30 12:32:02 +00:00
Matthias Seitz
0808bd67c2 chore: shrink outgoing broadcast messages (#20672) 2025-12-30 11:30:37 +00:00
iPLAY888
3b4bc77532 docs(network): update FetchClient struct to use NetworkPrimitives generic (#20680) 2025-12-30 11:23:12 +00:00
Sophia Raye
4eaa5c7d46 docs(eth-wire): add missing eth/70 message types (#20676) 2025-12-30 10:25:43 +00:00
iPLAY888
34c6b8d81c docs(network): update Swarm struct to use NetworkPrimitives generic (#20677) 2025-12-30 10:12:00 +00:00
Matthias Seitz
f79fdf3564 perf: pre-alloc removed vec (#20679) 2025-12-30 10:09:39 +00:00
Karl Yu
16f75bb0c3 feat: avoid mutex locking (#20678) 2025-12-30 09:28:40 +00:00
Hwangjae Lee
5053322711 docs(storage): fix typos in storage crates (#20673)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
Co-authored-by: YK <chiayongkang@hotmail.com>
2025-12-30 06:18:35 +00:00
YK
d72105b47c fix(storage): rocksdb consistency check on startup (#20596)
Co-authored-by: Federico Gimenez <fgimenez@users.noreply.github.com>
2025-12-30 06:17:32 +00:00
YK
0f585f892e perf(trie): flatten sparse trie branch node masks to reduce overhead (#20664) 2025-12-30 03:38:24 +00:00
iPLAY888
f7c77e72a7 docs(network): update NetworkConfig struct to match current API (#20665) 2025-12-29 22:00:40 +00:00
fig
fc248e3323 chore(stages): use with_capacity() at populate_range() (#20671) 2025-12-29 21:34:54 +00:00
Karl Yu
d564d9ba36 feat: add append_pooled_transaction_elements (#20654)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-29 21:00:40 +00:00
Hwangjae Lee
b7883953c4 chore(rpc): shrink active filters HashMap after clearing stale entries (#20660)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-29 20:45:52 +00:00
lisenokdonbassenok
b40b7dc210 docs: document http/ws api none option (#20666) 2025-12-29 20:43:27 +00:00
Matthias Seitz
65b5a149be chore: use with capacity (#20670) 2025-12-29 20:35:46 +00:00
Matthias Seitz
05ed753e58 chore: shrink range result vec to fit (#20639) 2025-12-29 10:22:11 +00:00
fig
624bfa1f49 perf(engine): paralellize evm_state_to_hashed_post_state() (#20635) 2025-12-29 10:06:08 +00:00
Desant pivo
d9c6f745c6 fix(chain-state): correct balance deduction in test block builder (#20308) 2025-12-29 09:59:19 +00:00
YK
240dc8602b perf(trie): flatten branch node mask to reduce overhead (#20659) 2025-12-29 07:35:46 +00:00
Matthias Seitz
489da4a38b perf: allocate signer vec exact size (#20638) 2025-12-29 02:18:27 +00:00
Matthias Seitz
05b3a8668c perf(trie): add FromIterator for HashedPostState and simplify from_bundle_state (#20653) 2025-12-28 11:29:07 +00:00
Hwangjae Lee
cb1de1ac19 docs(rpc): fix typos and complete incomplete doc comments (#20642)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2025-12-28 10:26:03 +00:00
github-actions[bot]
751a985ea7 chore(deps): weekly cargo update (#20650)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-28 09:37:00 +00:00
YK
a92cbb5e8b feat(storage): add AccountsHistory RocksDB consistency check (#20594) 2025-12-28 01:59:02 +00:00
DaniPopes
e595b58c28 feat: switch samply feature for CLI flags (#20586) 2025-12-27 15:16:49 +00:00
oooLowNeoNooo
a852084b43 fix(chainspec): use lazy error formatting in chain spec macro (#20643) 2025-12-26 11:18:57 +00:00
David Klank
5260532992 fix(rpc): use EthereumHardforks trait for Paris activation check (#20641) 2025-12-26 11:17:11 +00:00
bigbear
ca6853edd6 chore(primitives-traits): correct set_timestamp parameter name and type (#20637) 2025-12-25 12:07:03 +00:00
Matthias Seitz
8ae7a1c8d1 chore: ignore RUSTSEC-2025-0137 (#20633) 2025-12-24 23:32:49 +01:00
forkfury
150fd62bab docs: remove outdated gas metrics TODO (#20631) 2025-12-24 18:53:50 +01:00
fig
5fce0fea5e chore: remove stale insert_block_inner todo (#20632) 2025-12-24 18:35:37 +01:00
Doohyun Cho
0b90a613e0 perf(witness): avoid unnecessary HashMap clone when converting to BTreeMap (#20590) 2025-12-24 13:29:50 +00:00
James Niken
4fb453bb39 refactor: deduplicate dev_mining_mode logic (#20625) 2025-12-24 12:54:59 +00:00
ligt
97f6db61aa perf(persistence): optimize append_history_index with upsert (#19825)
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
2025-12-24 12:40:23 +00:00
Vitalyr
8e975f940c docs: remove deprecated --disable-deposit-contract-sync lighthouse flag (#20591) 2025-12-24 12:33:05 +00:00
Gigi
3ec1ca58e0 docs(exex): correct comparison order in backfill docs (#20592) 2025-12-24 12:30:31 +00:00
stevencartavia
ad37490e7d feat: integrate newPayload into ethstats (#20584)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-24 07:56:26 +00:00
Matthias Seitz
334d9f2a76 chore: defense against new variant (#20600) 2025-12-23 16:34:24 +00:00
Matthias Seitz
6627c19071 chore: add metric for batch size (#20610) 2025-12-23 16:10:38 +00:00
Brian Picciano
0b6361afa5 feat(engine): Prefetch storage and accounts when BAL is provided (#20468) 2025-12-23 16:04:05 +00:00
joshieDo
cf457689a6 docs: add additional context to PruneSenderRecoveryStage (#20606) 2025-12-23 15:30:23 +00:00
Matthias Seitz
6c49e5a89d chore: release lock early (#20605) 2025-12-23 15:09:45 +00:00
Brian Picciano
b79c58d835 feat(trie): Proof Rewrite: Support partial proofs (#20336)
Co-authored-by: YK <chiayongkang@hotmail.com>
2025-12-23 12:42:07 +00:00
Sophia Raye
9f2aea0494 docs: add missing debug methods to pruning tables (#20601) 2025-12-23 12:34:58 +00:00
strmfos
ff2081dcf0 fix(exex): update lowest_committed_block_height in WAL cache on insert (#20548)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-23 10:58:03 +00:00
Lorsmirq Benton
66db0839a0 chore: prevent false-positive log in trie repair (#20589)
Co-authored-by: YK <chiayongkang@hotmail.com>
2025-12-23 08:22:59 +00:00
AJStonewee
f8b927c6cd refactor(stages): use LazyLock for zero address hash (#20576)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-23 08:20:45 +00:00
DaniPopes
8374646e49 chore: fix formatting in launch_node (#20582) 2025-12-23 08:18:40 +00:00
DaniPopes
353c2a7f70 fix(cli): remove unnecessary bound from Cli::configure (#20583) 2025-12-23 03:52:04 +00:00
Matthias Seitz
21934d9946 fix: fuse shutdown (#20580) 2025-12-23 01:09:45 +00:00
cui
538de9e456 feat: update fork id in discv5[WIP] (#19139)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-23 00:30:36 +00:00
forkfury
b9d14d4a54 chore: delete redundant todo comment (#20571) 2025-12-23 00:14:05 +00:00
Matthew Vauxhall
529aa83777 chore: remove block_to_payload_v3 (#20540)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-23 00:10:38 +00:00
DaniPopes
da10201b88 chore: minor reth-bench cleanup (#20577) 2025-12-22 23:56:36 +00:00
Arsenii Kulikov
eec76a3faf perf: spawn prewarm workers in parallel (#20575) 2025-12-22 20:41:52 +00:00
Arsenii Kulikov
5e4a219182 perf: spawn prewarming before multiproof (#20572)
Co-authored-by: Brian Picciano <me@mediocregopher.com>
2025-12-22 17:56:14 +00:00
AJStonewee
ccb897f9a0 refactor(stages): cache hashed address in storage hashing loop (#20318)
Co-authored-by: Brian Picciano <me@mediocregopher.com>
2025-12-22 16:05:46 +00:00
radik878
f9d872e9cb fix(net): correct config builder doc comments (#20299) 2025-12-22 16:00:47 +00:00
Matthias Seitz
642bbea2a8 perf: make BlockState::parent_state_chain return iterator (#20496)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 15:58:46 +00:00
fuder.eth
1c4233d1b4 chore: prevent false-positive log when peer not found in transaction propagation (#20523) 2025-12-22 15:55:41 +00:00
Lorsmirq Benton
eeb2d55f44 docs: add debug execution witness methods to pruning tables (#20561) 2025-12-22 15:53:58 +00:00
fig
96c77fd8b2 feat(storage): make insert_block() operate with references (#20504)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-22 15:13:43 +00:00
VolodymyrBg
ed7a5696b7 fix(engine): sync invalid header cache count gauge on hit eviction (#20567) 2025-12-22 14:59:18 +00:00
Brian Picciano
5a3cffa3e9 fix(stage): Don't clear merkle changesets in unwind near genesis (#20568) 2025-12-22 14:56:18 +00:00
YK
535d97f39e refactor(provider): extract heal_segment for NippyJar consistency (#20508) 2025-12-22 14:01:12 +00:00
DaniPopes
f3aea8dac0 chore: simplify size functions (#20560) 2025-12-22 11:14:50 +00:00
Matthias Seitz
807fac0409 chore: use clone_into_consensus (#20530) 2025-12-22 12:15:09 +01:00
Brian Picciano
7b2fbdcd51 chore(db): Remove Sync from DbTx (#20516) 2025-12-22 10:13:57 +00:00
Merkel Tranjes
3b8acd4b07 feat(payload): add transaction_count to ExecutionPayload trait (#20534) 2025-12-22 10:07:31 +01:00
YK
62abfdaeb5 feat(cli): add tracing-samply to profiling (#20546) 2025-12-21 11:52:26 +00:00
emmmm
256a9fdb79 docs: add missing trace methods to pruning tables (#20547) 2025-12-21 12:40:58 +01:00
github-actions[bot]
4d9aff99bf chore(deps): weekly cargo update (#20545)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2025-12-21 12:40:14 +01:00
Vitalyr
28bb2891bb refactor(consensus): simplify verify_receipts return (#20517) 2025-12-20 19:05:50 +01:00
kurahin
1d8f265744 chore(net): remove stale ECIES rand TODO (#20531) 2025-12-20 19:05:37 +01:00
Matthias Seitz
c754caf8c7 fix: remove stale blobs (#20528) 2025-12-20 15:35:22 +00:00
cui
e1b0046329 chore: remove todo after jovian fork (#20535)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2025-12-20 15:31:08 +00:00
cui
ddfe177578 chore: remove todo (#20533)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2025-12-20 15:19:53 +00:00
Gigi
178558c6d7 fix(tree): correct block buffer eviction policy comment (#20512) 2025-12-20 09:44:51 +00:00
Emilia Hane
f4d3a9701f chore(trie): Rm redundant clone of propagated error (#20466)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-20 08:42:20 +00:00
Gigi
42e41a9370 docs: add reth JSON-RPC namespace documentation (#20522) 2025-12-20 08:03:06 +00:00
pepes
a66dcce834 chore(evm): remove deprecated state_change compatibility alias (#20518) 2025-12-20 07:50:12 +00:00
Arsenii Kulikov
21d835cf2b perf: use LRU eviction policy for precompile cache (#20527) 2025-12-20 02:12:42 +00:00
Alexey Shekhirin
29438631be fix: propagate keccak-cache-global feature to reth-node-core (#20524) 2025-12-19 17:11:41 +00:00
Brian Picciano
0eb4e0ce29 fix(stages): Fix two bugs related to stage checkpoints and pipeline syncs (#20521) 2025-12-19 16:09:57 +00:00
gustavo
9147f9aafe perf(trie): remove more unnecessary channels (#20489) 2025-12-19 15:34:42 +00:00
Snezhkko
13b111e058 refactor: remove dead storage multiproof path (#20485) 2025-12-19 15:11:31 +00:00
leniram159
25c247b14c refactor(engine): simplify fork detection in insert_block (#20441) 2025-12-19 14:49:33 +00:00
Matthias Seitz
72bea44d8c chore: remove redundant num hash (#20501) 2025-12-19 14:48:42 +00:00
alex017
63b9d5fe57 refactor(db-api): remove redundant clone and unused import in unwind (#20499)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-19 14:47:11 +00:00
Arsenii Kulikov
30162c535e perf: properly share precompile cache + use moka (#20502) 2025-12-18 22:42:44 +00:00
Federico Gimenez
cd8fec3273 feat(stages): use EitherWriter for TransactionLookupStage RocksDB writes (#20428) 2025-12-18 21:34:17 +00:00
Tomass
1e38c7fea8 chore(hardforks): drop unnecessary field reassignment in TTD branch (#20457)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-18 21:02:56 +00:00
Block Wizard
4dfaf238c9 chore(net): fix misleading comment about uncompressed message size check (#19510) 2025-12-18 20:34:50 +00:00
forkfury
4cf36dda54 docs: correct FinishedStateUpdates message name (#20471) 2025-12-18 20:16:15 +00:00
phrwlk
41ce3d3bbf docs: fix Docker db-access troubleshooting example (#20483) 2025-12-18 20:13:01 +00:00
sashass1315
429d13772e chore(cli): correct p2p body error message (#20498) 2025-12-18 20:01:59 +00:00
Gigi
0cbf89193d docs: correct intra-doc link references (#20467) 2025-12-18 19:56:57 +00:00
radik878
0c3c42bffe chore(primitives-traits): correct SealedBlock::senders return description (#20465) 2025-12-18 19:56:22 +00:00
cui
cdbbd08677 fix: session config should be read from config file (#20484)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2025-12-18 19:53:18 +00:00
Alexey Shekhirin
4adb1fa5ac fix(cli): default to 0 genesis block number (#20494) 2025-12-18 15:07:59 +00:00
Brian Picciano
b3a792ad1e fix(engine): Use OverlayStateProviderFactory for state root fallback (#20462) 2025-12-18 14:30:11 +00:00
Arsenii Kulikov
98a7095c7a fix: properly determine first stage during pipeline consistency check (#20460) 2025-12-18 10:43:08 +00:00
Matthias Seitz
701e5ec455 chore: add engine terminate (#20420)
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
2025-12-18 09:01:36 +00:00
Lorsmirq Benton
8e00e81af4 docs: remove orphaned debug.mdx (#20474) 2025-12-18 04:14:23 +00:00
YK
453514c48f perf(engine): share Arc<ExecutionOutcome> to avoid cloning BundleState (#20448) 2025-12-18 01:07:18 +00:00
James Niken
432ac7afa1 chore: fix blob count in validation benchmark (#20456) 2025-12-18 00:51:45 +00:00
Emilia Hane
c7fca9f2b4 chore(node): Report actual gas price to ethstats (#20461)
Co-authored-by: Rifvck Zieger <rifvckzieger@gmail.com>
2025-12-18 00:50:16 +00:00
DaniPopes
715ca5b980 chore: simplify prewarm state providers (#20469) 2025-12-17 22:11:11 +00:00
Federico Gimenez
9ae62aad26 feat(storage): add method to check invariants on RocksDB tables (#20340) 2025-12-17 20:26:51 +00:00
YK
c65df40526 perf: remove redundant contains_key check in ProofSequencer::add_proof (#20459) 2025-12-17 13:58:59 +00:00
Vui-Chee
d8acc1e4cf feat: support non-zero genesis block numbers (#19877)
Co-authored-by: JimmyShi22 <417711026@qq.com>
2025-12-17 11:03:12 +00:00
sashass1315
852aad8126 docs(exex): document ChainRevert flow in how-it-works (#20455) 2025-12-17 10:28:49 +00:00
Karl Yu
61c072ad20 feat: add engine_getBlobsV3 method (#20451) 2025-12-17 10:15:49 +00:00
Lorsmirq Benton
6a5b985113 docs: remove orphaned recover CLI documentation (#20447) 2025-12-17 10:13:55 +00:00
joshieDo
1adc6aec00 chore(engine): extract on_persistence_complete (#20443) 2025-12-17 09:07:54 +00:00
Matthias Seitz
5edc16ad85 perf: only populate cache during prewarm (#20445) 2025-12-17 08:46:16 +00:00
phrwlk
f54a8a1ef5 fix(payload): clarify PayloadTransactions mark_invalid semantics (#20452) 2025-12-17 08:44:17 +00:00
leniram159
c681851ec8 chore: make docs correct (#20440)
Co-authored-by: YK <chiayongkang@hotmail.com>
2025-12-17 04:32:18 +00:00
DaniPopes
d964fcbcde chore: simplify execution state providers (#20444) 2025-12-16 22:52:57 +00:00
Alexey Shekhirin
e79691aae7 feat: turn on asm-keccak by default, use maxperf profile in Dockerfiles (#20422) 2025-12-16 22:43:20 +00:00
bigbear
4231f4b688 docs: fix incorrect API example in node-components.mdx (#20297) 2025-12-16 15:09:29 +00:00
Léa Narzis
0b607113dc refactor(era): make era count in era file name optional (#20292) 2025-12-16 15:08:43 +00:00
emmmm
be4dc53b92 docs: fix --color auto option description (#20352) 2025-12-16 15:06:04 +00:00
emmmm
4afb555d06 docs(opstack): document all rollup CLI arguments (#20374) 2025-12-16 15:04:34 +00:00
Matthias Seitz
ab2ef99458 chore: add keccak-global (#20418) 2025-12-16 14:59:09 +00:00
Sophia Raye
bfd4b79245 docs(trace): remove duplicate comment (#20360) 2025-12-16 14:56:01 +00:00
Federico Gimenez
49057b1c0c feat(storage): add with_default_tables() to register RocksDB column families at initialization (#20416) 2025-12-16 12:59:58 +00:00
Gigi
b6772370d7 docs: fix incorrect method reference in try_recover_sealed_with_senders (#20410) 2025-12-16 12:27:53 +00:00
Karl Yu
d72935628a feat: add support for eth/70 eip-7975 (#20255)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-16 12:05:11 +00:00
YK
ad63b135d6 feat(storage): implement EitherWriter/EitherReader methods for RocksDB (#20408) 2025-12-16 11:26:31 +00:00
Brian Picciano
90651ae8e8 feat(engine): Use BAL in state root validation (#20383) 2025-12-16 11:05:51 +00:00
Matthias Seitz
bbd51862d4 chore: rm flaky bench (#20413) 2025-12-16 09:35:38 +00:00
Arsenii Kulikov
08a16a5bde perf: recover transactions in parallel during network import (#20385) 2025-12-16 09:33:24 +00:00
Snezhkko
f2c39db7a2 chore(rpc): fix misleading link and comment (#20367) 2025-12-16 09:32:25 +00:00
oooLowNeoNooo
ae9e84d6e3 fix(discv4): correct ping_interval default value in docs (#20396) 2025-12-16 09:29:45 +00:00
theo
c51da593d1 feat(net/p2p): support fixed external addresses with DNS resolution (#20411) 2025-12-16 09:28:31 +00:00
Matthias Seitz
0e08f9f56c perf: remove unnecessary channels from parallel trie operations (#20406) 2025-12-16 09:15:27 +00:00
sashass1315
7eef092110 docs(exex): sync hello-world notifications loop with code (#20403) 2025-12-16 08:39:45 +00:00
YK
40e8241bf5 feat(storage): use RocksDBBatch in EitherWriter and related modules (#20377) 2025-12-16 03:57:41 +00:00
dependabot[bot]
dd9ff731e4 chore(deps): bump peter-evans/create-pull-request from 7 to 8 (#20402)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 00:11:22 +00:00
dependabot[bot]
83f9d1837f chore(deps): bump actions/download-artifact from 4 to 7 (#20401)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 00:11:00 +00:00
dependabot[bot]
68911e617b chore(deps): bump actions/upload-artifact from 5 to 6 (#20400)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-16 00:10:40 +00:00
0xcharry
36ba6db029 chore: remove redundant .as_str() calls after to_string() (#20404) 2025-12-16 00:10:03 +00:00
Matthias Seitz
fec4432d82 perf: defer transaction pool notifications until after lock release (#20405) 2025-12-15 23:06:34 +00:00
Matthias Seitz
179da26305 perf: use RwLock for transaction pool listeners (#20398)
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2025-12-15 21:47:59 +00:00
Matthias Seitz
b5e7a694d2 chore: update metric once (#20371) 2025-12-15 20:38:24 +00:00
Maxim Evtush
9489667814 fix: post-state generator to include deletions in proptest (#20276) 2025-12-15 16:43:02 +00:00
gustavo
004877ba59 refactor(cli): cleanup repair-trie metrics (#20226) 2025-12-15 16:41:48 +00:00
Brian Picciano
a9e36923e1 feat(trie): Proof Rewrite: Use cached branch nodes (#20075)
Co-authored-by: YK <chiayongkang@hotmail.com>
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
2025-12-15 15:27:04 +00:00
DaniPopes
74a3816611 ci: reduce feature powerset depth (#20379) 2025-12-15 14:42:14 +00:00
Alexey Shekhirin
5576d4547f revert: feat(engine): run sync state root if not enough parallelism (#20127) (#20378) 2025-12-15 14:05:54 +00:00
DaniPopes
21216e2f24 perf: use indexed parallel iterators for tx recovery (#20342) 2025-12-15 13:40:03 +00:00
YK
42c1e1afe1 feat(storage): add account history constructors to EitherWriter/EitherReader (#20366) 2025-12-15 12:45:07 +00:00
MoNyAvA
5f7e87fa2a docs: add blob sub-pool to tx pool docs (#20375) 2025-12-15 12:27:54 +00:00
Matthias Seitz
1b417dacc4 chore: sanity check for u64::Max (#20373) 2025-12-15 11:33:50 +00:00
Niven
bb952be5b5 feat(flashblocks): support eth_getBlockTransactionCount for flashblocks (#20291)
Co-authored-by: lucas <66681646+limyeechern@users.noreply.github.com>
Co-authored-by: lucas.lim <lucas.lim@okg.com>
2025-12-15 11:29:23 +00:00
Federico Magnani
f927eec880 chore: export FlashBlockDecoder (#20370) 2025-12-15 11:00:46 +00:00
Tomass
9c61f5568c fix(rpc-testing-util): use buffer_unordered in trace_block_opcode_gas_unordered (#20369) 2025-12-15 10:38:40 +00:00
ligt
662c0486a1 feat(storage): add rocksdb provider into database provider (#20253) 2025-12-15 10:15:57 +00:00
Matthias Seitz
997848c2a1 fix(txpool): remove stale senderinfo (#20368) 2025-12-15 10:00:25 +00:00
Olexandr88
155bdecf3b docs(repo): add Ethereum-specific crates section (#20363) 2025-12-15 09:56:40 +00:00
github-actions[bot]
679234f105 chore(deps): weekly cargo update (#20359)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2025-12-14 20:54:42 +00:00
phrwlk
419c7b489b fix(rpc): remove dead flashbots module config (#20364) 2025-12-14 20:54:15 +00:00
Rej Ect
06dac07b5f ci(hive): bump actions/cache to v5 (#20349) 2025-12-13 09:04:07 +00:00
YK
5621132b8b feat: add RocksDB variant to EitherReader and EitherWriter (#20288) 2025-12-13 04:06:44 +00:00
Matthias Seitz
3380eb69c8 fix: only collect already tracked accounts (#20341) 2025-12-12 22:09:21 +00:00
Arsenii Kulikov
0366497ada perf: skip redundant recovery (#20343) 2025-12-12 22:01:05 +00:00
Alexey Shekhirin
cd71f3d5a4 feat(engine): record total latencies on instrumented state provider drop (#20337) 2025-12-12 21:14:44 +00:00
Alexey Shekhirin
64909d33e6 feat(engine): cli argument to disable state cache (#20143) 2025-12-12 17:51:22 +00:00
Alexey Shekhirin
3c9ad31344 chore(engine): make InstrumentedStateProvider public (#20335) 2025-12-12 16:41:42 +00:00
gustavo
f3e14fd061 feat(rpc): handle dedicated eth_simulate errors (#20099) 2025-12-12 16:40:13 +00:00
Alexey Shekhirin
daf6b88dc6 feat(node): engine args defaults (#20203) 2025-12-12 15:54:05 +00:00
emmmm
d2d58f9a0e docs: add missing RPC namespaces to JSON-RPC intro (#20321) 2025-12-12 15:40:38 +00:00
Matthias Seitz
ace4e515b5 chore: bump inspectors 0.33.2 (#20334) 2025-12-12 15:39:04 +00:00
Hesham Shabanah
134164954b feat: add --max-peers CLI flag (#20139)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-12 13:26:44 +00:00
Lorsmirq Benton
2775dd1f23 docs: correct comments in custom-inspector (#20304) 2025-12-12 13:21:03 +00:00
Alexey Shekhirin
ac0f9687bd chore(engine): move noisy multiproof debug logs to trace level (#20331) 2025-12-12 13:01:01 +00:00
Arsenii Kulikov
a9c21a395d perf: spawn rpc handlers as blocking (#20330) 2025-12-12 12:15:02 +00:00
Federico Magnani
df7ad9ae45 chore(ethapi): increase visibility tx_batch_sender (#20315)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-12 12:14:43 +00:00
sashass1315
5903e42a98 docs: refresh repo layout crate lists (#20319) 2025-12-12 10:59:57 +00:00
Matthias Seitz
3c41b99599 chore: lower block buffer size (#20324) 2025-12-12 08:15:54 +00:00
pepes
d70d80fff1 fix(docs): document discv5 discovery port 9200 (#20322) 2025-12-12 08:12:08 +00:00
gustavo
ed3a8a03d5 feat(node-core): make rpc server args customizable (#20312) 2025-12-11 23:24:31 +00:00
YK
bfcd46d01d feat: add account_history_in_rocksdb field to StorageSettings (#20282) 2025-12-11 19:37:36 +00:00
Brian Picciano
194d545fae feat(engine): Add BAL stub methods to ExecutionPayload and BlockOrPayload (#20311)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-11 19:07:43 +00:00
sashass1315
97243ec1f4 docs: fix misleading links (#20300) 2025-12-11 18:49:18 +00:00
DaniPopes
93c1b0f52f ci: add more sccache (#20316) 2025-12-11 18:46:11 +00:00
Arsenii Kulikov
474c09095f feat: bump alloy-evm (#20314) 2025-12-11 19:46:34 +01:00
Matthias Seitz
24c298133f feat: allow larger ws frames on client side (#20307) 2025-12-11 16:43:10 +00:00
Block Wizard
da27336a1e docs: add architecture diagrams to ExEx documentation (#20193)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-11 11:41:15 +00:00
Matthias Seitz
2e567d6658 feat: add semaphore for blocking IO requests (#20289) 2025-12-11 11:35:50 +00:00
Alexey Shekhirin
28e7c8a7cb ci: scale down depot runners (#20295) 2025-12-11 11:33:49 +00:00
Matthias Seitz
a2a5e03cb8 perf: fetch header directly (#20294) 2025-12-11 11:18:51 +00:00
Sophia Raye
6073aa5b4a docs(exex): fix DebugApi comment (#20296) 2025-12-11 10:06:31 +00:00
Karl Yu
e90cfedf3d feat: add support for testing_ rpc namespace and testing_buildBlockV1 (#20094)
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-11 08:56:46 +00:00
Matthias Seitz
8b27ca6fa2 chore: update engine_getBlobs metric (#20290) 2025-12-11 08:11:54 +00:00
Tomass
1752d6fb99 chore(optimism): move predeploy constant to op-alloy (#20181)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-11 07:04:01 +00:00
emmmm
ac891a780b docs: fix stages order and add missing EraStage (#20283) 2025-12-11 06:26:27 +00:00
Adrian
036626b8a7 docs: improve map_add_ons method documentation (#20248) 2025-12-11 06:03:34 +00:00
josé v
68f0c9812f feat: add transaction_hash_numbers_in_rocksdb field to StorageSettings (#20209) 2025-12-11 01:07:12 +00:00
sashass1315
c9920c9690 docs: clarify network mode, tx gossip and NAT (#20247) 2025-12-10 21:52:04 +00:00
Karl Yu
af82606ff4 feat: add support for debug_getBadBlock (#20177)
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2025-12-10 21:03:53 +00:00
radik878
38331a362e fix(rpc): avoid signing Optimism deposit transactions (#20254) 2025-12-10 20:46:43 +00:00
Tomass
e8dae2ae7d chore(deps): bump op-alloy to 0.23.0 (#20256)
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2025-12-10 20:44:54 +00:00
Sophia Raye
ce5f90175b docs(jsonrpc): add missing debug namespace RPC methods (#20267) 2025-12-10 17:24:29 +00:00
gustavo
8c361c87c2 feat(txpool): handle more simulated scenarios in test_utils/pool.rs (#20138) 2025-12-10 17:13:59 +00:00
Block Wizard
4fbbb1fe54 feat: add recover_transactions_unchecked_ref to BlockBody (#20266) 2025-12-10 17:13:08 +00:00
Brian Picciano
b7d8815104 perf(prune): use delete_current_duplicates for MerkleChangeSets tables (#20230) 2025-12-10 13:33:11 +00:00
Alexey Shekhirin
b91cd8f451 ci: sccache (#20265) 2025-12-10 13:05:25 +00:00
Alexey Shekhirin
09aee4e35a ci: use 16 cores for Hive workflow (#20264) 2025-12-10 13:02:14 +00:00
Alexey Shekhirin
505a384b10 ci: increase partitions for crate-checks to 3 (#20261) 2025-12-10 13:02:11 +00:00
490 changed files with 16050 additions and 5974 deletions

View File

@@ -12,7 +12,7 @@ workflows:
# Check that `A` activates the features of `B`.
"propagate-feature",
# These are the features to check:
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,js-tracer,portable",
"--features=std,op,dev,asm-keccak,jemalloc,jemalloc-prof,tracy-allocator,serde-bincode-compat,serde,test-utils,arbitrary,bench,alloy-compat,min-error-logs,min-warn-logs,min-info-logs,min-debug-logs,min-trace-logs,otlp,js-tracer,portable,keccak-cache-global",
# Do not try to add a new section to `[features]` of `A` only because `B` exposes that feature. There are edge-cases where this is still needed, but we can add them manually.
"--left-side-feature-missing=ignore",
# Ignore the case that `A` it outside of the workspace. Otherwise it will report errors in external dependencies that we have no influence on.

View File

@@ -11,17 +11,14 @@ go build .
# Run each hive command in the background for each simulator and wait
echo "Building images"
./hive -client reth --sim "ethereum/eels" \
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/bal@v1.8.0/fixtures_bal.tar.gz \
--sim.buildarg branch=eips/amsterdam/eip-7928 \
--sim.timelimit 1s || true &
# TODO: test code has been moved from https://github.com/ethereum/execution-spec-tests to https://github.com/ethereum/execution-specs we need to pin eels branch with `--sim.buildarg branch=<release-branch-name>` once we have the fusaka release tagged on the new repo
./hive -client reth --sim "ethereum/eels" --sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz -sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/rpc-compat" -sim.timelimit 1s || true &
./hive -client reth --sim "smoke/genesis" -sim.timelimit 1s || true &
./hive -client reth --sim "smoke/network" -sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/sync" -sim.timelimit 1s || true &
wait
# Run docker save in parallel, wait and exit on error
@@ -40,7 +37,7 @@ for pid in "${saving_pids[@]}"; do
wait "$pid" || exit
done
# Make sure we don't rebuild images on the CI jobs
# Make sure we don't rebuild images on the CI jobs
git apply ../.github/assets/hive/no_sim_build.diff
go build .
mv ./hive ../hive_assets/

View File

@@ -24,4 +24,4 @@ done
wait
docker image ls -a
docker image ls -a

View File

@@ -7,7 +7,7 @@ sim="${1}"
limit="${2}"
run_hive() {
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism 8 --client reth 2>&1 | tee /tmp/log || true
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism 16 --client reth 2>&1 | tee /tmp/log || true
}
check_log() {

View File

@@ -3,14 +3,15 @@
on:
pull_request:
# TODO: Disabled temporarily for https://github.com/CodSpeedHQ/runner/issues/55
# merge_group :
# merge_group:
push:
branches: ["**"]
branches: [main]
env:
CARGO_TERM_COLOR: always
BASELINE: base
SEED: reth
RUSTC_WRAPPER: "sccache"
name: bench
jobs:
@@ -22,6 +23,7 @@ jobs:
submodules: true
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

View File

@@ -10,9 +10,12 @@ on:
types: [opened, reopened, synchronize, closed]
merge_group:
env:
RUSTC_WRAPPER: "sccache"
jobs:
build:
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest-8
timeout-minutes: 90
steps:
- name: Checkout
@@ -33,6 +36,8 @@ jobs:
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Build docs
run: cd docs/vocs && bash scripts/build-cargo-docs.sh
@@ -69,4 +74,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v4

View File

@@ -9,15 +9,16 @@ on:
pull_request:
merge_group:
push:
branches: ["**"]
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: "sccache"
name: compact-codec
jobs:
compact-codec:
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest
strategy:
matrix:
bin:
@@ -26,6 +27,7 @@ jobs:
steps:
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

View File

@@ -6,11 +6,12 @@ on:
pull_request:
merge_group:
push:
branches: ["**"]
branches: [main]
env:
CARGO_TERM_COLOR: always
SEED: rustethereumethereumrust
RUSTC_WRAPPER: "sccache"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -19,13 +20,14 @@ concurrency:
jobs:
test:
name: e2e-testsuite
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest-4
env:
RUST_BACKTRACE: 1
timeout-minutes: 90
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
with:

View File

@@ -6,9 +6,7 @@ on:
workflow_dispatch:
schedule:
- cron: "0 */6 * * *"
pull_request:
branches:
- main
env:
CARGO_TERM_COLOR: always
@@ -26,14 +24,14 @@ jobs:
prepare-hive:
if: github.repository == 'paradigmxyz/reth'
timeout-minutes: 45
runs-on: depot-ubuntu-latest
runs-on:
group: Reth
steps:
- uses: actions/checkout@v6
- name: Checkout hive tests
uses: actions/checkout@v6
with:
repository: ethereum/hive
ref: master
path: hivetests
- name: Get hive commit hash
@@ -47,7 +45,7 @@ jobs:
- name: Restore hive assets cache
id: cache-hive
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ./hive_assets
key: hive-assets-${{ steps.hive-commit.outputs.hash }}-${{ hashFiles('.github/assets/hive/build_simulators.sh') }}
@@ -70,7 +68,7 @@ jobs:
chmod +x hive
- name: Upload hive assets
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: hive_assets
path: ./hive_assets
@@ -118,29 +116,27 @@ jobs:
# eth_ rpc methods
- sim: ethereum/rpc-compat
include:
# - eth_blockNumber
- eth_blockNumber
- eth_call
# - eth_chainId
# - eth_createAccessList
# - eth_estimateGas
# - eth_feeHistory
# - eth_getBalance
# - eth_getBlockBy
# - eth_getBlockTransactionCountBy
# - eth_getCode
# - eth_getProof
# - eth_getStorage
# - eth_getTransactionBy
# - eth_getTransactionCount
# - eth_getTransactionReceipt
# - eth_sendRawTransaction
# - eth_syncing
# # debug_ rpc methods
# - debug_
- eth_chainId
- eth_createAccessList
- eth_estimateGas
- eth_feeHistory
- eth_getBalance
- eth_getBlockBy
- eth_getBlockTransactionCountBy
- eth_getCode
- eth_getProof
- eth_getStorage
- eth_getTransactionBy
- eth_getTransactionCount
- eth_getTransactionReceipt
- eth_sendRawTransaction
- eth_syncing
# debug_ rpc methods
- debug_
# consume-engine
- sim: ethereum/eels/consume-engine
limit: .*tests/amsterdam.*
- sim: ethereum/eels/consume-engine
limit: .*tests/osaka.*
- sim: ethereum/eels/consume-engine
@@ -157,10 +153,10 @@ jobs:
limit: .*tests/homestead.*
- sim: ethereum/eels/consume-engine
limit: .*tests/frontier.*
- sim: ethereum/eels/consume-engine
limit: .*tests/paris.*
# consume-rlp
- sim: ethereum/eels/consume-rlp
limit: .*tests/amsterdam.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/osaka.*
- sim: ethereum/eels/consume-rlp
@@ -177,11 +173,14 @@ jobs:
limit: .*tests/homestead.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/frontier.*
- sim: ethereum/eels/consume-rlp
limit: .*tests/paris.*
needs:
- prepare-reth
- prepare-hive
name: run ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
runs-on: depot-ubuntu-latest
runs-on:
group: Reth
permissions:
issues: write
steps:
@@ -190,13 +189,13 @@ jobs:
fetch-depth: 0
- name: Download hive assets
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
name: hive_assets
path: /tmp
- name: Download reth image
uses: actions/download-artifact@v5
uses: actions/download-artifact@v7
with:
name: artifacts
path: /tmp
@@ -248,7 +247,7 @@ jobs:
notify-on-error:
needs: test
if: failure()
runs-on: depot-ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@v2

View File

@@ -6,7 +6,7 @@ on:
pull_request:
merge_group:
push:
branches: ["**"]
branches: [main]
schedule:
# Run once a day at 3:00 UTC
- cron: "0 3 * * *"
@@ -14,6 +14,7 @@ on:
env:
CARGO_TERM_COLOR: always
SEED: rustethereumethereumrust
RUSTC_WRAPPER: "sccache"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -23,7 +24,7 @@ jobs:
test:
name: test / ${{ matrix.network }}
if: github.event_name != 'schedule'
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest-4
env:
RUST_BACKTRACE: 1
strategy:
@@ -37,6 +38,7 @@ jobs:
- name: Install Geth
run: .github/assets/install_geth.sh
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -74,6 +76,7 @@ jobs:
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

View File

@@ -41,7 +41,7 @@ jobs:
fetch-depth: 0
- name: Download reth image
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: artifacts
path: /tmp
@@ -85,7 +85,7 @@ jobs:
notify-on-error:
needs: test
if: failure()
runs-on: depot-ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@v2

View File

@@ -39,7 +39,7 @@ jobs:
fetch-depth: 0
- name: Download reth image
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
with:
name: artifacts
path: /tmp
@@ -58,7 +58,7 @@ jobs:
notify-on-error:
needs: test
if: failure()
runs-on: depot-ubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Slack Webhook Action
uses: rtCamp/action-slack-notify@v2

View File

@@ -4,10 +4,11 @@ on:
pull_request:
merge_group:
push:
branches: ["**"]
branches: [main]
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: "sccache"
jobs:
clippy-binaries:
@@ -26,6 +27,7 @@ jobs:
- uses: dtolnay/rust-toolchain@clippy
with:
components: clippy
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -40,7 +42,7 @@ jobs:
clippy:
name: clippy
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
@@ -48,6 +50,7 @@ jobs:
- uses: dtolnay/rust-toolchain@nightly
with:
components: clippy
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -65,6 +68,7 @@ jobs:
with:
target: wasm32-wasip1
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -84,6 +88,7 @@ jobs:
with:
target: riscv32imac-unknown-none-elf
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -93,17 +98,18 @@ jobs:
crate-checks:
name: crate-checks (${{ matrix.partition }}/${{ matrix.total_partitions }})
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest-4
strategy:
matrix:
partition: [1, 2]
total_partitions: [2]
partition: [1, 2, 3]
total_partitions: [3]
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-hack
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -124,6 +130,7 @@ jobs:
- uses: dtolnay/rust-toolchain@master
with:
toolchain: "1.88" # MSRV
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -133,12 +140,13 @@ jobs:
docs:
name: docs
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest-4
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -158,17 +166,19 @@ jobs:
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Run fmt
run: cargo fmt --all --check
udeps:
name: udeps
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -177,12 +187,13 @@ jobs:
book:
name: book
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@nightly
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -234,17 +245,14 @@ jobs:
# Checks that selected crates can compile with power set of features
features:
name: features (${{ matrix.partition }}/${{ matrix.total_partitions }})
runs-on: depot-ubuntu-latest-16
strategy:
matrix:
partition: [1, 2]
total_partitions: [2]
name: features
runs-on: depot-ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@clippy
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -256,7 +264,7 @@ jobs:
--package reth-primitives-traits \
--package reth-primitives \
--feature-powerset \
--partition ${{ matrix.partition }}/${{ matrix.total_partitions }}
--depth 2
env:
RUSTFLAGS: -D warnings
@@ -267,8 +275,9 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: rui314/setup-mold@v1
- uses: taiki-e/cache-cargo-install-action@v2
- uses: taiki-e/cache-cargo-install-action@v3
with:
tool: zepter
- name: Eagerly pull dependencies

View File

@@ -50,7 +50,7 @@ jobs:
- name: Upload reth image
id: upload
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: artifacts
path: ./artifacts

View File

@@ -22,6 +22,7 @@ env:
CARGO_TERM_COLOR: always
DOCKER_IMAGE_NAME_URL: https://ghcr.io/${{ github.repository_owner }}/reth
DOCKER_OP_IMAGE_NAME_URL: https://ghcr.io/${{ github.repository_owner }}/op-reth
RUSTC_WRAPPER: "sccache"
jobs:
dry-run:
@@ -51,6 +52,7 @@ jobs:
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Verify crate version matches tag
# Check that the Cargo version starts with the tag,
# so that Cargo version 1.4.8 can be matched against both v1.4.8 and v1.4.8-rc.1
@@ -104,6 +106,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
target: ${{ matrix.configs.target }}
- uses: mozilla-actions/sccache-action@v0.0.9
- name: Install cross main
id: cross_main
run: |
@@ -141,14 +144,14 @@ jobs:
- name: Upload artifact
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz
- name: Upload signature
if: ${{ github.event.inputs.dry_run != 'true' }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
path: ${{ matrix.build.binary }}-${{ needs.extract-version.outputs.VERSION }}-${{ matrix.configs.target }}.tar.gz.asc
@@ -170,7 +173,7 @@ jobs:
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v6
uses: actions/download-artifact@v7
- name: Generate full changelog
id: changelog
run: |

View File

@@ -42,7 +42,7 @@ jobs:
echo "Binaries SHA256 on ${{ matrix.machine }}: $(cat checksum.sha256)"
- name: Upload the hash
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: checksum-${{ matrix.machine }}
path: |
@@ -55,12 +55,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download artifacts from machine-1
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: checksum-machine-1
path: machine-1/
- name: Download artifacts from machine-2
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: checksum-machine-2
path: machine-2/

View File

@@ -6,12 +6,13 @@ on:
pull_request:
merge_group:
push:
branches: ["**"]
branches: [main]
env:
CARGO_TERM_COLOR: always
FROM_BLOCK: 0
TO_BLOCK: 50000
RUSTC_WRAPPER: "sccache"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -31,6 +32,7 @@ jobs:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

View File

@@ -9,6 +9,7 @@ on:
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: "sccache"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -41,6 +42,7 @@ jobs:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

View File

@@ -9,6 +9,7 @@ on:
env:
CARGO_TERM_COLOR: always
RUSTC_WRAPPER: "sccache"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -41,6 +42,7 @@ jobs:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

View File

@@ -4,14 +4,14 @@ name: unit
on:
pull_request:
branches: ["**"]
merge_group:
push:
branches: ["**"]
branches: [main]
env:
CARGO_TERM_COLOR: always
SEED: rustethereumethereumrust
RUSTC_WRAPPER: "sccache"
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
@@ -20,7 +20,7 @@ concurrency:
jobs:
test:
name: test / ${{ matrix.type }} (${{ matrix.partition }}/${{ matrix.total_partitions }})
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest-4
env:
RUST_BACKTRACE: 1
strategy:
@@ -47,6 +47,7 @@ jobs:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -92,6 +93,7 @@ jobs:
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -99,7 +101,7 @@ jobs:
doc:
name: doc tests
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest
env:
RUST_BACKTRACE: 1
timeout-minutes: 30
@@ -107,6 +109,7 @@ jobs:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@stable
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

View File

@@ -27,7 +27,7 @@ jobs:
./fetch_superchain_config.sh
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
commit-message: "chore: update superchain config"
title: "chore: update superchain config"

View File

@@ -4,14 +4,17 @@ name: windows
on:
push:
branches: ["**"]
branches: [main]
pull_request:
branches: ["**"]
branches: [main]
merge_group:
env:
RUSTC_WRAPPER: "sccache"
jobs:
check-reth:
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest
timeout-minutes: 60
steps:
@@ -21,6 +24,7 @@ jobs:
with:
target: x86_64-pc-windows-gnu
- uses: taiki-e/install-action@cross
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
@@ -30,7 +34,7 @@ jobs:
run: cargo check --target x86_64-pc-windows-gnu
check-op-reth:
runs-on: depot-ubuntu-latest-16
runs-on: depot-ubuntu-latest
timeout-minutes: 60
steps:
@@ -40,6 +44,7 @@ jobs:
with:
target: x86_64-pc-windows-gnu
- uses: taiki-e/install-action@cross
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

3
.gitignore vendored
View File

@@ -12,6 +12,9 @@ target/
# Generated by Intellij-based IDEs.
.idea
# ck-search metadata
.ck
# Generated by MacOS
.DS_Store

986
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -376,11 +376,11 @@ reth-era-utils = { path = "crates/era-utils" }
reth-errors = { path = "crates/errors" }
reth-eth-wire = { path = "crates/net/eth-wire" }
reth-eth-wire-types = { path = "crates/net/eth-wire-types" }
reth-ethereum-payload-builder = { path = "crates/ethereum/payload" }
reth-ethereum-cli = { path = "crates/ethereum/cli", default-features = false }
reth-ethereum-consensus = { path = "crates/ethereum/consensus", default-features = false }
reth-ethereum-engine-primitives = { path = "crates/ethereum/engine-primitives", default-features = false }
reth-ethereum-forks = { path = "crates/ethereum/hardforks", default-features = false }
reth-ethereum-payload-builder = { path = "crates/ethereum/payload" }
reth-ethereum-primitives = { path = "crates/ethereum/primitives", default-features = false }
reth-ethereum = { path = "crates/ethereum/reth" }
reth-etl = { path = "crates/etl" }
@@ -475,64 +475,64 @@ reth-ress-provider = { path = "crates/ress/provider" }
# revm
revm = { version = "33.1.0", default-features = false }
revm-bytecode = { version = "7.1.1", default-features = false }
revm-database = { version = "9.0.6", default-features = false }
revm-database = { version = "9.0.5", default-features = false }
revm-state = { version = "8.1.1", default-features = false }
revm-primitives = { version = "21.0.2", default-features = false }
revm-interpreter = { version = "31.1.0", default-features = false }
revm-database-interface = { version = "8.0.5", default-features = false }
op-revm = { version = "14.1.0", default-features = false }
revm-inspectors = "0.33.1"
revm-inspectors = "0.33.2"
# eth
alloy-primitives = { version = "1.4.1", default-features = false, features = ["map-foldhash"] }
alloy-chains = { version = "0.2.5", default-features = false }
alloy-evm = { version = "0.24.2", default-features = false }
alloy-dyn-abi = "1.4.1"
alloy-eip2124 = { version = "0.2.0", default-features = false }
alloy-eip7928 = { version = "0.1.0" }
alloy-evm = { version = "0.25.1", default-features = false }
alloy-primitives = { version = "1.5.0", default-features = false, features = ["map-foldhash"] }
alloy-rlp = { version = "0.3.10", default-features = false, features = ["core-net"] }
alloy-sol-macro = "1.4.1"
alloy-sol-types = { version = "1.4.1", default-features = false }
alloy-sol-macro = "1.5.0"
alloy-sol-types = { version = "1.5.0", default-features = false }
alloy-trie = { version = "0.9.1", default-features = false }
alloy-hardforks = "0.4.5"
alloy-consensus = { version = "1.1.3", default-features = false }
alloy-contract = { version = "1.1.3", default-features = false }
alloy-eips = { version = "1.1.3", default-features = false }
alloy-genesis = { version = "1.1.3", default-features = false }
alloy-json-rpc = { version = "1.1.3", default-features = false }
alloy-network = { version = "1.1.3", default-features = false }
alloy-network-primitives = { version = "1.1.3", default-features = false }
alloy-provider = { version = "1.1.3", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.1.3", default-features = false }
alloy-rpc-client = { version = "1.1.3", default-features = false }
alloy-rpc-types = { version = "1.1.3", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.1.3", default-features = false }
alloy-rpc-types-anvil = { version = "1.1.3", default-features = false }
alloy-rpc-types-beacon = { version = "1.1.3", default-features = false }
alloy-rpc-types-debug = { version = "1.1.3", default-features = false }
alloy-rpc-types-engine = { version = "1.1.3", default-features = false }
alloy-rpc-types-eth = { version = "1.1.3", default-features = false }
alloy-rpc-types-mev = { version = "1.1.3", default-features = false }
alloy-rpc-types-trace = { version = "1.1.3", default-features = false }
alloy-rpc-types-txpool = { version = "1.1.3", default-features = false }
alloy-serde = { version = "1.1.3", default-features = false }
alloy-signer = { version = "1.1.3", default-features = false }
alloy-signer-local = { version = "1.1.3", default-features = false }
alloy-transport = { version = "1.1.3" }
alloy-transport-http = { version = "1.1.3", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.1.3", default-features = false }
alloy-transport-ws = { version = "1.1.3", default-features = false }
alloy-consensus = { version = "1.2.1", default-features = false }
alloy-contract = { version = "1.2.1", default-features = false }
alloy-eips = { version = "1.2.1", default-features = false }
alloy-genesis = { version = "1.2.1", default-features = false }
alloy-json-rpc = { version = "1.2.1", default-features = false }
alloy-network = { version = "1.2.1", default-features = false }
alloy-network-primitives = { version = "1.2.1", default-features = false }
alloy-provider = { version = "1.2.1", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.2.1", default-features = false }
alloy-rpc-client = { version = "1.2.1", default-features = false }
alloy-rpc-types = { version = "1.2.1", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.2.1", default-features = false }
alloy-rpc-types-anvil = { version = "1.2.1", default-features = false }
alloy-rpc-types-beacon = { version = "1.2.1", default-features = false }
alloy-rpc-types-debug = { version = "1.2.1", default-features = false }
alloy-rpc-types-engine = { version = "1.2.1", default-features = false }
alloy-rpc-types-eth = { version = "1.2.1", default-features = false }
alloy-rpc-types-mev = { version = "1.2.1", default-features = false }
alloy-rpc-types-trace = { version = "1.2.1", default-features = false }
alloy-rpc-types-txpool = { version = "1.2.1", default-features = false }
alloy-serde = { version = "1.2.1", default-features = false }
alloy-signer = { version = "1.2.1", default-features = false }
alloy-signer-local = { version = "1.2.1", default-features = false }
alloy-transport = { version = "1.2.1" }
alloy-transport-http = { version = "1.2.1", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.2.1", default-features = false }
alloy-transport-ws = { version = "1.2.1", default-features = false }
# op
alloy-op-evm = { version = "0.24.2", default-features = false }
alloy-op-hardforks = "0.4.3"
op-alloy-rpc-types = { version = "0.22.0", default-features = false }
op-alloy-rpc-types-engine = { version = "0.22.0", default-features = false }
op-alloy-network = { version = "0.22.0", default-features = false }
op-alloy-consensus = { version = "0.22.0", default-features = false }
op-alloy-rpc-jsonrpsee = { version = "0.22.0", default-features = false }
alloy-op-evm = { version = "0.25.0", default-features = false }
alloy-op-hardforks = "0.4.4"
op-alloy-rpc-types = { version = "0.23.1", default-features = false }
op-alloy-rpc-types-engine = { version = "0.23.1", default-features = false }
op-alloy-network = { version = "0.23.1", default-features = false }
op-alloy-consensus = { version = "0.23.1", default-features = false }
op-alloy-rpc-jsonrpsee = { version = "0.23.1", default-features = false }
op-alloy-flz = { version = "0.13.1", default-features = false }
# misc
@@ -548,6 +548,7 @@ bytes = { version = "1.5", default-features = false }
brotli = "8"
cfg-if = "1.0"
clap = "4"
color-eyre = "0.6"
dashmap = "6.0"
derive_more = { version = "2", default-features = false, features = ["full"] }
dirs-next = "2.0.0"
@@ -587,6 +588,7 @@ url = { version = "2.3", default-features = false }
zstd = "0.13"
byteorder = "1"
mini-moka = "0.10"
moka = "0.12"
tar-no-std = { version = "0.3.2", default-features = false }
miniz_oxide = { version = "0.8.4", default-features = false }
chrono = "0.4.41"
@@ -692,7 +694,7 @@ ahash = "0.8"
anyhow = "1.0"
bindgen = { version = "0.71", default-features = false }
block-padding = "0.3.2"
cc = "=1.2.15"
cc = "1.2.15"
cipher = "0.4.3"
comfy-table = "7.0"
concat-kdf = "0.1.0"
@@ -729,6 +731,7 @@ socket2 = { version = "0.5", default-features = false }
sysinfo = { version = "0.33", default-features = false }
tracing-journald = "0.3"
tracing-logfmt = "0.3.3"
tracing-samply = "0.1"
tracing-subscriber = { version = "0.3", default-features = false }
triehash = "0.8"
typenum = "1.15.0"
@@ -736,40 +739,39 @@ vergen = "9.0.4"
visibility = "0.1.1"
walkdir = "2.3.3"
vergen-git2 = "1.0.5"
# networking
ipnet = "2.11"
[patch.crates-io]
alloy-consensus = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-contract = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-eips = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-genesis = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-json-rpc = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-network = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-network-primitives = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-provider = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-pubsub = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-client = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-admin = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-anvil = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-beacon = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-debug = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-engine = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-eth = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-mev = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-trace = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-rpc-types-txpool = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-serde = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-signer = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-signer-local = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-transport = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-transport-http = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-transport-ipc = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
alloy-transport-ws = { git = "https://github.com/Soubhik-10/alloy", branch = "bal" }
# alloy-hardforks = { git = "https://github.com/Rimeeeeee/hardforks", branch = "amsterdam" }
# [patch.crates-io]
# alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-network-primitives = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-pubsub = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-admin = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-anvil = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-beacon = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-debug = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-engine = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-eth = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-mev = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-trace = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-rpc-types-txpool = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-serde = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-signer = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-signer-local = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "3049f232fbb44d1909883e154eb38ec5962f53a3" }
# alloy-op-hardforks = { git = "https://github.com/Rimeeeeee/hardforks", branch = "amsterdam" }
# op-alloy-consensus = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
# op-alloy-network = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
# op-alloy-rpc-types = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
@@ -777,20 +779,6 @@ alloy-transport-ws = { git = "https://github.com/Soubhik-10/alloy", branch = "ba
# op-alloy-rpc-jsonrpsee = { git = "https://github.com/alloy-rs/op-alloy", rev = "a79d6fc" }
#
# revm-inspectors = { git = "https://github.com/paradigmxyz/revm-inspectors", rev = "1207e33" }
# need to change to bluealloy rakita/bal
revm = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
alloy-evm = { git = "https://github.com/Rimeeeeee/evm", branch = "new-approach4" }
alloy-op-evm = { git = "https://github.com/Rimeeeeee/evm", branch = "new-approach4" }
revm-bytecode = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-database = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-state = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-primitives = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-interpreter = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-inspector = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-context = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-context-interface = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
revm-database-interface = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
op-revm = { git = "https://github.com/Rimeeeeee/revm", branch = "rakita/bal" }
#
# jsonrpsee = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }
# jsonrpsee-core = { git = "https://github.com/paradigmxyz/jsonrpsee", branch = "matt/make-rpc-service-pub" }

View File

@@ -18,7 +18,7 @@ FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
# Build profile, release by default
ARG BUILD_PROFILE=release
ARG BUILD_PROFILE=maxperf
ENV BUILD_PROFILE=$BUILD_PROFILE
# Extra Cargo flags

View File

@@ -14,7 +14,7 @@ RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
ARG BUILD_PROFILE=release
ARG BUILD_PROFILE=maxperf
ENV BUILD_PROFILE=$BUILD_PROFILE
ARG RUSTFLAGS=""

View File

@@ -186,7 +186,7 @@ APPENDIX: How to apply the Apache License to your work.
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022-2025 Reth Contributors
Copyright 2022-2026 Reth Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2022-2025 Reth Contributors
Copyright (c) 2022-2026 Reth Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -521,5 +521,3 @@ pr:
make update-book-cli && \
cargo docs --document-private-items && \
make test
check-features:

View File

@@ -164,12 +164,42 @@ pub(crate) struct Args {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub reth_args: Vec<String>,
/// Comma-separated list of features to enable during reth compilation
/// 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)
///
/// Example: `--baseline-features jemalloc`
#[arg(long, value_name = "FEATURES")]
pub baseline_features: Option<String>,
/// Comma-separated list of features to enable only for feature build (overrides --features)
///
/// Example: `--feature-features jemalloc,asm-keccak`
#[arg(long, value_name = "FEATURES")]
pub feature_features: Option<String>,
/// RUSTFLAGS to use for both baseline and feature builds
///
/// Example: `--rustflags "-C target-cpu=native"`
#[arg(long, value_name = "FLAGS", default_value = "-C target-cpu=native")]
pub rustflags: String,
/// RUSTFLAGS to use only for baseline build (overrides --rustflags)
///
/// Example: `--baseline-rustflags "-C target-cpu=native -C lto"`
#[arg(long, value_name = "FLAGS")]
pub baseline_rustflags: Option<String>,
/// RUSTFLAGS to use only for feature build (overrides --rustflags)
///
/// Example: `--feature-rustflags "-C target-cpu=native -C lto"`
#[arg(long, value_name = "FLAGS")]
pub feature_rustflags: Option<String>,
/// Disable automatic --debug.startup-sync-state-idle flag for specific runs.
/// Can be "baseline", "feature", or "all".
/// By default, the flag is passed to warmup, baseline, and feature runs.
@@ -328,7 +358,6 @@ pub(crate) async fn run_comparison(args: Args, _ctx: CliContext) -> Result<()> {
git_manager.repo_root().to_string(),
output_dir.clone(),
git_manager.clone(),
args.features.clone(),
)?;
// Initialize node manager
let mut node_manager = NodeManager::new(&args);
@@ -448,6 +477,18 @@ async fn run_compilation_phase(
let ref_type = ref_types[i];
let commit = &ref_commits[git_ref];
// Get per-build features and rustflags
let features = match ref_type {
"baseline" => args.baseline_features.as_ref().unwrap_or(&args.features),
"feature" => args.feature_features.as_ref().unwrap_or(&args.features),
_ => &args.features,
};
let rustflags = match ref_type {
"baseline" => args.baseline_rustflags.as_ref().unwrap_or(&args.rustflags),
"feature" => args.feature_rustflags.as_ref().unwrap_or(&args.rustflags),
_ => &args.rustflags,
};
info!(
"Compiling {} binary for reference: {} (commit: {})",
ref_type,
@@ -459,7 +500,7 @@ async fn run_compilation_phase(
git_manager.switch_ref(git_ref)?;
// Compile reth (with caching)
compilation_manager.compile_reth(commit, is_optimism)?;
compilation_manager.compile_reth(commit, is_optimism, features, rustflags)?;
info!("Completed compilation for {} reference", ref_type);
}

View File

@@ -39,7 +39,8 @@ pub(crate) struct BenchmarkResults {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct CombinedLatencyRow {
pub block_number: u64,
pub transaction_count: u64,
#[serde(default)]
pub transaction_count: Option<u64>,
pub gas_used: u64,
pub new_payload_latency: u128,
}
@@ -48,7 +49,8 @@ pub(crate) struct CombinedLatencyRow {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct TotalGasRow {
pub block_number: u64,
pub transaction_count: u64,
#[serde(default)]
pub transaction_count: Option<u64>,
pub gas_used: u64,
pub time: u128,
}
@@ -125,7 +127,8 @@ pub(crate) struct ComparisonSummary {
#[derive(Debug, Serialize)]
pub(crate) struct BlockComparison {
pub block_number: u64,
pub transaction_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub transaction_count: Option<u64>,
pub gas_used: u64,
pub baseline_new_payload_latency: u128,
pub feature_new_payload_latency: u128,

View File

@@ -13,7 +13,6 @@ pub(crate) struct CompilationManager {
repo_root: String,
output_dir: PathBuf,
git_manager: GitManager,
features: String,
}
impl CompilationManager {
@@ -22,9 +21,8 @@ impl CompilationManager {
repo_root: String,
output_dir: PathBuf,
git_manager: GitManager,
features: String,
) -> Result<Self> {
Ok(Self { repo_root, output_dir, git_manager, features })
Ok(Self { repo_root, output_dir, git_manager })
}
/// Detect if the RPC endpoint is an Optimism chain
@@ -68,7 +66,13 @@ impl CompilationManager {
}
/// Compile reth using cargo build and cache the binary
pub(crate) fn compile_reth(&self, commit: &str, is_optimism: bool) -> Result<()> {
pub(crate) fn compile_reth(
&self,
commit: &str,
is_optimism: bool,
features: &str,
rustflags: &str,
) -> Result<()> {
// Validate that current git commit matches the expected commit
let current_commit = self.git_manager.get_current_commit()?;
if current_commit != commit {
@@ -100,9 +104,8 @@ impl CompilationManager {
let mut cmd = Command::new("cargo");
cmd.arg("build").arg("--profile").arg("profiling");
// Add features
cmd.arg("--features").arg(&self.features);
info!("Using features: {}", self.features);
cmd.arg("--features").arg(features);
info!("Using features: {features}");
// Add bin-specific arguments for optimism
if is_optimism {
@@ -114,8 +117,9 @@ impl CompilationManager {
cmd.current_dir(&self.repo_root);
// Set RUSTFLAGS for native CPU optimization
cmd.env("RUSTFLAGS", "-C target-cpu=native");
// Set RUSTFLAGS
cmd.env("RUSTFLAGS", rustflags);
info!("Using RUSTFLAGS: {rustflags}");
// Debug log the command
debug!("Executing cargo command: {:?}", cmd);

View File

@@ -211,6 +211,11 @@ impl NodeManager {
cmd.arg("--");
cmd.args(reth_args);
// Enable tracing-samply
if supports_samply_flags(&reth_args[0]) {
cmd.arg("--log.samply");
}
// Set environment variable to disable log styling
cmd.env("RUST_LOG_STYLE", "never");
@@ -403,7 +408,7 @@ impl NodeManager {
/// Stop the reth node gracefully
pub(crate) async fn stop_node(&self, child: &mut tokio::process::Child) -> Result<()> {
let pid = child.id().expect("Child process ID should be available");
let pid = child.id().ok_or_eyre("Child process ID should be available")?;
// Check if the process has already exited
match child.try_wait() {
@@ -552,3 +557,16 @@ impl NodeManager {
Ok(())
}
}
fn supports_samply_flags(bin: &str) -> bool {
let mut cmd = std::process::Command::new(bin);
// NOTE: The flag to check must come before --help.
// We pass --help as a shortcut to not execute any command.
cmd.args(["--log.samply", "--help"]);
debug!(?cmd, "Checking samply flags support");
let Ok(output) = cmd.output() else {
return false;
};
debug!(?output, "Samply flags support check");
output.status.success()
}

View File

@@ -58,6 +58,7 @@ tokio = { workspace = true, features = ["sync", "macros", "time", "rt-multi-thre
# misc
clap = { workspace = true, features = ["derive", "env"] }
eyre.workspace = true
color-eyre.workspace = true
thiserror.workspace = true
humantime.workspace = true

View File

@@ -103,14 +103,20 @@ impl BenchContext {
(bench_args.from, bench_args.to)
};
// If neither `--from` nor `--to` are provided, we will run the benchmark continuously,
// If `--to` are not provided, we will run the benchmark continuously,
// starting at the latest block.
let mut benchmark_mode = BenchMode::new(from, to)?;
let latest_block = block_provider
.get_block_by_number(BlockNumberOrTag::Latest)
.full()
.await?
.ok_or_else(|| eyre::eyre!("Failed to fetch latest block from RPC"))?;
let mut benchmark_mode = BenchMode::new(from, to, latest_block.into_inner().number())?;
let first_block = match benchmark_mode {
BenchMode::Continuous => {
// fetch Latest block
block_provider.get_block_by_number(BlockNumberOrTag::Latest).full().await?.unwrap()
BenchMode::Continuous(start) => {
block_provider.get_block_by_number(start.into()).full().await?.ok_or_else(|| {
eyre::eyre!("Failed to fetch block {} from RPC for continuous mode", start)
})?
}
BenchMode::Range(ref mut range) => {
match range.next() {
@@ -120,7 +126,9 @@ impl BenchContext {
.get_block_by_number(block_number.into())
.full()
.await?
.unwrap()
.ok_or_else(|| {
eyre::eyre!("Failed to fetch block {} from RPC", block_number)
})?
}
None => {
return Err(eyre::eyre!(

View File

@@ -5,9 +5,8 @@ use std::ops::RangeInclusive;
/// Whether or not the benchmark should run as a continuous stream of payloads.
#[derive(Debug, PartialEq, Eq)]
pub enum BenchMode {
// TODO: just include the start block in `Continuous`
/// Run the benchmark as a continuous stream of payloads, until the benchmark is interrupted.
Continuous,
Continuous(u64),
/// Run the benchmark for a specific range of blocks.
Range(RangeInclusive<u64>),
}
@@ -16,18 +15,19 @@ impl BenchMode {
/// Check if the block number is in the range
pub fn contains(&self, block_number: u64) -> bool {
match self {
Self::Continuous => true,
Self::Continuous(start) => block_number >= *start,
Self::Range(range) => range.contains(&block_number),
}
}
/// Create a [`BenchMode`] from optional `from` and `to` fields.
pub fn new(from: Option<u64>, to: Option<u64>) -> Result<Self, eyre::Error> {
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,
// starting at the latest block.
match (from, to) {
(Some(from), Some(to)) => Ok(Self::Range(from..=to)),
(None, None) => Ok(Self::Continuous),
(None, None) => Ok(Self::Continuous(latest_block)),
(Some(start), None) => Ok(Self::Continuous(start)),
_ => {
// both or neither are allowed, everything else is ambiguous
Err(eyre::eyre!("`from` and `to` must be provided together, or not at all."))

View File

@@ -23,7 +23,7 @@ use bench::BenchmarkCommand;
use clap::Parser;
use reth_cli_runner::CliRunner;
fn main() {
fn main() -> eyre::Result<()> {
// Enable backtraces unless a RUST_BACKTRACE value has already been explicitly provided.
if std::env::var_os("RUST_BACKTRACE").is_none() {
unsafe {
@@ -31,12 +31,11 @@ fn main() {
}
}
color_eyre::install()?;
// Run until either exit or sigint or sigterm
let runner = CliRunner::try_default_runtime().unwrap();
runner
.run_command_until_exit(|ctx| {
let command = BenchmarkCommand::parse();
command.execute(ctx)
})
.unwrap();
let runner = CliRunner::try_default_runtime()?;
runner.run_command_until_exit(|ctx| BenchmarkCommand::parse().execute(ctx))?;
Ok(())
}

View File

@@ -150,45 +150,6 @@ pub(crate) fn block_to_new_payload(
let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
let (version, params) = match payload {
ExecutionPayload::V4(payload) => {
let cancun = sidecar.cancun().unwrap();
if let Some(prague) = sidecar.prague() {
if is_optimism {
(
EngineApiMessageVersion::V4,
serde_json::to_value((
OpExecutionPayloadV4 {
payload_inner: payload.payload_inner,
withdrawals_root: block.withdrawals_root.unwrap(),
},
cancun.versioned_hashes.clone(),
cancun.parent_beacon_block_root,
Requests::default(),
))?,
)
} else {
(
EngineApiMessageVersion::V4,
serde_json::to_value((
payload,
cancun.versioned_hashes.clone(),
cancun.parent_beacon_block_root,
prague.requests.requests_hash(),
))?,
)
}
} else {
(
EngineApiMessageVersion::V3,
serde_json::to_value((
payload,
cancun.versioned_hashes.clone(),
cancun.parent_beacon_block_root,
))?,
)
}
}
ExecutionPayload::V3(payload) => {
let cancun = sidecar.cancun().unwrap();
@@ -283,10 +244,7 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
payload_attributes: Option<PayloadAttributes>,
) -> TransportResult<ForkchoiceUpdated> {
match message_version {
EngineApiMessageVersion::V3 |
EngineApiMessageVersion::V4 |
EngineApiMessageVersion::V5 |
EngineApiMessageVersion::V6 => {
EngineApiMessageVersion::V3 | EngineApiMessageVersion::V4 | EngineApiMessageVersion::V5 => {
provider.fork_choice_updated_v3_wait(forkchoice_state, payload_attributes).await
}
EngineApiMessageVersion::V2 => {

View File

@@ -81,7 +81,7 @@ backon.workspace = true
tempfile.workspace = true
[features]
default = ["jemalloc", "otlp", "reth-revm/portable", "js-tracer"]
default = ["jemalloc", "otlp", "reth-revm/portable", "js-tracer", "keccak-cache-global", "asm-keccak"]
otlp = [
"reth-ethereum-cli/otlp",
@@ -102,7 +102,10 @@ asm-keccak = [
"reth-ethereum-cli/asm-keccak",
"reth-node-ethereum/asm-keccak",
]
keccak-cache-global = [
"reth-node-core/keccak-cache-global",
"reth-node-ethereum/keccak-cache-global",
]
jemalloc = [
"reth-cli-util/jemalloc",
"reth-node-core/jemalloc",

View File

@@ -38,11 +38,26 @@ pub struct ComputedTrieData {
/// Trie input bundled with its anchor hash.
///
/// This is used to store the trie input and anchor hash for a block together.
/// The `trie_input` contains the **cumulative** overlay of all in-memory ancestor blocks
/// since the anchor, not just this block's changes. This enables O(1) overlay reuse
/// when building child blocks with the same anchor.
///
/// # Invariants
///
/// For correctness of overlay reuse optimizations:
/// - The `ancestors` passed to [`DeferredTrieData::pending`] must form a true ancestor chain (each
/// entry's parent is the previous entry, oldest to newest order)
/// - When `anchor_hash` matches the parent's `anchor_hash`, the parent's `trie_input` already
/// contains all ancestors in that chain, enabling O(1) reuse
/// - A given `anchor_hash` uniquely identifies a persisted base state
#[derive(Clone, Debug)]
pub struct AnchoredTrieInput {
/// The persisted ancestor hash this trie input is anchored to.
pub anchor_hash: B256,
/// Trie input constructed from in-memory overlays.
/// Cumulative trie input overlay from all in-memory ancestors since the anchor.
/// Note: This is the merged overlay, not just this block's changes.
/// The per-block changes are in [`ComputedTrieData::hashed_state`] and
/// [`ComputedTrieData::trie_updates`].
pub trie_input: Arc<TrieInputSorted>,
}
@@ -54,6 +69,12 @@ struct DeferredTrieMetrics {
deferred_trie_async_ready: Counter,
/// Number of times deferred trie data required synchronous computation (fallback path).
deferred_trie_sync_fallback: Counter,
/// Number of times the parent's trie overlay was reused (O(1) fast path).
deferred_trie_overlay_reused: Counter,
/// Number of times the trie overlay was rebuilt from all ancestors (O(N) slow path).
deferred_trie_overlay_rebuilt: Counter,
/// Number of times `Arc::make_mut` triggered a clone (`strong_count` > 1).
deferred_trie_arc_clone_triggered: Counter,
}
static DEFERRED_TRIE_METRICS: LazyLock<DeferredTrieMetrics> =
@@ -138,8 +159,15 @@ impl DeferredTrieData {
///
/// # Process
/// 1. Sort the current block's hashed state and trie updates
/// 2. Merge ancestor overlays (oldest -> newest, so later state takes precedence)
/// 3. Extend the merged overlay with this block's sorted data
/// 2. Try to reuse parent's overlay if anchor matches (O(1) fast path)
/// 3. Otherwise, merge all ancestor overlays (O(N) slow path, rare after persist/reorg)
/// 4. Extend the overlay with this block's sorted data
///
/// # Complexity
/// - Normal case (same anchor as parent): O(1) - just clone parent's overlay
/// - After persist/reorg (anchor mismatch): O(N) - merge all ancestors once
///
/// This eliminates the previous O(N²) complexity where each block re-merged all ancestors.
///
/// Used by both the async background task and the synchronous fallback path.
///
@@ -147,7 +175,7 @@ impl DeferredTrieData {
/// * `hashed_state` - Unsorted hashed post-state (account/storage changes) from execution
/// * `trie_updates` - Unsorted trie node updates from state root computation
/// * `anchor_hash` - The persisted ancestor hash this trie input is anchored to
/// * `ancestors` - Deferred trie data from ancestor blocks for merging
/// * `ancestors` - Deferred trie data from ancestor blocks for merging (oldest -> newest)
pub fn sort_and_build_trie_input(
hashed_state: &HashedPostState,
trie_updates: &TrieUpdates,
@@ -156,28 +184,63 @@ impl DeferredTrieData {
) -> ComputedTrieData {
// Sort the current block's hashed state and trie updates
let sorted_hashed_state = Arc::new(hashed_state.clone_into_sorted());
let sorted_trie_updates = Arc::new(trie_updates.clone().into_sorted());
let sorted_trie_updates = Arc::new(trie_updates.clone_into_sorted());
// Merge trie data from ancestors (oldest -> newest so later state takes precedence)
let mut overlay = TrieInputSorted::default();
for ancestor in ancestors {
let ancestor_data = ancestor.wait_cloned();
{
let state_mut = Arc::make_mut(&mut overlay.state);
state_mut.extend_ref(ancestor_data.hashed_state.as_ref());
}
{
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
nodes_mut.extend_ref(ancestor_data.trie_updates.as_ref());
}
}
// Determine base overlay by checking if we can reuse parent's overlay
let mut overlay = if let Some(parent) = ancestors.last() {
let parent_data = parent.wait_cloned();
// Extend overlay with current block's sorted data
match &parent_data.anchored_trie_input {
// Fast path: reuse parent's already-merged overlay if anchors match.
// Parent's overlay already contains all ancestors merged, so we just clone
// the Arc-wrapped nodes and state (O(1)).
//
// IMPORTANT: We do NOT clone prefix_sets from the parent overlay.
// Prefix sets only need to represent the current block's changes, not
// cumulative ancestor changes. The incremental state root algorithms
// use prefix_sets to identify which trie branches changed since the
// last root computation - ancestors' changes are already embodied in
// the trie nodes. This matches the pattern in merkle_changesets.rs
// which explicitly uses per-block prefix sets.
Some(AnchoredTrieInput { anchor_hash: parent_anchor, trie_input })
if *parent_anchor == anchor_hash =>
{
DEFERRED_TRIE_METRICS.deferred_trie_overlay_reused.increment(1);
TrieInputSorted::new(
Arc::clone(&trie_input.nodes),
Arc::clone(&trie_input.state),
Default::default(), // Fresh prefix_sets - will be set by caller
)
}
// Slow path: no matching parent overlay -> rebuild from all ancestors.
// This happens after persist (anchor changes) or if parent lacks anchored input.
// O(N) but only at persist/reorg boundaries, not per block.
_ => {
DEFERRED_TRIE_METRICS.deferred_trie_overlay_rebuilt.increment(1);
Self::merge_ancestors_into_overlay(ancestors)
}
}
} else {
// No ancestors: start from empty overlay (first block after anchor)
TrieInputSorted::default()
};
// Extend overlay with current block's sorted data.
// Track if Arc::make_mut triggers a clone (strong_count > 1 means parent still holds ref).
{
let will_clone = Arc::strong_count(&overlay.state) > 1;
if will_clone {
DEFERRED_TRIE_METRICS.deferred_trie_arc_clone_triggered.increment(1);
}
let state_mut = Arc::make_mut(&mut overlay.state);
state_mut.extend_ref(sorted_hashed_state.as_ref());
}
{
let will_clone = Arc::strong_count(&overlay.nodes) > 1;
if will_clone {
DEFERRED_TRIE_METRICS.deferred_trie_arc_clone_triggered.increment(1);
}
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
nodes_mut.extend_ref(sorted_trie_updates.as_ref());
}
@@ -190,6 +253,40 @@ impl DeferredTrieData {
)
}
/// Merge all ancestors into a single overlay.
///
/// This is the slow path used when the parent's overlay cannot be reused
/// (e.g., after persist when anchor changes). Iterates ancestors oldest -> newest
/// so newer state takes precedence.
///
/// Note: We intentionally do NOT reuse ancestor cached overlays here because
/// those overlays were built with a different anchor_hash. The slow path is
/// triggered precisely because the anchor changed, so we must rebuild from
/// each ancestor's per-block state changes.
fn merge_ancestors_into_overlay(ancestors: &[Self]) -> TrieInputSorted {
let mut overlay = TrieInputSorted::default();
for ancestor in ancestors {
let ancestor_data = ancestor.wait_cloned();
{
let will_clone = Arc::strong_count(&overlay.state) > 1;
if will_clone {
DEFERRED_TRIE_METRICS.deferred_trie_arc_clone_triggered.increment(1);
}
let state_mut = Arc::make_mut(&mut overlay.state);
state_mut.extend_ref(ancestor_data.hashed_state.as_ref());
}
{
let will_clone = Arc::strong_count(&overlay.nodes) > 1;
if will_clone {
DEFERRED_TRIE_METRICS.deferred_trie_arc_clone_triggered.increment(1);
}
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
nodes_mut.extend_ref(ancestor_data.trie_updates.as_ref());
}
}
overlay
}
/// Returns trie data, computing synchronously if the async task hasn't completed.
///
/// - If the async task has completed (`Ready`), returns the cached result.
@@ -441,4 +538,274 @@ mod tests {
let (_, account) = &overlay_state[0];
assert_eq!(account.unwrap().nonce, 2);
}
/// Helper to create a ready block with anchored trie input containing specific state.
fn ready_block_with_state(
anchor_hash: B256,
accounts: Vec<(B256, Option<Account>)>,
) -> DeferredTrieData {
let hashed_state = Arc::new(HashedPostStateSorted::new(accounts, B256Map::default()));
let trie_updates = Arc::default();
let mut overlay = TrieInputSorted::default();
Arc::make_mut(&mut overlay.state).extend_ref(hashed_state.as_ref());
DeferredTrieData::ready(ComputedTrieData {
hashed_state,
trie_updates,
anchored_trie_input: Some(AnchoredTrieInput {
anchor_hash,
trie_input: Arc::new(overlay),
}),
})
}
/// Verifies that first block after anchor (no ancestors) creates empty base overlay.
#[test]
fn first_block_after_anchor_creates_empty_base() {
let anchor = B256::with_last_byte(1);
let key = B256::with_last_byte(42);
let account = Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None };
// First block after anchor - no ancestors
let first_block = DeferredTrieData::pending(
Arc::new(HashedPostState::default().with_accounts([(key, Some(account))])),
Arc::new(TrieUpdates::default()),
anchor,
vec![], // No ancestors
);
let result = first_block.wait_cloned();
// Should have overlay with just this block's data
let overlay = result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.anchor_hash, anchor);
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
let (found_key, found_account) = &overlay.trie_input.state.accounts[0];
assert_eq!(*found_key, key);
assert_eq!(found_account.unwrap().nonce, 1);
}
/// Verifies that when parent has matching anchor, its overlay is reused (O(1) fast path).
#[test]
fn reuses_parent_overlay_when_anchor_matches() {
let anchor = B256::with_last_byte(1);
let key = B256::with_last_byte(42);
let account = Account { nonce: 100, balance: U256::ZERO, bytecode_hash: None };
// Create parent with anchored trie input
let parent = ready_block_with_state(anchor, vec![(key, Some(account))]);
// Create child with same anchor - should reuse parent's overlay
let child = DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
anchor, // Same anchor as parent
vec![parent],
);
let result = child.wait_cloned();
// Verify parent's account is in the overlay
let overlay = result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.anchor_hash, anchor);
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
let (found_key, found_account) = &overlay.trie_input.state.accounts[0];
assert_eq!(*found_key, key);
assert_eq!(found_account.unwrap().nonce, 100);
}
/// Verifies that when anchor changes (after persist), all ancestors are rebuilt.
#[test]
fn rebuilds_overlay_when_anchor_changes() {
let old_anchor = B256::with_last_byte(1);
let new_anchor = B256::with_last_byte(2);
let key = B256::with_last_byte(42);
let account = Account { nonce: 50, balance: U256::ZERO, bytecode_hash: None };
// Create parent with OLD anchor
let parent = ready_block_with_state(old_anchor, vec![(key, Some(account))]);
// Create child with NEW anchor (simulates after persist)
let child = DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
new_anchor, // Different anchor - triggers rebuild
vec![parent],
);
let result = child.wait_cloned();
// Verify result uses new anchor and still has parent's data
let overlay = result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.anchor_hash, new_anchor);
// Parent's account should still be in the overlay (from rebuild)
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
let (found_key, found_account) = &overlay.trie_input.state.accounts[0];
assert_eq!(*found_key, key);
assert_eq!(found_account.unwrap().nonce, 50);
}
/// Verifies that parent without `anchored_trie_input` triggers rebuild path.
#[test]
fn rebuilds_when_parent_has_no_anchored_input() {
let anchor = B256::with_last_byte(1);
let key = B256::with_last_byte(42);
let account = Account { nonce: 25, balance: U256::ZERO, bytecode_hash: None };
// Create parent WITHOUT anchored trie input (e.g., from without_trie_input constructor)
let parent_state =
HashedPostStateSorted::new(vec![(key, Some(account))], B256Map::default());
let parent = DeferredTrieData::ready(ComputedTrieData {
hashed_state: Arc::new(parent_state),
trie_updates: Arc::default(),
anchored_trie_input: None, // No anchored input
});
// Create child - should rebuild from parent's hashed_state
let child = DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
anchor,
vec![parent],
);
let result = child.wait_cloned();
// Verify overlay is built and contains parent's data
let overlay = result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.anchor_hash, anchor);
assert_eq!(overlay.trie_input.state.accounts.len(), 1);
}
/// Verifies that a chain of blocks with matching anchors builds correct cumulative overlay.
#[test]
fn chain_of_blocks_builds_cumulative_overlay() {
let anchor = B256::with_last_byte(1);
let key1 = B256::with_last_byte(1);
let key2 = B256::with_last_byte(2);
let key3 = B256::with_last_byte(3);
// Block 1: sets account at key1
let block1 = ready_block_with_state(
anchor,
vec![(key1, Some(Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None }))],
);
// Block 2: adds account at key2, ancestor is block1
let block2_hashed = HashedPostState::default().with_accounts([(
key2,
Some(Account { nonce: 2, balance: U256::ZERO, bytecode_hash: None }),
)]);
let block2 = DeferredTrieData::pending(
Arc::new(block2_hashed),
Arc::new(TrieUpdates::default()),
anchor,
vec![block1.clone()],
);
// Compute block2's trie data
let block2_computed = block2.wait_cloned();
let block2_ready = DeferredTrieData::ready(block2_computed);
// Block 3: adds account at key3, ancestor is block2 (which includes block1)
let block3_hashed = HashedPostState::default().with_accounts([(
key3,
Some(Account { nonce: 3, balance: U256::ZERO, bytecode_hash: None }),
)]);
let block3 = DeferredTrieData::pending(
Arc::new(block3_hashed),
Arc::new(TrieUpdates::default()),
anchor,
vec![block1, block2_ready],
);
let result = block3.wait_cloned();
// Verify all three accounts are in the cumulative overlay
let overlay = result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.trie_input.state.accounts.len(), 3);
// Accounts should be sorted by key (B256 ordering)
let accounts = &overlay.trie_input.state.accounts;
assert!(accounts.iter().any(|(k, a)| *k == key1 && a.unwrap().nonce == 1));
assert!(accounts.iter().any(|(k, a)| *k == key2 && a.unwrap().nonce == 2));
assert!(accounts.iter().any(|(k, a)| *k == key3 && a.unwrap().nonce == 3));
}
/// Verifies that child block's state overwrites parent's state for the same key.
#[test]
fn child_state_overwrites_parent() {
let anchor = B256::with_last_byte(1);
let key = B256::with_last_byte(42);
// Parent sets nonce to 10
let parent = ready_block_with_state(
anchor,
vec![(key, Some(Account { nonce: 10, balance: U256::ZERO, bytecode_hash: None }))],
);
// Child overwrites nonce to 99
let child_hashed = HashedPostState::default().with_accounts([(
key,
Some(Account { nonce: 99, balance: U256::ZERO, bytecode_hash: None }),
)]);
let child = DeferredTrieData::pending(
Arc::new(child_hashed),
Arc::new(TrieUpdates::default()),
anchor,
vec![parent],
);
let result = child.wait_cloned();
// Verify child's value wins (extend_ref uses later value)
let overlay = result.anchored_trie_input.as_ref().unwrap();
// Note: extend_ref may result in duplicate keys; check the last occurrence
let accounts = &overlay.trie_input.state.accounts;
let last_account = accounts.iter().rfind(|(k, _)| *k == key).unwrap();
assert_eq!(last_account.1.unwrap().nonce, 99);
}
/// Stress test: verify O(N) behavior by building a chain of many blocks.
/// This test ensures the fix doesn't regress - previously this would be O(N²).
#[test]
fn long_chain_builds_in_linear_time() {
let anchor = B256::with_last_byte(1);
let num_blocks = 50; // Enough to notice O(N²) vs O(N) difference
let mut ancestors: Vec<DeferredTrieData> = Vec::new();
let start = Instant::now();
for i in 0..num_blocks {
let key = B256::with_last_byte(i as u8);
let account = Account { nonce: i as u64, balance: U256::ZERO, bytecode_hash: None };
let hashed = HashedPostState::default().with_accounts([(key, Some(account))]);
let block = DeferredTrieData::pending(
Arc::new(hashed),
Arc::new(TrieUpdates::default()),
anchor,
ancestors.clone(),
);
// Compute and add to ancestors for next iteration
let computed = block.wait_cloned();
ancestors.push(DeferredTrieData::ready(computed));
}
let elapsed = start.elapsed();
// With O(N) fix, 50 blocks should complete quickly (< 1 second)
// With O(N²), this would take significantly longer
assert!(
elapsed < Duration::from_secs(2),
"Chain of {num_blocks} blocks took {:?}, possible O(N²) regression",
elapsed
);
// Verify final overlay has all accounts
let final_result = ancestors.last().unwrap().wait_cloned();
let overlay = final_result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.trie_input.state.accounts.len(), num_blocks);
}
}

View File

@@ -86,14 +86,20 @@ impl<N: NodePrimitives> InMemoryState<N> {
///
/// This tries to acquire a read lock. Drop any write locks before calling this.
pub(crate) fn update_metrics(&self) {
let numbers = self.numbers.read();
if let Some((earliest_block_number, _)) = numbers.first_key_value() {
self.metrics.earliest_block.set(*earliest_block_number as f64);
let (count, earliest, latest) = {
let numbers = self.numbers.read();
let count = numbers.len();
let earliest = numbers.first_key_value().map(|(number, _)| *number);
let latest = numbers.last_key_value().map(|(number, _)| *number);
(count, earliest, latest)
};
if let Some(earliest_block_number) = earliest {
self.metrics.earliest_block.set(earliest_block_number as f64);
}
if let Some((latest_block_number, _)) = numbers.last_key_value() {
self.metrics.latest_block.set(*latest_block_number as f64);
if let Some(latest_block_number) = latest {
self.metrics.latest_block.set(latest_block_number as f64);
}
self.metrics.num_blocks.set(numbers.len() as f64);
self.metrics.num_blocks.set(count as f64);
}
/// Returns the state for a given block hash.
@@ -664,22 +670,14 @@ impl<N: NodePrimitives> BlockState<N> {
receipts.first().map(|receipts| receipts.deref()).unwrap_or_default()
}
/// Returns a vector of __parent__ `BlockStates`.
/// Returns an iterator over __parent__ `BlockStates`.
///
/// The block state order in the output vector is newest to oldest (highest to lowest):
/// The block state order is newest to oldest (highest to lowest):
/// `[5,4,3,2,1]`
///
/// Note: This does not include self.
pub fn parent_state_chain(&self) -> Vec<&Self> {
let mut parents = Vec::new();
let mut current = self.parent.as_deref();
while let Some(parent) = current {
parents.push(parent);
current = parent.parent.as_deref();
}
parents
pub fn parent_state_chain(&self) -> impl Iterator<Item = &Self> + '_ {
std::iter::successors(self.parent.as_deref(), |state| state.parent.as_deref())
}
/// Returns a vector of `BlockStates` representing the entire in memory chain.
@@ -690,6 +688,11 @@ impl<N: NodePrimitives> BlockState<N> {
}
/// Appends the parent chain of this [`BlockState`] to the given vector.
///
/// Parents are appended in order from newest to oldest (highest to lowest).
/// This does not include self, only the parent states.
///
/// This is a convenience method equivalent to `chain.extend(self.parent_state_chain())`.
pub fn append_parent_chain<'a>(&'a self, chain: &mut Vec<&'a Self>) {
chain.extend(self.parent_state_chain());
}
@@ -1453,19 +1456,18 @@ mod tests {
let mut test_block_builder: TestBlockBuilder = TestBlockBuilder::default();
let chain = create_mock_state_chain(&mut test_block_builder, 4);
let parents = chain[3].parent_state_chain();
let parents: Vec<_> = chain[3].parent_state_chain().collect();
assert_eq!(parents.len(), 3);
assert_eq!(parents[0].block().recovered_block().number, 3);
assert_eq!(parents[1].block().recovered_block().number, 2);
assert_eq!(parents[2].block().recovered_block().number, 1);
let parents = chain[2].parent_state_chain();
let parents: Vec<_> = chain[2].parent_state_chain().collect();
assert_eq!(parents.len(), 2);
assert_eq!(parents[0].block().recovered_block().number, 2);
assert_eq!(parents[1].block().recovered_block().number, 1);
let parents = chain[0].parent_state_chain();
assert_eq!(parents.len(), 0);
assert_eq!(chain[0].parent_state_chain().count(), 0);
}
#[test]
@@ -1476,8 +1478,7 @@ mod tests {
create_mock_state(&mut test_block_builder, single_block_number, B256::random());
let single_block_hash = single_block.block().recovered_block().hash();
let parents = single_block.parent_state_chain();
assert_eq!(parents.len(), 0);
assert_eq!(single_block.parent_state_chain().count(), 0);
let block_state_chain = single_block.chain().collect::<Vec<_>>();
assert_eq!(block_state_chain.len(), 1);

View File

@@ -5,14 +5,14 @@ use reth_errors::ProviderResult;
use reth_primitives_traits::{Account, Bytecode, NodePrimitives};
use reth_storage_api::{
AccountReader, BlockHashReader, BytecodeReader, HashedPostStateProvider, StateProofProvider,
StateProvider, StateRootProvider, StorageRootProvider,
StateProvider, StateProviderBox, StateRootProvider, StorageRootProvider,
};
use reth_trie::{
updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof,
MultiProofTargets, StorageMultiProof, TrieInput,
};
use revm_database::BundleState;
use std::sync::OnceLock;
use std::{borrow::Cow, sync::OnceLock};
/// A state provider that stores references to in-memory blocks along with their state as well as a
/// reference of the historical state provider for fallback lookups.
@@ -24,15 +24,11 @@ pub struct MemoryOverlayStateProviderRef<
/// Historical state provider for state lookups that are not found in memory blocks.
pub(crate) historical: Box<dyn StateProvider + 'a>,
/// The collection of executed parent blocks. Expected order is newest to oldest.
pub(crate) in_memory: Vec<ExecutedBlock<N>>,
pub(crate) in_memory: Cow<'a, [ExecutedBlock<N>]>,
/// Lazy-loaded in-memory trie data.
pub(crate) trie_input: OnceLock<TrieInput>,
}
/// A state provider that stores references to in-memory blocks along with their state as well as
/// the historical state provider for fallback lookups.
pub type MemoryOverlayStateProvider<N> = MemoryOverlayStateProviderRef<'static, N>;
impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
/// Create new memory overlay state provider.
///
@@ -42,7 +38,7 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
/// - `historical` - a historical state provider for the latest ancestor block stored in the
/// database.
pub fn new(historical: Box<dyn StateProvider + 'a>, in_memory: Vec<ExecutedBlock<N>>) -> Self {
Self { historical, in_memory, trie_input: OnceLock::new() }
Self { historical, in_memory: Cow::Owned(in_memory), trie_input: OnceLock::new() }
}
/// Turn this state provider into a state provider
@@ -53,11 +49,14 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
/// Return lazy-loaded trie state aggregated from in-memory blocks.
fn trie_input(&self) -> &TrieInput {
self.trie_input.get_or_init(|| {
let bundles: Vec<_> =
self.in_memory.iter().rev().map(|block| block.trie_data()).collect();
TrieInput::from_blocks_sorted(
bundles.iter().map(|data| (data.hashed_state.as_ref(), data.trie_updates.as_ref())),
)
let mut input = TrieInput::default();
// Iterate from oldest to newest
for block in self.in_memory.iter().rev() {
let data = block.trie_data();
input.nodes.extend_from_sorted(&data.trie_updates);
input.state.extend_from_sorted(&data.hashed_state);
}
input
})
}
@@ -71,7 +70,7 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> {
impl<N: NodePrimitives> BlockHashReader for MemoryOverlayStateProviderRef<'_, N> {
fn block_hash(&self, number: BlockNumber) -> ProviderResult<Option<B256>> {
for block in &self.in_memory {
for block in self.in_memory.iter() {
if block.recovered_block().number() == number {
return Ok(Some(block.recovered_block().hash()));
}
@@ -90,7 +89,7 @@ impl<N: NodePrimitives> BlockHashReader for MemoryOverlayStateProviderRef<'_, N>
let mut in_memory_hashes = Vec::with_capacity(range.size_hint().0);
// iterate in ascending order (oldest to newest = low to high)
for block in &self.in_memory {
for block in self.in_memory.iter() {
let block_num = block.recovered_block().number();
if range.contains(&block_num) {
in_memory_hashes.push(block.recovered_block().hash());
@@ -112,7 +111,7 @@ impl<N: NodePrimitives> BlockHashReader for MemoryOverlayStateProviderRef<'_, N>
impl<N: NodePrimitives> AccountReader for MemoryOverlayStateProviderRef<'_, N> {
fn basic_account(&self, address: &Address) -> ProviderResult<Option<Account>> {
for block in &self.in_memory {
for block in self.in_memory.iter() {
if let Some(account) = block.execution_output.account(address) {
return Ok(account);
}
@@ -216,7 +215,7 @@ impl<N: NodePrimitives> StateProvider for MemoryOverlayStateProviderRef<'_, N> {
address: Address,
storage_key: StorageKey,
) -> ProviderResult<Option<StorageValue>> {
for block in &self.in_memory {
for block in self.in_memory.iter() {
if let Some(value) = block.execution_output.storage(&address, storage_key.into()) {
return Ok(Some(value));
}
@@ -228,7 +227,7 @@ impl<N: NodePrimitives> StateProvider for MemoryOverlayStateProviderRef<'_, N> {
impl<N: NodePrimitives> BytecodeReader for MemoryOverlayStateProviderRef<'_, N> {
fn bytecode_by_hash(&self, code_hash: &B256) -> ProviderResult<Option<Bytecode>> {
for block in &self.in_memory {
for block in self.in_memory.iter() {
if let Some(contract) = block.execution_output.bytecode(code_hash) {
return Ok(Some(contract));
}
@@ -237,3 +236,46 @@ impl<N: NodePrimitives> BytecodeReader for MemoryOverlayStateProviderRef<'_, N>
self.historical.bytecode_by_hash(code_hash)
}
}
/// An owned state provider that stores references to in-memory blocks along with their state as
/// well as a reference of the historical state provider for fallback lookups.
#[expect(missing_debug_implementations)]
pub struct MemoryOverlayStateProvider<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives> {
/// Historical state provider for state lookups that are not found in memory blocks.
pub(crate) historical: StateProviderBox,
/// The collection of executed parent blocks. Expected order is newest to oldest.
pub(crate) in_memory: Vec<ExecutedBlock<N>>,
/// Lazy-loaded in-memory trie data.
pub(crate) trie_input: OnceLock<TrieInput>,
}
impl<N: NodePrimitives> MemoryOverlayStateProvider<N> {
/// Create new memory overlay state provider.
///
/// ## Arguments
///
/// - `in_memory` - the collection of executed ancestor blocks in reverse.
/// - `historical` - a historical state provider for the latest ancestor block stored in the
/// database.
pub fn new(historical: StateProviderBox, in_memory: Vec<ExecutedBlock<N>>) -> Self {
Self { historical, in_memory, trie_input: OnceLock::new() }
}
/// Returns a new provider that takes the `TX` as reference
#[inline(always)]
fn as_ref(&self) -> MemoryOverlayStateProviderRef<'_, N> {
MemoryOverlayStateProviderRef {
historical: Box::new(self.historical.as_ref()),
in_memory: Cow::Borrowed(&self.in_memory),
trie_input: self.trie_input.clone(),
}
}
/// Wraps the [`Self`] in a `Box`.
pub fn boxed(self) -> StateProviderBox {
Box::new(self)
}
}
// Delegates all provider impls to [`MemoryOverlayStateProviderRef`]
reth_storage_api::macros::delegate_provider_impls!(MemoryOverlayStateProvider<N> where [N: NodePrimitives]);

View File

@@ -117,7 +117,7 @@ impl<N: NodePrimitives> TestBlockBuilder<N> {
.map(|_| {
let tx = mock_tx(self.signer_build_account_info.nonce);
self.signer_build_account_info.nonce += 1;
self.signer_build_account_info.balance -= signer_balance_decrease;
self.signer_build_account_info.balance -= Self::single_tx_cost();
tx
})
.collect();
@@ -173,7 +173,6 @@ impl<N: NodePrimitives> TestBlockBuilder<N> {
transactions: transactions.into_iter().map(|tx| tx.into_inner()).collect(),
ommers: Vec::new(),
withdrawals: Some(vec![].into()),
block_access_list: None,
},
)
}

View File

@@ -45,10 +45,6 @@ use reth_network_peers::{
};
use reth_primitives_traits::{sync::LazyLock, BlockHeader, SealedHeader};
/// The hash of an empty block access list.
const EMPTY_BLOCK_ACCESS_LIST_HASH: B256 =
b256!("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347");
/// Helper method building a [`Header`] given [`Genesis`] and [`ChainHardforks`].
pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Header {
// If London is activated at genesis, we set the initial base fee as per EIP-1559.
@@ -83,13 +79,9 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
.active_at_timestamp(genesis.timestamp)
.then_some(EMPTY_REQUESTS_HASH);
// If Amsterdam is activated at genesis we set block access list hash empty hash.
let block_access_list_hash = hardforks
.fork(EthereumHardfork::Amsterdam)
.active_at_timestamp(genesis.timestamp)
.then_some(EMPTY_BLOCK_ACCESS_LIST_HASH);
Header {
number: genesis.number.unwrap_or_default(),
parent_hash: genesis.parent_hash.unwrap_or_default(),
gas_limit: genesis.gas_limit,
difficulty: genesis.difficulty,
nonce: genesis.nonce.into(),
@@ -104,7 +96,6 @@ pub fn make_genesis_header(genesis: &Genesis, hardforks: &ChainHardforks) -> Hea
blob_gas_used,
excess_blob_gas,
requests_hash,
block_access_list_hash,
..Default::default()
}
}
@@ -309,7 +300,6 @@ pub fn create_chain_config(
cancun_time: timestamp(EthereumHardfork::Cancun),
prague_time: timestamp(EthereumHardfork::Prague),
osaka_time: timestamp(EthereumHardfork::Osaka),
amsterdam_time: timestamp(EthereumHardfork::Amsterdam),
bpo1_time: timestamp(EthereumHardfork::Bpo1),
bpo2_time: timestamp(EthereumHardfork::Bpo2),
bpo3_time: timestamp(EthereumHardfork::Bpo3),
@@ -899,7 +889,6 @@ impl From<Genesis> for ChainSpec {
(EthereumHardfork::Bpo3.boxed(), genesis.config.bpo3_time),
(EthereumHardfork::Bpo4.boxed(), genesis.config.bpo4_time),
(EthereumHardfork::Bpo5.boxed(), genesis.config.bpo5_time),
(EthereumHardfork::Amsterdam.boxed(), genesis.config.amsterdam_time),
];
let mut time_hardforks = time_hardfork_opts
@@ -981,7 +970,7 @@ impl<H: BlockHeader> EthereumHardforks for ChainSpec<H> {
/// A trait for reading the current chainspec.
#[auto_impl::auto_impl(&, Arc)]
pub trait ChainSpecProvider: Debug + Send + Sync {
pub trait ChainSpecProvider: Debug + Send {
/// The chain spec type.
type ChainSpec: EthChainSpec + 'static;
@@ -1206,19 +1195,6 @@ impl ChainSpecBuilder {
self
}
/// Enable Amsterdam at genesis.
pub fn amsterdam_activated(mut self) -> Self {
self = self.osaka_activated();
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(0));
self
}
/// Enable Amsterdam at the given timestamp.
pub fn with_amsterdam_at(mut self, timestamp: u64) -> Self {
self.hardforks.insert(EthereumHardfork::Amsterdam, ForkCondition::Timestamp(timestamp));
self
}
/// Build the resulting [`ChainSpec`].
///
/// # Panics

View File

@@ -23,7 +23,10 @@ use reth_node_core::{
dirs::{ChainPath, DataDirPath},
};
use reth_provider::{
providers::{BlockchainProvider, NodeTypesForProvider, StaticFileProvider},
providers::{
BlockchainProvider, NodeTypesForProvider, RocksDBProvider, StaticFileProvider,
StaticFileProviderBuilder,
},
ProviderFactory, StaticFileProviderFactory,
};
use reth_stages::{sets::DefaultStages, Pipeline, PipelineTarget};
@@ -75,10 +78,12 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
let data_dir = self.datadir.clone().resolve_datadir(self.chain.chain());
let db_path = data_dir.db();
let sf_path = data_dir.static_files();
let rocksdb_path = data_dir.rocksdb();
if access.is_read_write() {
reth_fs_util::create_dir_all(&db_path)?;
reth_fs_util::create_dir_all(&sf_path)?;
reth_fs_util::create_dir_all(&rocksdb_path)?;
}
let config_path = self.config.clone().unwrap_or_else(|| data_dir.config());
@@ -98,18 +103,32 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
}
info!(target: "reth::cli", ?db_path, ?sf_path, "Opening storage");
let genesis_block_number = self.chain.genesis().number.unwrap_or_default();
let (db, sfp) = match access {
AccessRights::RW => (
Arc::new(init_db(db_path, self.db.database_args())?),
StaticFileProvider::read_write(sf_path)?,
),
AccessRights::RO | AccessRights::RoInconsistent => (
Arc::new(open_db_read_only(&db_path, self.db.database_args())?),
StaticFileProvider::read_only(sf_path, false)?,
StaticFileProviderBuilder::read_write(sf_path)?
.with_genesis_block_number(genesis_block_number)
.build()?,
),
AccessRights::RO | AccessRights::RoInconsistent => {
(Arc::new(open_db_read_only(&db_path, self.db.database_args())?), {
let provider = StaticFileProviderBuilder::read_only(sf_path)?
.with_genesis_block_number(genesis_block_number)
.build()?;
provider.watch_directory();
provider
})
}
};
// TransactionDB only support read-write mode
let rocksdb_provider = RocksDBProvider::builder(data_dir.rocksdb())
.with_default_tables()
.with_database_log_level(self.db.log_level)
.build()?;
let provider_factory = self.create_provider_factory(&config, db, sfp, access)?;
let provider_factory =
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.static_files.to_settings())?;
@@ -128,6 +147,7 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
config: &Config,
db: Arc<DatabaseEnv>,
static_file_provider: StaticFileProvider<N::Primitives>,
rocksdb_provider: RocksDBProvider,
access: AccessRights,
) -> eyre::Result<ProviderFactory<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>>
where
@@ -138,6 +158,7 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
db,
self.chain.clone(),
static_file_provider,
rocksdb_provider,
)?
.with_prune_modes(prune_modes.clone());

View File

@@ -2,6 +2,7 @@ use crate::common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs};
use clap::{Parser, Subcommand};
use reth_chainspec::{EthChainSpec, EthereumHardforks};
use reth_cli::chainspec::ChainSpecParser;
use reth_cli_runner::CliContext;
use reth_db::version::{get_db_version, DatabaseVersionError, DB_VERSION};
use reth_db_common::DbTool;
use std::{
@@ -79,7 +80,10 @@ macro_rules! db_exec {
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
/// Execute `db` command
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(self) -> eyre::Result<()> {
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(
self,
ctx: CliContext,
) -> eyre::Result<()> {
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();
@@ -158,7 +162,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
let access_rights =
if command.dry_run { AccessRights::RO } else { AccessRights::RW };
db_exec!(self.env, tool, N, access_rights, {
command.execute(&tool)?;
command.execute(&tool, ctx.task_executor.clone())?;
});
}
Subcommands::StaticFileHeader(command) => {

View File

@@ -18,6 +18,7 @@ use reth_node_metrics::{
};
use reth_provider::{providers::ProviderNodeTypes, ChainSpecProvider, StageCheckpointReader};
use reth_stages::StageId;
use reth_tasks::TaskExecutor;
use reth_trie::{
verify::{Output, Verifier},
Nibbles,
@@ -48,52 +49,37 @@ pub struct Command {
impl Command {
/// Execute `db repair-trie` command
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
pub fn execute<N: ProviderNodeTypes>(
self,
tool: &DbTool<N>,
task_executor: TaskExecutor,
) -> eyre::Result<()> {
// Set up metrics server if requested
let _metrics_handle = if let Some(listen_addr) = self.metrics {
// Spawn an OS thread with a single-threaded tokio runtime for the metrics server
let chain_name = tool.provider_factory.chain_spec().chain().to_string();
let executor = task_executor.clone();
let handle = std::thread::Builder::new().name("metrics-server".to_string()).spawn(
move || {
// Create a single-threaded tokio runtime
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to create tokio runtime for metrics server");
let handle = task_executor.spawn_critical("metrics server", async move {
let config = MetricServerConfig::new(
listen_addr,
VersionInfo {
version: version_metadata().cargo_pkg_version.as_ref(),
build_timestamp: version_metadata().vergen_build_timestamp.as_ref(),
cargo_features: version_metadata().vergen_cargo_features.as_ref(),
git_sha: version_metadata().vergen_git_sha.as_ref(),
target_triple: version_metadata().vergen_cargo_target_triple.as_ref(),
build_profile: version_metadata().build_profile_name.as_ref(),
},
ChainSpecInfo { name: chain_name },
executor,
Hooks::builder().build(),
);
let handle = runtime.handle().clone();
runtime.block_on(async move {
let task_manager = reth_tasks::TaskManager::new(handle.clone());
let task_executor = task_manager.executor();
let config = MetricServerConfig::new(
listen_addr,
VersionInfo {
version: version_metadata().cargo_pkg_version.as_ref(),
build_timestamp: version_metadata().vergen_build_timestamp.as_ref(),
cargo_features: version_metadata().vergen_cargo_features.as_ref(),
git_sha: version_metadata().vergen_git_sha.as_ref(),
target_triple: version_metadata()
.vergen_cargo_target_triple
.as_ref(),
build_profile: version_metadata().build_profile_name.as_ref(),
},
ChainSpecInfo { name: chain_name },
task_executor,
Hooks::builder().build(),
);
// Spawn the metrics server
if let Err(e) = MetricServer::new(config).serve().await {
tracing::error!("Metrics server error: {}", e);
}
// Block forever to keep the runtime alive
std::future::pending::<()>().await
});
},
)?;
// Spawn the metrics server
if let Err(e) = MetricServer::new(config).serve().await {
tracing::error!("Metrics server error: {}", e);
}
});
Some(handle)
} else {
@@ -315,8 +301,8 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
if inconsistent_nodes == 0 {
info!("No inconsistencies found");
} else {
info!("Repaired {} inconsistencies, committing changes", inconsistent_nodes);
provider_rw.commit()?;
info!("Repaired {} inconsistencies and committed changes", inconsistent_nodes);
}
Ok(())

View File

@@ -92,6 +92,8 @@ impl Command {
receipts_in_static_files: _,
transaction_senders_in_static_files: _,
storages_history_in_rocksdb: _,
transaction_hash_numbers_in_rocksdb: _,
account_history_in_rocksdb: _,
} = settings.unwrap_or_else(StorageSettings::legacy);
// Update the setting based on the key

View File

@@ -1,8 +1,9 @@
//! Command that initializes the node from a genesis file.
use crate::common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs};
use alloy_consensus::BlockHeader;
use clap::Parser;
use reth_chainspec::{EthChainSpec, EthereumHardforks};
use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
use reth_cli::chainspec::ChainSpecParser;
use reth_provider::BlockHashReader;
use std::sync::Arc;
@@ -22,8 +23,9 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitComman
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
let genesis_block_number = provider_factory.chain_spec().genesis_header().number();
let hash = provider_factory
.block_hash(0)?
.block_hash(genesis_block_number)?
.ok_or_else(|| eyre::eyre!("Genesis hash not found."))?;
info!(target: "reth::cli", hash = ?hash, "Genesis block written");

View File

@@ -79,7 +79,7 @@ where
+ StaticFileProviderFactory<Primitives: NodePrimitives<BlockHeader: Compact>>,
{
provider_rw.insert_block(
SealedBlock::<<Provider::Primitives as NodePrimitives>::Block>::from_sealed_parts(
&SealedBlock::<<Provider::Primitives as NodePrimitives>::Block>::from_sealed_parts(
header.clone(),
Default::default(),
)

View File

@@ -72,7 +72,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
.split();
if result.len() != 1 {
eyre::bail!(
"Invalid number of headers received. Expected: 1. Received: {}",
"Invalid number of bodies received. Expected: 1. Received: {}",
result.len()
)
}
@@ -189,7 +189,7 @@ impl<C: ChainSpecParser> DownloadArgs<C> {
let net = NetworkConfigBuilder::<N::NetworkPrimitives>::new(p2p_secret_key)
.peer_config(config.peers_config_with_basic_nodes_from_file(None))
.external_ip_resolver(self.network.nat)
.external_ip_resolver(self.network.nat.clone())
.network_id(self.network.network_id)
.boot_nodes(boot_nodes.clone())
.apply(|builder| {

View File

@@ -110,7 +110,6 @@ impl<C: ChainSpecParser> Command<C> {
tx.clear::<tables::TransactionBlocks>()?;
tx.clear::<tables::BlockOmmers<HeaderTy<N>>>()?;
tx.clear::<tables::BlockWithdrawals>()?;
tx.clear::<tables::BlockAccessLists>()?;
reset_stage_checkpoint(tx, StageId::Bodies)?;
insert_genesis_header(&provider_rw, &self.env.chain)?;

View File

@@ -9,7 +9,7 @@ use reth_evm::ConfigureEvm;
use reth_node_builder::NodeTypesWithDB;
use reth_node_core::dirs::{ChainPath, DataDirPath};
use reth_provider::{
providers::{ProviderNodeTypes, StaticFileProvider},
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
DatabaseProviderFactory, ProviderFactory,
};
use reth_stages::{stages::ExecutionStage, Stage, StageCheckpoint, UnwindInput};
@@ -42,6 +42,7 @@ where
Arc::new(output_db),
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
)?,
to,
from,

View File

@@ -6,7 +6,7 @@ use reth_db_api::{database::Database, table::TableImporter, tables};
use reth_db_common::DbTool;
use reth_node_core::dirs::{ChainPath, DataDirPath};
use reth_provider::{
providers::{ProviderNodeTypes, StaticFileProvider},
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
DatabaseProviderFactory, ProviderFactory,
};
use reth_stages::{stages::AccountHashingStage, Stage, StageCheckpoint, UnwindInput};
@@ -39,6 +39,7 @@ pub(crate) async fn dump_hashing_account_stage<N: ProviderNodeTypes<DB = Arc<Dat
Arc::new(output_db),
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
)?,
to,
from,

View File

@@ -5,7 +5,7 @@ use reth_db_api::{database::Database, table::TableImporter, tables};
use reth_db_common::DbTool;
use reth_node_core::dirs::{ChainPath, DataDirPath};
use reth_provider::{
providers::{ProviderNodeTypes, StaticFileProvider},
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
DatabaseProviderFactory, ProviderFactory,
};
use reth_stages::{stages::StorageHashingStage, Stage, StageCheckpoint, UnwindInput};
@@ -29,6 +29,7 @@ pub(crate) async fn dump_hashing_storage_stage<N: ProviderNodeTypes<DB = Arc<Dat
Arc::new(output_db),
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
)?,
to,
from,

View File

@@ -12,7 +12,7 @@ use reth_evm::ConfigureEvm;
use reth_exex::ExExManagerHandle;
use reth_node_core::dirs::{ChainPath, DataDirPath};
use reth_provider::{
providers::{ProviderNodeTypes, StaticFileProvider},
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
DatabaseProviderFactory, ProviderFactory,
};
use reth_stages::{
@@ -62,6 +62,7 @@ where
Arc::new(output_db),
db_tool.chain(),
StaticFileProvider::read_write(output_datadir.static_files())?,
RocksDBProvider::builder(output_datadir.rocksdb()).build()?,
)?,
to,
from,

View File

@@ -97,6 +97,57 @@ impl CliRunner {
command_res
}
/// Executes a command in a blocking context with access to `CliContext`.
///
/// See [`Runtime::spawn_blocking`](tokio::runtime::Runtime::spawn_blocking).
pub fn run_blocking_command_until_exit<F, E>(
self,
command: impl FnOnce(CliContext) -> F + Send + 'static,
) -> Result<(), E>
where
F: Future<Output = Result<(), E>> + Send + 'static,
E: Send + Sync + From<std::io::Error> + From<reth_tasks::PanickedTaskError> + 'static,
{
let AsyncCliRunner { context, mut task_manager, tokio_runtime } =
AsyncCliRunner::new(self.tokio_runtime);
// Spawn the command on the blocking thread pool
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 = 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") },
),
));
if command_res.is_err() {
error!(target: "reth::cli", "shutting down due to error");
} else {
debug!(target: "reth::cli", "shutting down gracefully");
task_manager.graceful_shutdown_with_timeout(Duration::from_secs(5));
}
// Shutdown the runtime on a separate thread
let (tx, rx) = mpsc::channel();
std::thread::Builder::new()
.name("tokio-runtime-shutdown".to_string())
.spawn(move || {
drop(tokio_runtime);
let _ = tx.send(());
})
.unwrap();
let _ = rx.recv_timeout(Duration::from_secs(5)).inspect_err(|err| {
debug!(target: "reth::cli", %err, "tokio runtime shutdown timed out");
});
command_res
}
/// Executes a regular future until completion or until external signal received.
pub fn run_until_ctrl_c<F, E>(self, fut: F) -> Result<(), E>
where

View File

@@ -22,7 +22,6 @@ pub const DEFAULT_BLOCK_INTERVAL: usize = 5;
#[cfg_attr(feature = "serde", serde(default))]
pub struct Config {
/// Configuration for each stage in the pipeline.
// TODO(onbjerg): Can we make this easier to maintain when we add/remove stages?
pub stages: StageConfig,
/// Configuration for pruning.
#[cfg_attr(feature = "serde", serde(default))]

View File

@@ -14,14 +14,11 @@ workspace = true
# reth
reth-chainspec.workspace = true
reth-consensus.workspace = true
tracing.workspace = true
# ethereum
reth-primitives-traits.workspace = true
alloy-consensus.workspace = true
alloy-primitives.workspace = true
alloy-eips.workspace = true
alloy-rlp.workspace = true
[dev-dependencies]
alloy-primitives = { workspace = true, features = ["rand"] }
@@ -38,6 +35,4 @@ std = [
"reth-primitives-traits/std",
"reth-ethereum-primitives/std",
"alloy-primitives/std",
"alloy-rlp/std",
"tracing/std",
]

View File

@@ -69,29 +69,6 @@ pub fn validate_shanghai_withdrawals<B: Block>(
Ok(())
}
/// Validate that block access lists are present in Amsterdam
///
/// [EIP-7928]: https://eips.ethereum.org/EIPS/eip-7928
#[inline]
pub fn validate_amsterdam_block_access_lists<B: Block>(
block: &SealedBlock<B>,
) -> Result<(), ConsensusError> {
let bal = block.body().block_access_list().ok_or(ConsensusError::BlockAccessListMissing)?;
let bal_hash = alloy_primitives::keccak256(alloy_rlp::encode(bal));
let header_bal_hash =
block.block_access_list_hash().ok_or(ConsensusError::BlockAccessListHashMissing)?;
if bal_hash != header_bal_hash {
tracing::error!(
target: "consensus",
?header_bal_hash,
?bal,
"Block access list hash mismatch in validation.rs in L81"
);
return Err(ConsensusError::InvalidBalHash);
}
Ok(())
}
/// Validate that blob gas is present in the block if Cancun is active.
///
/// See [EIP-4844]: Shard Blob Transactions
@@ -154,21 +131,6 @@ where
}
_ => return Err(ConsensusError::WithdrawalsRootUnexpected),
}
if let (Some(expected_hash), Some(body_bal)) =
(header.block_access_list_hash(), body.block_access_list())
{
let got_hash = alloy_primitives::keccak256(alloy_rlp::encode(body_bal));
if got_hash != expected_hash {
tracing::error!(
target: "consensus",
?expected_hash,
?body_bal,
"Block access list hash mismatch in validation.rs in L164"
);
return Err(ConsensusError::InvalidBalHash);
}
}
Ok(())
}
@@ -255,10 +217,6 @@ where
})
}
if chain_spec.is_amsterdam_active_at_timestamp(block.header().timestamp()) {
validate_amsterdam_block_access_lists(block)?;
}
Ok(())
}
@@ -321,20 +279,28 @@ pub fn validate_against_parent_hash_number<H: BlockHeader>(
header: &H,
parent: &SealedHeader<H>,
) -> Result<(), ConsensusError> {
// Parent number is consistent.
if parent.number() + 1 != header.number() {
return Err(ConsensusError::ParentBlockNumberMismatch {
parent_block_number: parent.number(),
block_number: header.number(),
})
}
if parent.hash() != header.parent_hash() {
return Err(ConsensusError::ParentHashMismatch(
GotExpected { got: header.parent_hash(), expected: parent.hash() }.into(),
))
}
let Some(parent_number) = parent.number().checked_add(1) else {
// parent block already reached the maximum
return Err(ConsensusError::ParentBlockNumberMismatch {
parent_block_number: parent.number(),
block_number: u64::MAX,
})
};
// Parent number is consistent.
if parent_number != header.number() {
return Err(ConsensusError::ParentBlockNumberMismatch {
parent_block_number: parent.number(),
block_number: header.number(),
})
}
Ok(())
}
@@ -526,7 +492,6 @@ mod tests {
transactions: vec![transaction],
ommers: vec![],
withdrawals: Some(Withdrawals::default()),
block_access_list: None,
};
let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });

View File

@@ -135,7 +135,7 @@ pub enum ConsensusError {
/// The gas limit in the block header.
gas_limit: u64,
},
/// Error when the gas the gas limit is more than the maximum allowed.
/// Error when the gas limit is more than the maximum allowed.
#[error(
"header gas limit ({gas_limit}) exceed the maximum allowed gas limit ({MAXIMUM_GAS_LIMIT_BLOCK})"
)]
@@ -404,41 +404,9 @@ pub enum ConsensusError {
/// The maximum allowed RLP length.
max_rlp_length: usize,
},
/// Error when the hash of block access list is different from the expected hash.
#[error("Block header's BAL hash does not match the computed BAL hash.")]
InvalidBalHash,
/// Error when the block access list hash is missing.
#[error("block access list hash missing")]
BlockAccessListHashMissing,
/// Error when the block access list is different from the expected access list.
#[error("Block's access list is invalid.")]
InvalidBlockAccessList,
/// Error when the block access list is missing.
#[error("block access list missing")]
BlockAccessListMissing,
/// Error when the block access list hash is unexpected.
#[error("block access list hash unexpected")]
BlockAccessListHashUnexpected,
/// Error when the block access list contains an account change that is not present in the
/// computed access list.
#[error("Block BAL contains an account change that is not present in the computed BAL.")]
InvalidBalExtraAccount,
/// Error when the block access list is missing an account change that is present in the
/// computed access list.
#[error("Block BAL is missing an account change that is present in the computed BAL.")]
InvalidBalMissingAccount,
/// EIP-7825: Transaction gas limit exceeds maximum allowed
#[error(transparent)]
TransactionGasLimitTooHigh(Box<TxGasLimitTooHighErr>),
/// Other, likely an injected L2 error.
#[error("{0}")]
Other(String),

View File

@@ -1,5 +1,5 @@
use crate::BlockProvider;
use alloy_provider::{ConnectionConfig, Network, Provider, ProviderBuilder};
use alloy_provider::{ConnectionConfig, Network, Provider, ProviderBuilder, WebSocketConfig};
use alloy_transport::TransportResult;
use futures::{Stream, StreamExt};
use reth_node_api::Block;
@@ -29,7 +29,12 @@ impl<N: Network, PrimitiveBlock> RpcBlockProvider<N, PrimitiveBlock> {
ProviderBuilder::default()
.connect_with_config(
rpc_url,
ConnectionConfig::default().with_max_retries(u32::MAX),
ConnectionConfig::default().with_max_retries(u32::MAX).with_ws_config(
WebSocketConfig::default()
// allow larger messages/frames for big blocks
.max_frame_size(Some(128 * 1024 * 1024))
.max_message_size(Some(128 * 1024 * 1024)),
),
)
.await?,
),
@@ -84,8 +89,8 @@ where
match res {
Ok(block) => {
if tx.send((self.convert)(block)).await.is_err() {
// Channel closed.
break;
// Channel closed - receiver dropped, exit completely.
return;
}
}
Err(err) => {
@@ -102,7 +107,7 @@ where
debug!(
target: "consensus::debug-client",
url=%self.url,
"Re-estbalishing block subscription",
"Re-establishing block subscription",
);
}
}

View File

@@ -11,6 +11,7 @@ use reth_node_builder::{
PayloadTypes,
};
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::TaskManager;
@@ -157,8 +158,8 @@ where
.await?;
let node = NodeTestContext::new(node, self.attributes_generator).await?;
let genesis = node.block_hash(0);
let genesis_number = self.chain_spec.genesis_header().number();
let genesis = node.block_hash(genesis_number);
node.update_forkchoice(genesis, genesis).await?;
eyre::Ok(node)

View File

@@ -110,6 +110,7 @@ pub async fn setup_engine_with_chain_import(
// Create database path and static files path
let db_path = datadir.join("db");
let static_files_path = datadir.join("static_files");
let rocksdb_dir_path = datadir.join("rocksdb");
// Initialize the database using init_db (same as CLI import command)
// Use the same database arguments as the node will use
@@ -125,6 +126,7 @@ pub async fn setup_engine_with_chain_import(
db.clone(),
chain_spec.clone(),
reth_provider::providers::StaticFileProvider::read_write(static_files_path.clone())?,
reth_provider::providers::RocksDBProvider::builder(rocksdb_dir_path).build().unwrap(),
)?;
// Initialize genesis if needed
@@ -311,6 +313,7 @@ mod tests {
std::fs::create_dir_all(&datadir).unwrap();
let db_path = datadir.join("db");
let static_files_path = datadir.join("static_files");
let rocksdb_dir_path = datadir.join("rocksdb");
// Import the chain
{
@@ -324,6 +327,9 @@ mod tests {
chain_spec.clone(),
reth_provider::providers::StaticFileProvider::read_write(static_files_path.clone())
.unwrap(),
reth_provider::providers::RocksDBProvider::builder(rocksdb_dir_path.clone())
.build()
.unwrap(),
)
.expect("failed to create provider factory");
@@ -385,6 +391,9 @@ mod tests {
chain_spec.clone(),
reth_provider::providers::StaticFileProvider::read_only(static_files_path, false)
.unwrap(),
reth_provider::providers::RocksDBProvider::builder(rocksdb_dir_path)
.build()
.unwrap(),
)
.expect("failed to create provider factory");
@@ -472,11 +481,15 @@ mod tests {
// Create static files path
let static_files_path = datadir.join("static_files");
// Create rocksdb path
let rocksdb_dir_path = datadir.join("rocksdb");
// Create a provider factory
let provider_factory: ProviderFactory<MockNodeTypesWithDB> = ProviderFactory::new(
db.clone(),
chain_spec.clone(),
reth_provider::providers::StaticFileProvider::read_write(static_files_path).unwrap(),
reth_provider::providers::RocksDBProvider::builder(rocksdb_dir_path).build().unwrap(),
)
.expect("failed to create provider factory");

View File

@@ -56,7 +56,6 @@ pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlo
excess_blob_gas: None,
parent_beacon_block_root: None,
requests_hash: None,
block_access_list_hash: None,
};
// Set required fields based on chain spec
@@ -107,7 +106,6 @@ pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlo
transactions: vec![],
ommers: vec![],
withdrawals: header.withdrawals_root.is_some().then(Withdrawals::default),
block_access_list: None,
};
// Create the block

View File

@@ -2,9 +2,7 @@
use crate::testsuite::{Action, Environment};
use alloy_primitives::B256;
use alloy_rpc_types_engine::{
ExecutionPayloadV1, ExecutionPayloadV2, ExecutionPayloadV3, PayloadStatusEnum,
};
use alloy_rpc_types_engine::{ExecutionPayloadV3, PayloadStatusEnum};
use alloy_rpc_types_eth::{Block, Header, Receipt, Transaction, TransactionRequest};
use eyre::Result;
use futures_util::future::BoxFuture;
@@ -131,7 +129,10 @@ where
})?;
// Convert block to ExecutionPayloadV3
let payload = block_to_payload_v3(block.clone());
let payload = ExecutionPayloadV3::from_block_unchecked(
block.hash(),
&block.map_transactions(|tx| tx.inner).into_consensus(),
);
// Send the payload to the target node
let target_engine = env.node_clients[self.node_idx].engine.http_client();
@@ -327,32 +328,3 @@ where
})
}
}
/// Helper function to convert a block to `ExecutionPayloadV3`
fn block_to_payload_v3(block: Block) -> ExecutionPayloadV3 {
use alloy_primitives::U256;
ExecutionPayloadV3 {
payload_inner: ExecutionPayloadV2 {
payload_inner: ExecutionPayloadV1 {
parent_hash: block.header.inner.parent_hash,
fee_recipient: block.header.inner.beneficiary,
state_root: block.header.inner.state_root,
receipts_root: block.header.inner.receipts_root,
logs_bloom: block.header.inner.logs_bloom,
prev_randao: block.header.inner.mix_hash,
block_number: block.header.inner.number,
gas_limit: block.header.inner.gas_limit,
gas_used: block.header.inner.gas_used,
timestamp: block.header.inner.timestamp,
extra_data: block.header.inner.extra_data.clone(),
base_fee_per_gas: U256::from(block.header.inner.base_fee_per_gas.unwrap_or(0)),
block_hash: block.header.hash,
transactions: vec![], // No transactions needed for buffering tests
},
withdrawals: block.withdrawals.unwrap_or_default().to_vec(),
},
blob_gas_used: block.header.inner.blob_gas_used.unwrap_or(0),
excess_blob_gas: block.header.inner.excess_blob_gas.unwrap_or(0),
}
}

View File

@@ -5,7 +5,7 @@ use pretty_assertions::Comparison;
use reth_engine_primitives::InvalidBlockHook;
use reth_evm::{execute::Executor, ConfigureEvm};
use reth_primitives_traits::{NodePrimitives, RecoveredBlock, SealedHeader};
use reth_provider::{BlockExecutionOutput, StateProvider, StateProviderFactory};
use reth_provider::{BlockExecutionOutput, StateProvider, StateProviderBox, StateProviderFactory};
use reth_revm::{
database::StateProviderDatabase,
db::{BundleState, State},
@@ -80,13 +80,13 @@ fn sort_bundle_state_for_comparison(bundle_state: &BundleState) -> BundleStateSo
BundleAccountSorted {
info: acc.info.clone(),
original_info: acc.original_info.clone(),
storage: BTreeMap::from_iter(acc.storage.clone()),
storage: acc.storage.iter().map(|(k, v)| (*k, *v)).collect(),
status: acc.status,
},
)
})
.collect(),
contracts: BTreeMap::from_iter(bundle_state.contracts.clone()),
contracts: bundle_state.contracts.iter().map(|(k, v)| (*k, v.clone())).collect(),
reverts: bundle_state
.reverts
.iter()
@@ -98,7 +98,7 @@ fn sort_bundle_state_for_comparison(bundle_state: &BundleState) -> BundleStateSo
*addr,
AccountRevertSorted {
account: rev.account.clone(),
storage: BTreeMap::from_iter(rev.storage.clone()),
storage: rev.storage.iter().map(|(k, v)| (*k, *v)).collect(),
previous_status: rev.previous_status,
wipe_storage: rev.wipe_storage,
},
@@ -114,7 +114,7 @@ fn sort_bundle_state_for_comparison(bundle_state: &BundleState) -> BundleStateSo
/// Extracts execution data including codes, preimages, and hashed state from database
fn collect_execution_data(
mut db: State<StateProviderDatabase<Box<dyn StateProvider>>>,
mut db: State<StateProviderDatabase<StateProviderBox>>,
) -> eyre::Result<CollectionResult> {
let bundle_state = db.take_bundle();
let mut codes = BTreeMap::new();
@@ -448,14 +448,12 @@ mod tests {
nonce: account.nonce,
code_hash: account.bytecode_hash.unwrap_or_default(),
code: None,
storage_id: None,
}),
original_info: (i == 0).then(|| AccountInfo {
balance: account.balance.checked_div(U256::from(2)).unwrap_or(U256::ZERO),
nonce: 0,
code_hash: account.bytecode_hash.unwrap_or_default(),
code: None,
storage_id: None,
}),
storage,
status: AccountStatus::default(),
@@ -532,9 +530,7 @@ mod tests {
// Create a State with StateProviderTest
let state_provider = StateProviderTest::default();
let mut state = State::builder()
.with_database(StateProviderDatabase::new(
Box::new(state_provider) as Box<dyn StateProvider>
))
.with_database(StateProviderDatabase::new(Box::new(state_provider) as StateProviderBox))
.with_bundle_update()
.build();
@@ -841,7 +837,6 @@ mod tests {
receipts: vec![],
requests: Requests::default(),
gas_used: 0,
block_access_list: Default::default(),
blob_gas_used: 0,
},
};

View File

@@ -204,7 +204,7 @@ where
EngineApiMessageVersion::default(),
)
.await?;
tracing::debug!(target: "engine::local", "FCU result: {res:?}");
if !res.is_valid() {
eyre::bail!("Invalid payload status")
}

View File

@@ -1,5 +1,7 @@
//! Engine tree configuration.
use alloy_eips::merge::EPOCH_SLOTS;
/// Triggers persistence when the number of canonical blocks in memory exceeds this threshold.
pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
@@ -40,7 +42,7 @@ pub const DEFAULT_RESERVED_CPU_CORES: usize = 1;
/// Default maximum concurrency for prewarm task.
pub const DEFAULT_PREWARM_MAX_CONCURRENCY: usize = 16;
const DEFAULT_BLOCK_BUFFER_LIMIT: u32 = 256;
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;
const DEFAULT_CROSS_BLOCK_CACHE_SIZE: u64 = 4 * 1024 * 1024 * 1024;
@@ -89,6 +91,8 @@ pub struct TreeConfig {
/// Whether to always compare trie updates from the state root task to the trie updates from
/// the regular state root calculation.
always_compare_trie_updates: bool,
/// Whether to disable state cache.
disable_state_cache: bool,
/// Whether to disable parallel prewarming.
disable_prewarming: bool,
/// Whether to disable the parallel sparse trie state root algorithm.
@@ -97,7 +101,7 @@ pub struct TreeConfig {
state_provider_metrics: bool,
/// Cross-block cache size in bytes.
cross_block_cache_size: u64,
/// Whether the host has enough parallelism to run state root in parallel.
/// Whether the host has enough parallelism to run state root task.
has_enough_parallelism: bool,
/// Whether multiproof task should chunk proof targets.
multiproof_chunking_enabled: bool,
@@ -143,6 +147,7 @@ impl Default for TreeConfig {
max_execute_block_batch_size: DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE,
legacy_state_root: false,
always_compare_trie_updates: false,
disable_state_cache: false,
disable_prewarming: false,
disable_parallel_sparse_trie: false,
state_provider_metrics: false,
@@ -173,6 +178,7 @@ impl TreeConfig {
max_execute_block_batch_size: usize,
legacy_state_root: bool,
always_compare_trie_updates: bool,
disable_state_cache: bool,
disable_prewarming: bool,
disable_parallel_sparse_trie: bool,
state_provider_metrics: bool,
@@ -197,6 +203,7 @@ impl TreeConfig {
max_execute_block_batch_size,
legacy_state_root,
always_compare_trie_updates,
disable_state_cache,
disable_prewarming,
disable_parallel_sparse_trie,
state_provider_metrics,
@@ -271,7 +278,12 @@ impl TreeConfig {
self.disable_parallel_sparse_trie
}
/// Returns whether or not parallel prewarming should be used.
/// Returns whether or not state cache is disabled.
pub const fn disable_state_cache(&self) -> bool {
self.disable_state_cache
}
/// Returns whether or not parallel prewarming is disabled.
pub const fn disable_prewarming(&self) -> bool {
self.disable_prewarming
}
@@ -363,6 +375,12 @@ impl TreeConfig {
self
}
/// Setter for whether to disable state cache.
pub const fn without_state_cache(mut self, disable_state_cache: bool) -> Self {
self.disable_state_cache = disable_state_cache;
self
}
/// Setter for whether to disable parallel prewarming.
pub const fn without_prewarming(mut self, disable_prewarming: bool) -> Self {
self.disable_prewarming = disable_prewarming;
@@ -385,17 +403,12 @@ impl TreeConfig {
self
}
/// Setter for whether or not the host has enough parallelism to run state root in parallel.
/// Setter for has enough parallelism.
pub const fn with_has_enough_parallelism(mut self, has_enough_parallelism: bool) -> Self {
self.has_enough_parallelism = has_enough_parallelism;
self
}
/// Whether or not the host has enough parallelism to run state root in parallel.
pub const fn has_enough_parallelism(&self) -> bool {
self.has_enough_parallelism
}
/// Setter for state provider metrics.
pub const fn with_state_provider_metrics(mut self, state_provider_metrics: bool) -> Self {
self.state_provider_metrics = state_provider_metrics;

View File

@@ -22,7 +22,8 @@ use reth_trie_common::HashedPostState;
use serde::{de::DeserializeOwned, Serialize};
// Re-export [`ExecutionPayload`] moved to `reth_payload_primitives`
pub use reth_evm::{ConfigureEngineEvm, ExecutableTxIterator};
#[cfg(feature = "std")]
pub use reth_evm::{ConfigureEngineEvm, ExecutableTxIterator, ExecutableTxTuple};
pub use reth_payload_primitives::ExecutionPayload;
mod error;
@@ -61,8 +62,7 @@ pub trait EngineTypes:
+ TryInto<Self::ExecutionPayloadEnvelopeV2>
+ TryInto<Self::ExecutionPayloadEnvelopeV3>
+ TryInto<Self::ExecutionPayloadEnvelopeV4>
+ TryInto<Self::ExecutionPayloadEnvelopeV5>
+ TryInto<Self::ExecutionPayloadEnvelopeV6>,
+ TryInto<Self::ExecutionPayloadEnvelopeV5>,
> + DeserializeOwned
+ Serialize
{
@@ -106,14 +106,6 @@ pub trait EngineTypes:
+ Send
+ Sync
+ 'static;
/// Execution Payload V6 envelope type.
type ExecutionPayloadEnvelopeV6: DeserializeOwned
+ Serialize
+ Clone
+ Unpin
+ Send
+ Sync
+ 'static;
}
/// Type that validates the payloads processed by the engine API.

View File

@@ -16,7 +16,7 @@ reth-chain-state.workspace = true
reth-chainspec = { workspace = true, optional = true }
reth-consensus.workspace = true
reth-db.workspace = true
reth-engine-primitives.workspace = true
reth-engine-primitives = { workspace = true, features = ["std"] }
reth-errors.workspace = true
reth-execution-types.workspace = true
reth-evm = { workspace = true, features = ["metrics"] }
@@ -29,6 +29,7 @@ reth-provider.workspace = true
reth-prune.workspace = true
reth-revm.workspace = true
reth-stages-api.workspace = true
reth-storage-errors.workspace = true
reth-tasks.workspace = true
reth-trie-parallel.workspace = true
reth-trie-sparse = { workspace = true, features = ["std", "metrics"] }
@@ -39,6 +40,7 @@ reth-trie.workspace = true
alloy-evm.workspace = true
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-eip7928.workspace = true
alloy-primitives.workspace = true
alloy-rlp.workspace = true
alloy-rpc-types-engine.workspace = true
@@ -51,6 +53,7 @@ futures.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "sync", "macros"] }
mini-moka = { workspace = true, features = ["sync"] }
moka = { workspace = true, features = ["sync"] }
smallvec.workspace = true
# metrics

View File

@@ -26,12 +26,10 @@ fn create_bench_state(num_accounts: usize) -> EvmState {
nonce: 10,
code_hash: B256::from_slice(&rng.random::<[u8; 32]>()),
code: Default::default(),
storage_id: None,
},
storage,
status: AccountStatus::empty(),
transaction_id: 0,
..Default::default()
};
let address = Address::with_last_byte(i as u8);

View File

@@ -62,7 +62,6 @@ fn create_bench_state_updates(params: &BenchParams) -> Vec<EvmState> {
storage: HashMap::default(),
status: AccountStatus::SelfDestructed,
transaction_id: 0,
..Default::default()
}
} else {
RevmAccount {
@@ -71,7 +70,6 @@ fn create_bench_state_updates(params: &BenchParams) -> Vec<EvmState> {
nonce: rng.random::<u64>(),
code_hash: KECCAK_EMPTY,
code: Some(Default::default()),
storage_id: None,
},
storage: (0..rng.random_range(0..=params.storage_slots_per_account))
.map(|_| {
@@ -87,7 +85,6 @@ fn create_bench_state_updates(params: &BenchParams) -> Vec<EvmState> {
.collect(),
status: AccountStatus::Touched,
transaction_id: 0,
..Default::default()
}
};
@@ -233,17 +230,18 @@ fn bench_state_root(c: &mut Criterion) {
let mut handle = payload_processor.spawn(
Default::default(),
(
core::iter::empty::<
Vec::<
Result<
Recovered<TransactionSigned>,
core::convert::Infallible,
>,
>(),
>::new(),
std::convert::identity,
),
StateProviderBuilder::new(provider.clone(), genesis_hash, None),
OverlayStateProviderFactory::new(provider),
&TreeConfig::default(),
None,
);
let mut state_hook = handle.state_hook();

View File

@@ -128,12 +128,12 @@ we send them along with the state updates to the [Sparse Trie Task](#sparse-trie
### Finishing the calculation
Once all transactions are executed, the [Engine](#engine) sends a `StateRootMessage::FinishStateUpdates` message
Once all transactions are executed, the [Engine](#engine) sends a `StateRootMessage::FinishedStateUpdates` message
to the State Root Task, marking the end of receiving state updates.
Every time we receive a new proof from the [MultiProof Manager](#multiproof-manager), we also check
the following conditions:
1. Are all updates received? (`StateRootMessage::FinishStateUpdates` was sent)
1. Are all updates received? (`StateRootMessage::FinishedStateUpdates` was sent)
2. Is `ProofSequencer` empty? (no proofs are pending for sequencing)
3. Are all proofs that were sent to the [`MultiProofManager::spawn_or_queue`](#multiproof-manager) finished
calculating and were sent to the [Sparse Trie Task](#sparse-trie-task)?

View File

@@ -47,7 +47,7 @@ impl BackfillSyncState {
}
/// Backfill sync mode functionality.
pub trait BackfillSync: Send + Sync {
pub trait BackfillSync: Send {
/// Performs a backfill action.
fn on_action(&mut self, action: BackfillAction);

View File

@@ -219,10 +219,19 @@ pub enum HandlerEvent<T> {
}
/// Internal events issued by the [`ChainOrchestrator`].
#[derive(Clone, Debug)]
#[derive(Debug)]
pub enum FromOrchestrator {
/// Invoked when backfill sync finished
BackfillSyncFinished(ControlFlow),
/// Invoked when backfill sync started
BackfillSyncStarted,
/// Gracefully terminate the engine service.
///
/// When this variant is received, the engine will persist all remaining in-memory blocks
/// to disk before shutting down. Once persistence is complete, a signal is sent through
/// the oneshot channel to notify the caller.
Terminate {
/// Channel to signal termination completion.
tx: tokio::sync::oneshot::Sender<()>,
},
}

View File

@@ -19,6 +19,8 @@ pub(crate) struct PersistenceMetrics {
pub(crate) remove_blocks_above_duration_seconds: Histogram,
/// How long it took for blocks to be saved
pub(crate) save_blocks_duration_seconds: Histogram,
/// How many blocks we persist at once.
pub(crate) save_blocks_block_count: Histogram,
/// How long it took for blocks to be pruned
pub(crate) prune_before_duration_seconds: Histogram,
}

View File

@@ -1,5 +1,4 @@
use crate::metrics::PersistenceMetrics;
use alloy_consensus::BlockHeader;
use alloy_eips::BlockNumHash;
use reth_chain_state::ExecutedBlock;
use reth_errors::ProviderError;
@@ -142,27 +141,25 @@ where
&self,
blocks: Vec<ExecutedBlock<N::Primitives>>,
) -> Result<Option<BlockNumHash>, PersistenceError> {
let first_block_hash = blocks.first().map(|b| b.recovered_block.num_hash());
let last_block_hash = blocks.last().map(|b| b.recovered_block.num_hash());
debug!(target: "engine::persistence", first=?first_block_hash, last=?last_block_hash, "Saving range of blocks");
let first_block = blocks.first().map(|b| b.recovered_block.num_hash());
let last_block = blocks.last().map(|b| b.recovered_block.num_hash());
let block_count = blocks.len();
debug!(target: "engine::persistence", ?block_count, first=?first_block, last=?last_block, "Saving range of blocks");
let start_time = Instant::now();
let last_block_hash_num = blocks.last().map(|block| BlockNumHash {
hash: block.recovered_block().hash(),
number: block.recovered_block().header().number(),
});
if last_block_hash_num.is_some() {
if last_block.is_some() {
let provider_rw = self.provider.database_provider_rw()?;
provider_rw.save_blocks(blocks)?;
provider_rw.commit()?;
}
debug!(target: "engine::persistence", first=?first_block_hash, last=?last_block_hash, "Saved range of blocks");
debug!(target: "engine::persistence", first=?first_block, last=?last_block, "Saved range of blocks");
self.metrics.save_blocks_block_count.record(block_count as f64);
self.metrics.save_blocks_duration_seconds.record(start_time.elapsed());
Ok(last_block_hash_num)
Ok(last_block)
}
}

View File

@@ -14,7 +14,7 @@ use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
/// * [`BlockBuffer::remove_old_blocks`] to remove old blocks that precede the finalized number.
///
/// Note: Buffer is limited by number of blocks that it can contain and eviction of the block
/// is done by last recently used block.
/// is done in FIFO order (oldest inserted block is evicted first).
#[derive(Debug)]
pub struct BlockBuffer<B: Block> {
/// All blocks in the buffer stored by their block hash.
@@ -66,9 +66,14 @@ impl<B: Block> BlockBuffer<B> {
pub fn insert_block(&mut self, block: SealedBlock<B>) {
let hash = block.hash();
self.parent_to_child.entry(block.parent_hash()).or_default().insert(hash);
self.earliest_blocks.entry(block.number()).or_default().insert(hash);
self.blocks.insert(hash, block);
match self.blocks.entry(hash) {
std::collections::hash_map::Entry::Occupied(_) => return,
std::collections::hash_map::Entry::Vacant(entry) => {
self.parent_to_child.entry(block.parent_hash()).or_default().insert(hash);
self.earliest_blocks.entry(block.number()).or_default().insert(hash);
entry.insert(block);
}
};
// Add block to FIFO queue and handle eviction if needed
if self.block_queue.len() >= self.max_blocks {

View File

@@ -31,6 +31,9 @@ pub(crate) struct CachedStateProvider<S> {
/// Metrics for the cached state provider
metrics: CachedStateMetrics,
/// If prewarm enabled we populate every cache miss
prewarm: bool,
}
impl<S> CachedStateProvider<S>
@@ -39,12 +42,32 @@ where
{
/// Creates a new [`CachedStateProvider`] from an [`ExecutionCache`], state provider, and
/// [`CachedStateMetrics`].
pub(crate) const fn new_with_caches(
pub(crate) const fn new(
state_provider: S,
caches: ExecutionCache,
metrics: CachedStateMetrics,
) -> Self {
Self { state_provider, caches, metrics }
Self { state_provider, caches, metrics, prewarm: false }
}
}
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(crate) 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
}
}
@@ -123,7 +146,10 @@ impl<S: AccountReader> AccountReader for CachedStateProvider<S> {
self.metrics.account_cache_misses.increment(1);
let res = self.state_provider.basic_account(address)?;
self.caches.account_cache.insert(*address, res);
if self.is_prewarm() {
self.caches.account_cache.insert(*address, res);
}
Ok(res)
}
}
@@ -148,15 +174,19 @@ impl<S: StateProvider> StateProvider for CachedStateProvider<S> {
match self.caches.get_storage(&account, &storage_key) {
(SlotStatus::NotCached, maybe_cache) => {
let final_res = self.state_provider.storage(account, storage_key)?;
let account_cache = maybe_cache.unwrap_or_default();
account_cache.insert_storage(storage_key, final_res);
// we always need to insert the value to update the weights.
// Note: there exists a race when the storage cache did not exist yet and two
// consumers looking up the a storage value for this account for the first time,
// however we can assume that this will only happen for the very first (mostlikely
// the same) value, and don't expect that this will accidentally
// replace an account storage cache with additional values.
self.caches.insert_storage_cache(account, account_cache);
if self.is_prewarm() {
let account_cache = maybe_cache.unwrap_or_default();
account_cache.insert_storage(storage_key, final_res);
// we always need to insert the value to update the weights.
// Note: there exists a race when the storage cache did not exist yet and two
// consumers looking up the a storage value for this account for the first time,
// however we can assume that this will only happen for the very first
// (mostlikely the same) value, and don't expect that this
// will accidentally replace an account storage cache with
// additional values.
self.caches.insert_storage_cache(account, account_cache);
}
self.metrics.storage_cache_misses.increment(1);
Ok(final_res)
@@ -183,7 +213,11 @@ impl<S: BytecodeReader> BytecodeReader for CachedStateProvider<S> {
self.metrics.code_cache_misses.increment(1);
let final_res = self.state_provider.bytecode_by_hash(code_hash)?;
self.caches.code_cache.insert(*code_hash, final_res.clone());
if self.is_prewarm() {
self.caches.code_cache.insert(*code_hash, final_res.clone());
}
Ok(final_res)
}
}
@@ -785,7 +819,7 @@ mod tests {
let caches = ExecutionCacheBuilder::default().build_caches(1000);
let state_provider =
CachedStateProvider::new_with_caches(provider, caches, CachedStateMetrics::zeroed());
CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
// check that the storage is empty
let res = state_provider.storage(address, storage_key);
@@ -808,7 +842,7 @@ mod tests {
let caches = ExecutionCacheBuilder::default().build_caches(1000);
let state_provider =
CachedStateProvider::new_with_caches(provider, caches, CachedStateMetrics::zeroed());
CachedStateProvider::new(provider, caches, CachedStateMetrics::zeroed());
// check that the storage returns the expected value
let res = state_provider.storage(address, storage_key);

View File

@@ -22,7 +22,7 @@ const NANOS_PER_SEC: u32 = 1_000_000_000;
/// An atomic version of [`Duration`], using an [`AtomicU64`] to store the total nanoseconds in the
/// duration.
#[derive(Default)]
#[derive(Debug, Default)]
pub(crate) struct AtomicDuration {
/// The nanoseconds part of the duration
///
@@ -59,7 +59,8 @@ impl AtomicDuration {
}
/// A wrapper of a state provider and latency metrics.
pub(crate) struct InstrumentedStateProvider<S> {
#[derive(Debug)]
pub struct InstrumentedStateProvider<S> {
/// The state provider
state_provider: S,
@@ -80,11 +81,12 @@ impl<S> InstrumentedStateProvider<S>
where
S: StateProvider,
{
/// Creates a new [`InstrumentedStateProvider`] from a state provider
pub(crate) fn from_state_provider(state_provider: S) -> Self {
/// Creates a new [`InstrumentedStateProvider`] from a state provider with the provided label
/// for metrics.
pub fn new(state_provider: S, source: &'static str) -> Self {
Self {
state_provider,
metrics: StateProviderMetrics::default(),
metrics: StateProviderMetrics::new_with_labels(&[("source", source)]),
total_storage_fetch_latency: AtomicDuration::zero(),
total_code_fetch_latency: AtomicDuration::zero(),
total_account_fetch_latency: AtomicDuration::zero(),
@@ -134,6 +136,12 @@ impl<S> InstrumentedStateProvider<S> {
}
}
impl<S> Drop for InstrumentedStateProvider<S> {
fn drop(&mut self) {
self.record_total_latency();
}
}
/// Metrics for the instrumented state provider
#[derive(Metrics, Clone)]
#[metrics(scope = "sync.state_provider")]

View File

@@ -48,6 +48,7 @@ impl InvalidHeaderCache {
// if we get here, the entry has been hit too many times, so we evict it
self.headers.remove(hash);
self.metrics.hit_evictions.increment(1);
self.metrics.count.set(self.headers.len() as f64);
None
}

View File

@@ -64,6 +64,7 @@ impl EngineApiMetrics {
&self,
executor: E,
mut transactions: impl Iterator<Item = Result<impl ExecutableTx<E>, BlockExecutionError>>,
transaction_count: usize,
state_hook: Box<dyn OnStateHook>,
) -> Result<(BlockExecutionOutput<E::Receipt>, Vec<Address>), BlockExecutionError>
where
@@ -75,7 +76,7 @@ impl EngineApiMetrics {
// be accessible.
let wrapper = MeteredStateHook { metrics: self.executor.clone(), inner_hook: state_hook };
let mut senders = Vec::new();
let mut senders = Vec::with_capacity(transaction_count);
let mut executor = executor.with_state_hook(Some(Box::new(wrapper)));
let f = || {
@@ -477,7 +478,6 @@ mod tests {
receipts: vec![],
requests: Requests::default(),
gas_used: 1000,
block_access_list: None,
blob_gas_used: 0,
},
))
@@ -530,6 +530,7 @@ mod tests {
let _result = metrics.execute_metered::<_, EmptyDB>(
executor,
input.clone_transactions_recovered().map(Ok::<_, BlockExecutionError>),
input.transaction_count(),
state_hook,
);
@@ -571,12 +572,10 @@ mod tests {
nonce: 10,
code_hash: B256::random(),
code: Default::default(),
storage_id: None,
},
storage,
status: AccountStatus::default(),
transaction_id: 0,
..Default::default()
},
);
state
@@ -588,6 +587,7 @@ mod tests {
let _result = metrics.execute_metered::<_, EmptyDB>(
executor,
input.clone_transactions_recovered().map(Ok::<_, BlockExecutionError>),
input.transaction_count(),
state_hook,
);

View File

@@ -39,6 +39,7 @@ use revm::state::EvmState;
use state::TreeState;
use std::{
fmt::Debug,
ops,
sync::{
mpsc::{Receiver, RecvError, RecvTimeoutError, Sender},
Arc,
@@ -54,7 +55,7 @@ use tracing::*;
mod block_buffer;
mod cached_state;
pub mod error;
mod instrumented_state;
pub mod instrumented_state;
mod invalid_headers;
mod metrics;
mod payload_processor;
@@ -63,7 +64,6 @@ mod persistence_state;
pub mod precompile_cache;
#[cfg(test)]
mod tests;
// TODO(alexey): compare trie updates in `insert_block_inner`
#[expect(unused)]
mod trie_updates;
@@ -426,9 +426,13 @@ where
match self.try_recv_engine_message() {
Ok(Some(msg)) => {
debug!(target: "engine::tree", %msg, "received new engine message");
if let Err(fatal) = self.on_engine_message(msg) {
error!(target: "engine::tree", %fatal, "insert block fatal error");
return
match self.on_engine_message(msg) {
Ok(ops::ControlFlow::Break(())) => return,
Ok(ops::ControlFlow::Continue(())) => {}
Err(fatal) => {
error!(target: "engine::tree", %fatal, "insert block fatal error");
return
}
}
}
Ok(None) => {
@@ -536,7 +540,7 @@ where
// null}` if the expected and the actual arrays don't match.
//
// This validation **MUST** be instantly run in all cases even during active sync process.
tracing::debug!("Payload received {:?}", payload);
let num_hash = payload.num_hash();
let engine_event = ConsensusEngineEvent::BlockReceived(num_hash);
self.emit_event(EngineApiEvent::BeaconConsensus(engine_event));
@@ -545,7 +549,6 @@ where
// Check for invalid ancestors
if let Some(invalid) = self.find_invalid_ancestor(&payload) {
tracing::debug!(target: "engine::tree", ?invalid, "found invalid ancestor for payload");
let status = self.handle_invalid_ancestor_payload(payload, invalid)?;
return Ok(TreeOutcome::new(status));
}
@@ -554,7 +557,6 @@ where
self.metrics.block_validation.record_payload_validation(start.elapsed().as_secs_f64());
let status = if self.backfill_sync_state.is_idle() {
tracing::debug!(target: "engine::tree", "inserting payload directly");
self.try_insert_payload(payload)?
} else {
self.try_buffer_payload(payload)?
@@ -593,7 +595,7 @@ where
let parent_hash = payload.parent_hash();
let mut latest_valid_hash = None;
match self.insert_payload(payload.clone()) {
match self.insert_payload(payload) {
Ok(status) => {
let status = match status {
InsertPayloadOk::Inserted(BlockStatus::Valid) => {
@@ -615,10 +617,7 @@ where
Ok(PayloadStatus::new(status, latest_valid_hash))
}
Err(error) => match error {
InsertPayloadError::Block(error) => {
tracing::debug!("payload in new payload l 617 {:?}", payload);
Ok(self.on_insert_block_error(error)?)
}
InsertPayloadError::Block(error) => Ok(self.on_insert_block_error(error)?),
InsertPayloadError::Payload(error) => {
Ok(self.on_new_payload_error(error, num_hash, parent_hash)?)
}
@@ -826,7 +825,8 @@ where
new_head_number: u64,
current_head_number: u64,
) -> Vec<ExecutedBlock<N>> {
let mut old_blocks = Vec::new();
let mut old_blocks =
Vec::with_capacity((current_head_number.saturating_sub(new_head_number)) as usize);
for block_num in (new_head_number + 1)..=current_head_number {
if let Some(block_state) = self.canonical_in_memory_state.state_by_number(block_num) {
@@ -931,48 +931,6 @@ where
Ok(())
}
/// Determines if the given block is part of a fork by checking that these
/// conditions are true:
/// * walking back from the target hash to verify that the target hash is not part of an
/// extension of the canonical chain.
/// * walking back from the current head to verify that the target hash is not already part of
/// the canonical chain.
///
/// The header is required as an arg, because we might be checking that the header is a fork
/// block before it's in the tree state and before it's in the database.
fn is_fork(&self, target: BlockWithParent) -> ProviderResult<bool> {
let target_hash = target.block.hash;
// verify that the given hash is not part of an extension of the canon chain.
let canonical_head = self.state.tree_state.canonical_head();
let mut current_hash;
let mut current_block = target;
loop {
if current_block.block.hash == canonical_head.hash {
return Ok(false)
}
// We already passed the canonical head
if current_block.block.number <= canonical_head.number {
break
}
current_hash = current_block.parent;
let Some(next_block) = self.sealed_header_by_hash(current_hash)? else { break };
current_block = next_block.block_with_parent();
}
// verify that the given hash is not already part of canonical chain stored in memory
if self.canonical_in_memory_state.header_by_hash(target_hash).is_some() {
return Ok(false)
}
// verify that the given hash is not already part of persisted canonical chain
if self.provider.block_number(target_hash)?.is_some() {
return Ok(false)
}
Ok(true)
}
/// Invoked when we receive a new forkchoice update message. Calls into the blockchain tree
/// to resolve chain forks and ensure that the Execution Layer is working with the latest valid
/// chain.
@@ -1307,22 +1265,7 @@ where
// Check if persistence has complete
match rx.try_recv() {
Ok(last_persisted_hash_num) => {
self.metrics.engine.persistence_duration.record(start_time.elapsed());
let Some(BlockNumHash {
hash: last_persisted_block_hash,
number: last_persisted_block_number,
}) = last_persisted_hash_num
else {
// if this happened, then we persisted no blocks because we sent an
// empty vec of blocks
warn!(target: "engine::tree", "Persistence task completed but did not persist any blocks");
return Ok(())
};
debug!(target: "engine::tree", ?last_persisted_block_hash, ?last_persisted_block_number, elapsed=?start_time.elapsed(), "Finished persisting, calling finish");
self.persistence_state
.finish(last_persisted_block_hash, last_persisted_block_number);
self.on_new_persisted_block()?;
self.on_persistence_complete(last_persisted_hash_num, start_time)?;
}
Err(TryRecvError::Closed) => return Err(TryRecvError::Closed.into()),
Err(TryRecvError::Empty) => {
@@ -1335,7 +1278,8 @@ where
if let Some(new_tip_num) = self.find_disk_reorg()? {
self.remove_blocks(new_tip_num)
} else if self.should_persist() {
let blocks_to_persist = self.get_canonical_blocks_to_persist()?;
let blocks_to_persist =
self.get_canonical_blocks_to_persist(PersistTarget::Threshold)?;
self.persist_blocks(blocks_to_persist);
}
}
@@ -1343,11 +1287,72 @@ where
Ok(())
}
/// Finishes termination by persisting all remaining blocks and signaling completion.
///
/// This blocks until all persistence is complete. Always signals completion,
/// even if an error occurs.
fn finish_termination(
&mut self,
pending_termination: oneshot::Sender<()>,
) -> Result<(), AdvancePersistenceError> {
trace!(target: "engine::tree", "finishing termination, persisting remaining blocks");
let result = self.persist_until_complete();
let _ = pending_termination.send(());
result
}
/// Persists all remaining blocks until none are left.
fn persist_until_complete(&mut self) -> Result<(), AdvancePersistenceError> {
loop {
// Wait for any in-progress persistence to complete (blocking)
if let Some((rx, start_time, _action)) = self.persistence_state.rx.take() {
let result = rx.blocking_recv().map_err(|_| TryRecvError::Closed)?;
self.on_persistence_complete(result, start_time)?;
}
let blocks_to_persist = self.get_canonical_blocks_to_persist(PersistTarget::Head)?;
if blocks_to_persist.is_empty() {
debug!(target: "engine::tree", "persistence complete, signaling termination");
return Ok(())
}
debug!(target: "engine::tree", count = blocks_to_persist.len(), "persisting remaining blocks before shutdown");
self.persist_blocks(blocks_to_persist);
}
}
/// Handles a completed persistence task.
fn on_persistence_complete(
&mut self,
last_persisted_hash_num: Option<BlockNumHash>,
start_time: Instant,
) -> Result<(), AdvancePersistenceError> {
self.metrics.engine.persistence_duration.record(start_time.elapsed());
let Some(BlockNumHash {
hash: last_persisted_block_hash,
number: last_persisted_block_number,
}) = last_persisted_hash_num
else {
// if this happened, then we persisted no blocks because we sent an empty vec of blocks
warn!(target: "engine::tree", "Persistence task completed but did not persist any blocks");
return Ok(())
};
debug!(target: "engine::tree", ?last_persisted_block_hash, ?last_persisted_block_number, elapsed=?start_time.elapsed(), "Finished persisting, calling finish");
self.persistence_state.finish(last_persisted_block_hash, last_persisted_block_number);
self.on_new_persisted_block()?;
Ok(())
}
/// Handles a message from the engine.
///
/// Returns `ControlFlow::Break(())` if the engine should terminate.
fn on_engine_message(
&mut self,
msg: FromEngine<EngineApiRequest<T, N>, N::Block>,
) -> Result<(), InsertBlockFatalError> {
) -> Result<ops::ControlFlow<()>, InsertBlockFatalError> {
match msg {
FromEngine::Event(event) => match event {
FromOrchestrator::BackfillSyncStarted => {
@@ -1357,6 +1362,13 @@ where
FromOrchestrator::BackfillSyncFinished(ctrl) => {
self.on_backfill_sync_finished(ctrl)?;
}
FromOrchestrator::Terminate { tx } => {
debug!(target: "engine::tree", "received terminate request");
if let Err(err) = self.finish_termination(tx) {
error!(target: "engine::tree", %err, "Termination failed");
}
return Ok(ops::ControlFlow::Break(()))
}
},
FromEngine::Request(request) => {
match request {
@@ -1364,7 +1376,7 @@ where
let block_num_hash = block.recovered_block().num_hash();
if block_num_hash.number <= self.state.tree_state.canonical_block_number() {
// outdated block that can be skipped
return Ok(())
return Ok(ops::ControlFlow::Continue(()))
}
debug!(target: "engine::tree", block=?block_num_hash, "inserting already executed block");
@@ -1472,7 +1484,7 @@ where
}
}
}
Ok(())
Ok(ops::ControlFlow::Continue(()))
}
/// Invoked if the backfill sync has finished to target.
@@ -1706,10 +1718,10 @@ where
}
/// Returns a batch of consecutive canonical blocks to persist in the range
/// `(last_persisted_number .. canonical_head - threshold]`. The expected
/// order is oldest -> newest.
/// `(last_persisted_number .. target]`. The expected order is oldest -> newest.
fn get_canonical_blocks_to_persist(
&self,
target: PersistTarget,
) -> Result<Vec<ExecutedBlock<N>>, AdvancePersistenceError> {
// We will calculate the state root using the database, so we need to be sure there are no
// changes
@@ -1720,9 +1732,12 @@ where
let last_persisted_number = self.persistence_state.last_persisted_block.number;
let canonical_head_number = self.state.tree_state.canonical_block_number();
// Persist only up to block buffer target
let target_number =
canonical_head_number.saturating_sub(self.config.memory_block_buffer_target());
let target_number = match target {
PersistTarget::Head => canonical_head_number,
PersistTarget::Threshold => {
canonical_head_number.saturating_sub(self.config.memory_block_buffer_target())
}
};
debug!(
target: "engine::tree",
@@ -2512,14 +2527,11 @@ where
Ok(Some(_)) => {}
}
// determine whether we are on a fork chain
let is_fork = match self.is_fork(block_id) {
Err(err) => {
let block = convert_to_block(self, input)?;
return Err(InsertBlockError::new(block, err.into()).into());
}
Ok(is_fork) => is_fork,
};
// determine whether we are on a fork chain by comparing the block number with the
// canonical head. This is a simple check that is sufficient for the event emission below.
// A block is considered a fork if its number is less than or equal to the canonical head,
// as this indicates there's already a canonical block at that height.
let is_fork = block_id.block.number <= self.state.tree_state.current_canonical_head.number;
let ctx = TreeCtx::new(&mut self.state, &self.canonical_in_memory_state);
@@ -2865,3 +2877,12 @@ pub enum InsertPayloadOk {
/// The payload was valid and inserted into the tree.
Inserted(BlockStatus),
}
/// Target for block persistence.
#[derive(Debug, Clone, Copy)]
enum PersistTarget {
/// Persist up to `canonical_head - memory_block_buffer_target`.
Threshold,
/// Persist all blocks up to and including the canonical head.
Head,
}

View File

@@ -0,0 +1,541 @@
//! BAL (Block Access List, EIP-7928) related functionality.
use alloy_consensus::constants::KECCAK_EMPTY;
use alloy_eip7928::BlockAccessList;
use alloy_primitives::{keccak256, Address, StorageKey, U256};
use reth_primitives_traits::Account;
use reth_provider::{AccountReader, ProviderError};
use reth_trie::{HashedPostState, HashedStorage};
use std::ops::Range;
/// Returns the total number of storage slots (both changed and read-only) across all accounts in
/// the BAL.
pub fn total_slots(bal: &BlockAccessList) -> usize {
bal.iter().map(|account| account.storage_changes.len() + account.storage_reads.len()).sum()
}
/// Iterator over storage slots in a [`BlockAccessList`], with range-based filtering.
///
/// Iterates over all `(Address, StorageKey)` pairs representing both changed and read-only
/// storage slots across all accounts in the BAL. For each account, changed slots are iterated
/// first, followed by read-only slots. The iterator intelligently skips accounts and slots
/// outside the specified range for efficient traversal.
#[derive(Debug)]
pub(crate) struct BALSlotIter<'a> {
bal: &'a BlockAccessList,
range: Range<usize>,
current_index: usize,
account_idx: usize,
/// Index within the current account's combined slots (changed + read-only).
/// If `slot_idx < storage_changes.len()`, we're in changed slots.
/// Otherwise, we're in read-only slots at index `slot_idx - storage_changes.len()`.
slot_idx: usize,
}
impl<'a> BALSlotIter<'a> {
/// Creates a new iterator over storage slots within the specified range.
pub(crate) fn new(bal: &'a BlockAccessList, range: Range<usize>) -> Self {
let mut iter = Self { bal, range, current_index: 0, account_idx: 0, slot_idx: 0 };
iter.skip_to_range_start();
iter
}
/// Skips to the first item within the range.
fn skip_to_range_start(&mut self) {
while self.account_idx < self.bal.len() {
let account = &self.bal[self.account_idx];
let slots_in_account = account.storage_changes.len() + account.storage_reads.len();
// Check if this account contains items in our range
let account_end = self.current_index + slots_in_account;
if account_end <= self.range.start {
// Entire account is before range, skip it
self.current_index = account_end;
self.account_idx += 1;
self.slot_idx = 0;
} else if self.current_index < self.range.start {
// Range starts somewhere in this account
let skip_slots = self.range.start - self.current_index;
self.slot_idx = skip_slots;
self.current_index = self.range.start;
break;
} else {
// We're at or past range start
break;
}
}
}
}
impl<'a> Iterator for BALSlotIter<'a> {
type Item = (Address, StorageKey);
fn next(&mut self) -> Option<Self::Item> {
// Check if we've exceeded the range
if self.current_index >= self.range.end {
return None;
}
// Find the next valid slot
while self.account_idx < self.bal.len() {
let account = &self.bal[self.account_idx];
let changed_len = account.storage_changes.len();
let total_len = changed_len + account.storage_reads.len();
if self.slot_idx < total_len {
let address = account.address;
let slot = if self.slot_idx < changed_len {
// We're in changed slots
account.storage_changes[self.slot_idx].slot
} else {
// We're in read-only slots
account.storage_reads[self.slot_idx - changed_len]
};
self.slot_idx += 1;
self.current_index += 1;
// Check if we've reached the end of range
if self.current_index > self.range.end {
return None;
}
return Some((address, slot));
}
// Move to next account
self.account_idx += 1;
self.slot_idx = 0;
}
None
}
}
/// Converts a Block Access List into a [`HashedPostState`] by extracting the final state
/// of modified accounts and storage slots.
pub(crate) fn bal_to_hashed_post_state<P>(
bal: &BlockAccessList,
provider: P,
) -> Result<HashedPostState, ProviderError>
where
P: AccountReader,
{
let mut hashed_state = HashedPostState::with_capacity(bal.len());
for account_changes in bal {
let address = account_changes.address;
// Always fetch the account; even if we don't need the db account to construct the final
// `Account`, doing this fills the cache.
let existing_account = provider.basic_account(&address)?;
// Get the latest balance (last balance change if any)
let balance = account_changes.balance_changes.last().map(|change| change.post_balance);
// Get the latest nonce (last nonce change if any)
let nonce = account_changes.nonce_changes.last().map(|change| change.new_nonce);
// Get the latest code (last code change if any)
let code_hash = if let Some(code_change) = account_changes.code_changes.last() {
if code_change.new_code.is_empty() {
Some(Some(KECCAK_EMPTY))
} else {
Some(Some(keccak256(&code_change.new_code)))
}
} else {
None
};
// If the account was only read then don't add it to the HashedPostState
if balance.is_none() &&
nonce.is_none() &&
code_hash.is_none() &&
account_changes.storage_changes.is_empty()
{
continue
}
// Build the final account state
let account = Account {
balance: balance.unwrap_or_else(|| {
existing_account.as_ref().map(|acc| acc.balance).unwrap_or(U256::ZERO)
}),
nonce: nonce
.unwrap_or_else(|| existing_account.as_ref().map(|acc| acc.nonce).unwrap_or(0)),
bytecode_hash: code_hash.unwrap_or_else(|| {
existing_account.as_ref().and_then(|acc| acc.bytecode_hash).or(Some(KECCAK_EMPTY))
}),
};
let hashed_address = keccak256(address);
hashed_state.accounts.insert(hashed_address, Some(account));
// Process storage changes
if !account_changes.storage_changes.is_empty() {
let mut storage_map = HashedStorage::new(false);
for slot_changes in &account_changes.storage_changes {
let hashed_slot = keccak256(slot_changes.slot);
// Get the last change for this slot
if let Some(last_change) = slot_changes.changes.last() {
storage_map
.storage
.insert(hashed_slot, U256::from_be_bytes(last_change.new_value.0));
}
}
hashed_state.storages.insert(hashed_address, storage_map);
}
}
Ok(hashed_state)
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_eip7928::{
AccountChanges, BalanceChange, CodeChange, NonceChange, SlotChanges, StorageChange,
};
use alloy_primitives::{Address, Bytes, StorageKey, B256};
use reth_revm::test_utils::StateProviderTest;
#[test]
fn test_bal_to_hashed_post_state_basic() {
let provider = StateProviderTest::default();
let address = Address::random();
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(100))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
assert_eq!(result.accounts.len(), 1);
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
assert!(account_opt.is_some());
let account = account_opt.as_ref().unwrap();
assert_eq!(account.balance, U256::from(100));
assert_eq!(account.nonce, 1);
assert_eq!(account.bytecode_hash, Some(KECCAK_EMPTY));
}
#[test]
fn test_bal_with_storage_changes() {
let provider = StateProviderTest::default();
let address = Address::random();
let slot = StorageKey::random();
let value = B256::random();
let slot_changes = SlotChanges { slot, changes: vec![StorageChange::new(0, value)] };
let account_changes = AccountChanges {
address,
storage_changes: vec![slot_changes],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(500))],
nonce_changes: vec![NonceChange::new(0, 2)],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
assert!(result.storages.contains_key(&hashed_address));
let storage = result.storages.get(&hashed_address).unwrap();
let hashed_slot = keccak256(slot);
let stored_value = storage.storage.get(&hashed_slot).unwrap();
assert_eq!(*stored_value, U256::from_be_bytes(value.0));
}
#[test]
fn test_bal_with_code_change() {
let provider = StateProviderTest::default();
let address = Address::random();
let code = Bytes::from(vec![0x60, 0x80, 0x60, 0x40]); // Some bytecode
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(1000))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![CodeChange::new(0, code.clone())],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
let expected_code_hash = keccak256(&code);
assert_eq!(account.bytecode_hash, Some(expected_code_hash));
}
#[test]
fn test_bal_with_empty_code() {
let provider = StateProviderTest::default();
let address = Address::random();
let empty_code = Bytes::default();
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(1000))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![CodeChange::new(0, empty_code)],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
assert_eq!(account.bytecode_hash, Some(KECCAK_EMPTY));
}
#[test]
fn test_bal_multiple_changes_takes_last() {
let provider = StateProviderTest::default();
let address = Address::random();
// Multiple balance changes - should take the last one
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![
BalanceChange::new(0, U256::from(100)),
BalanceChange::new(1, U256::from(200)),
BalanceChange::new(2, U256::from(300)),
],
nonce_changes: vec![
NonceChange::new(0, 1),
NonceChange::new(1, 2),
NonceChange::new(2, 3),
],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
// Should have the last values
assert_eq!(account.balance, U256::from(300));
assert_eq!(account.nonce, 3);
}
#[test]
fn test_bal_uses_provider_for_missing_fields() {
let mut provider = StateProviderTest::default();
let address = Address::random();
let code_hash = B256::random();
let existing_account =
Account { balance: U256::from(999), nonce: 42, bytecode_hash: Some(code_hash) };
provider.insert_account(address, existing_account, None, Default::default());
// Only change balance, nonce and code should come from provider
let account_changes = AccountChanges {
address,
storage_changes: vec![],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(1500))],
nonce_changes: vec![],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let account_opt = result.accounts.get(&hashed_address).unwrap();
let account = account_opt.as_ref().unwrap();
// Balance should be updated
assert_eq!(account.balance, U256::from(1500));
// Nonce and bytecode_hash should come from provider
assert_eq!(account.nonce, 42);
assert_eq!(account.bytecode_hash, Some(code_hash));
}
#[test]
fn test_bal_multiple_storage_changes_per_slot() {
let provider = StateProviderTest::default();
let address = Address::random();
let slot = StorageKey::random();
// Multiple changes to the same slot - should take the last one
let slot_changes = SlotChanges {
slot,
changes: vec![
StorageChange::new(0, B256::from(U256::from(100).to_be_bytes::<32>())),
StorageChange::new(1, B256::from(U256::from(200).to_be_bytes::<32>())),
StorageChange::new(2, B256::from(U256::from(300).to_be_bytes::<32>())),
],
};
let account_changes = AccountChanges {
address,
storage_changes: vec![slot_changes],
storage_reads: vec![],
balance_changes: vec![BalanceChange::new(0, U256::from(100))],
nonce_changes: vec![NonceChange::new(0, 1)],
code_changes: vec![],
};
let bal = vec![account_changes];
let result = bal_to_hashed_post_state(&bal, &provider).unwrap();
let hashed_address = keccak256(address);
let storage = result.storages.get(&hashed_address).unwrap();
let hashed_slot = keccak256(slot);
let stored_value = storage.storage.get(&hashed_slot).unwrap();
// Should have the last value
assert_eq!(*stored_value, U256::from(300));
}
#[test]
fn test_bal_slot_iter() {
// Create test data with multiple accounts and slots (both changed and read-only)
let addr1 = Address::repeat_byte(0x01);
let addr2 = Address::repeat_byte(0x02);
let addr3 = Address::repeat_byte(0x03);
// Account 1: 2 changed slots + 1 read-only = 3 total slots (indices 0, 1, 2)
let account1 = AccountChanges {
address: addr1,
storage_changes: vec![
SlotChanges {
slot: StorageKey::from(U256::from(100)),
changes: vec![StorageChange::new(0, B256::ZERO)],
},
SlotChanges {
slot: StorageKey::from(U256::from(101)),
changes: vec![StorageChange::new(0, B256::ZERO)],
},
],
storage_reads: vec![StorageKey::from(U256::from(102))],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
// Account 2: 1 changed slot + 1 read-only = 2 total slots (indices 3, 4)
let account2 = AccountChanges {
address: addr2,
storage_changes: vec![SlotChanges {
slot: StorageKey::from(U256::from(200)),
changes: vec![StorageChange::new(0, B256::ZERO)],
}],
storage_reads: vec![StorageKey::from(U256::from(201))],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
// Account 3: 2 changed slots + 1 read-only = 3 total slots (indices 5, 6, 7)
let account3 = AccountChanges {
address: addr3,
storage_changes: vec![
SlotChanges {
slot: StorageKey::from(U256::from(300)),
changes: vec![StorageChange::new(0, B256::ZERO)],
},
SlotChanges {
slot: StorageKey::from(U256::from(301)),
changes: vec![StorageChange::new(0, B256::ZERO)],
},
],
storage_reads: vec![StorageKey::from(U256::from(302))],
balance_changes: vec![],
nonce_changes: vec![],
code_changes: vec![],
};
let bal = vec![account1, account2, account3];
// Test 1: Iterate over all slots (range 0..8)
let items: Vec<_> = BALSlotIter::new(&bal, 0..8).collect();
assert_eq!(items.len(), 8);
// Account 1: changed slots first (100, 101), then read-only (102)
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(100))));
assert_eq!(items[1], (addr1, StorageKey::from(U256::from(101))));
assert_eq!(items[2], (addr1, StorageKey::from(U256::from(102))));
// Account 2: changed slot (200), then read-only (201)
assert_eq!(items[3], (addr2, StorageKey::from(U256::from(200))));
assert_eq!(items[4], (addr2, StorageKey::from(U256::from(201))));
// Account 3: changed slots (300, 301), then read-only (302)
assert_eq!(items[5], (addr3, StorageKey::from(U256::from(300))));
assert_eq!(items[6], (addr3, StorageKey::from(U256::from(301))));
assert_eq!(items[7], (addr3, StorageKey::from(U256::from(302))));
// Test 2: Range that skips first account (range 3..6)
let items: Vec<_> = BALSlotIter::new(&bal, 3..6).collect();
assert_eq!(items.len(), 3);
assert_eq!(items[0], (addr2, StorageKey::from(U256::from(200))));
assert_eq!(items[1], (addr2, StorageKey::from(U256::from(201))));
assert_eq!(items[2], (addr3, StorageKey::from(U256::from(300))));
// Test 3: Range within first account (range 1..2)
let items: Vec<_> = BALSlotIter::new(&bal, 1..2).collect();
assert_eq!(items.len(), 1);
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(101))));
// Test 4: Range spanning multiple accounts (range 2..5)
let items: Vec<_> = BALSlotIter::new(&bal, 2..5).collect();
assert_eq!(items.len(), 3);
// Last slot from account 1 (read-only)
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(102))));
// Account 2 (changed + read-only)
assert_eq!(items[1], (addr2, StorageKey::from(U256::from(200))));
assert_eq!(items[2], (addr2, StorageKey::from(U256::from(201))));
// Test 5: Empty range
let items: Vec<_> = BALSlotIter::new(&bal, 5..5).collect();
assert_eq!(items.len(), 0);
// Test 6: Range beyond end (starts at index 6)
let items: Vec<_> = BALSlotIter::new(&bal, 6..100).collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0], (addr3, StorageKey::from(U256::from(301))));
assert_eq!(items[1], (addr3, StorageKey::from(U256::from(302))));
// Test 7: Range that starts in read-only slots (index 2 is the read-only slot of account 1)
let items: Vec<_> = BALSlotIter::new(&bal, 2..4).collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0], (addr1, StorageKey::from(U256::from(102))));
assert_eq!(items[1], (addr2, StorageKey::from(U256::from(200))));
}
}

View File

@@ -3,16 +3,17 @@
use super::precompile_cache::PrecompileCacheMap;
use crate::tree::{
cached_state::{
CachedStateMetrics, ExecutionCache as StateExecutionCache, ExecutionCacheBuilder,
SavedCache,
CachedStateMetrics, CachedStateProvider, ExecutionCache as StateExecutionCache,
ExecutionCacheBuilder, SavedCache,
},
payload_processor::{
prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmTaskEvent},
prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmMode, PrewarmTaskEvent},
sparse_trie::StateRootComputeOutcome,
},
sparse_trie::SparseTrieTask,
StateProviderBuilder, TreeConfig,
};
use alloy_eip7928::BlockAccessList;
use alloy_eips::eip1898::BlockWithParent;
use alloy_evm::{block::StateChangeSource, ToTxEnv};
use alloy_primitives::B256;
@@ -21,14 +22,17 @@ use executor::WorkloadExecutor;
use multiproof::{SparseTrieUpdate, *};
use parking_lot::RwLock;
use prewarm::PrewarmMetrics;
use rayon::iter::{ParallelBridge, ParallelIterator};
use reth_engine_primitives::ExecutableTxIterator;
use rayon::prelude::*;
use reth_evm::{
execute::{ExecutableTxFor, WithTxEnv},
ConfigureEvm, EvmEnvFor, OnStateHook, SpecFor, TxEnvFor,
ConfigureEvm, EvmEnvFor, ExecutableTxIterator, ExecutableTxTuple, OnStateHook, SpecFor,
TxEnvFor,
};
use reth_execution_types::ExecutionOutcome;
use reth_primitives_traits::NodePrimitives;
use reth_provider::{BlockReader, DatabaseProviderROFactory, StateProviderFactory, StateReader};
use reth_provider::{
BlockReader, DatabaseProviderROFactory, StateProvider, StateProviderFactory, StateReader,
};
use reth_revm::{db::BundleState, state::EvmState};
use reth_trie::{hashed_cursor::HashedCursorFactory, trie_cursor::TrieCursorFactory};
use reth_trie_parallel::{
@@ -42,6 +46,7 @@ use reth_trie_sparse::{
use reth_trie_sparse_parallel::{ParallelSparseTrie, ParallelismThresholds};
use std::{
collections::BTreeMap,
ops::Not,
sync::{
atomic::AtomicBool,
mpsc::{self, channel},
@@ -51,6 +56,7 @@ use std::{
};
use tracing::{debug, debug_span, instrument, warn, Span};
pub mod bal;
mod configured_sparse_trie;
pub mod executor;
pub mod multiproof;
@@ -90,6 +96,13 @@ 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;
/// Type alias for [`PayloadHandle`] returned by payload processor spawn methods.
type IteratorPayloadHandle<Evm, I, N> = PayloadHandle<
WithTxEnv<TxEnvFor<Evm>, <I as ExecutableTxTuple>::Tx>,
<I as ExecutableTxTuple>::Error,
<N as NodePrimitives>::Receipt,
>;
/// Entrypoint for executing the payload.
#[derive(Debug)]
pub struct PayloadProcessor<Evm>
@@ -106,6 +119,8 @@ where
cross_block_cache_size: u64,
/// Whether transactions should not be executed on prewarming task.
disable_transaction_prewarming: bool,
/// Whether state cache should be disable
disable_state_cache: bool,
/// Determines how to configure the evm for execution.
evm_config: Evm,
/// Whether precompile cache should be disabled.
@@ -149,6 +164,7 @@ where
cross_block_cache_size: config.cross_block_cache_size(),
disable_transaction_prewarming: config.disable_prewarming(),
evm_config,
disable_state_cache: config.disable_state_cache(),
precompile_cache_disabled: config.precompile_cache_disabled(),
precompile_cache_map,
sparse_state_trie: Arc::default(),
@@ -195,7 +211,6 @@ where
///
/// This returns a handle to await the final state root and to interact with the tasks (e.g.
/// canceling)
#[allow(clippy::type_complexity)]
#[instrument(
level = "debug",
target = "engine::tree::payload_processor",
@@ -209,7 +224,8 @@ where
provider_builder: StateProviderBuilder<N, P>,
multiproof_provider_factory: F,
config: &TreeConfig,
) -> PayloadHandle<WithTxEnv<TxEnvFor<Evm>, I::Tx>, I::Error>
bal: Option<Arc<BlockAccessList>>,
) -> IteratorPayloadHandle<Evm, I, N>
where
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
F: DatabaseProviderROFactory<Provider: TrieCursorFactory + HashedCursorFactory>
@@ -223,11 +239,36 @@ where
let span = Span::current();
let (to_sparse_trie, sparse_trie_rx) = channel();
let (to_multi_proof, from_multi_proof) = crossbeam_channel::unbounded();
// We rely on the cursor factory to provide whatever DB overlay is necessary to see a
// consistent view of the database, including the trie tables. Because of this there is no
// need for an overarching prefix set to invalidate any section of the trie tables, and so
// we use an empty prefix set.
// Handle BAL-based optimization if available
let prewarm_handle = if let Some(bal) = bal {
// When BAL is present, use BAL prewarming and send BAL to multiproof
debug!(target: "engine::tree::payload_processor", "BAL present, using BAL prewarming");
// 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,
transaction_count_hint,
provider_builder.clone(),
None, // Don't send proof targets when BAL is present
Some(bal),
)
} else {
// Normal path: spawn with transaction prewarming
self.spawn_caching_with(
env,
prewarm_rx,
transaction_count_hint,
provider_builder.clone(),
Some(to_multi_proof.clone()),
None,
)
};
// Create and spawn the storage proof task
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
@@ -244,24 +285,28 @@ where
proof_handle.clone(),
to_sparse_trie,
config.multiproof_chunking_enabled().then_some(config.multiproof_chunk_size()),
to_multi_proof,
from_multi_proof,
);
// wire the multiproof task to the prewarm task
let to_multi_proof = Some(multi_proof_task.state_root_message_sender());
let prewarm_handle = self.spawn_caching_with(
env,
prewarm_rx,
transaction_count_hint,
provider_builder,
to_multi_proof.clone(),
);
// spawn multi-proof task
let parent_span = span.clone();
let saved_cache = prewarm_handle.saved_cache.clone();
self.executor.spawn_blocking(move || {
let _enter = parent_span.entered();
multi_proof_task.run();
// Build a state provider for the multiproof task
let provider = provider_builder.build().expect("failed to build provider");
let provider = if let Some(saved_cache) = saved_cache {
let (cache, metrics) = saved_cache.split();
Box::new(CachedStateProvider::new(provider, cache, metrics))
as Box<dyn StateProvider>
} else {
Box::new(provider)
};
multi_proof_task.run(provider);
});
// wire the sparse trie to the state root response receiver
@@ -288,13 +333,14 @@ where
env: ExecutionEnv<Evm>,
transactions: I,
provider_builder: StateProviderBuilder<N, P>,
) -> PayloadHandle<WithTxEnv<TxEnvFor<Evm>, I::Tx>, I::Error>
bal: Option<Arc<BlockAccessList>>,
) -> IteratorPayloadHandle<Evm, I, N>
where
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
{
let (prewarm_rx, execution_rx, size_hint) = self.spawn_tx_iterator(transactions);
let prewarm_handle =
self.spawn_caching_with(env, prewarm_rx, size_hint, provider_builder, None);
self.spawn_caching_with(env, prewarm_rx, size_hint, provider_builder, None, bal);
PayloadHandle {
to_multi_proof: None,
prewarm_handle,
@@ -315,36 +361,32 @@ where
usize,
) {
let (transactions, convert) = transactions.into();
let transactions = transactions.into_iter();
// Get the transaction count for prewarming task
// Use upper bound if available (more accurate), otherwise use lower bound
let (lower, upper) = transactions.size_hint();
let transaction_count_hint = upper.unwrap_or(lower);
let transactions = transactions.into_par_iter();
let transaction_count_hint = transactions.len();
// Spawn a task that iterates through all transactions in parallel and sends them to the
// main task.
let (tx, rx) = mpsc::channel();
let (ooo_tx, ooo_rx) = mpsc::channel();
let (prewarm_tx, prewarm_rx) = mpsc::channel();
let (execute_tx, execute_rx) = mpsc::channel();
// Spawn a task that `convert`s all transactions in parallel and sends them out-of-order.
self.executor.spawn_blocking(move || {
transactions.enumerate().par_bridge().for_each_with(tx, |sender, (idx, tx)| {
transactions.enumerate().for_each_with(ooo_tx, |ooo_tx, (idx, tx)| {
let tx = convert(tx);
let tx = tx.map(|tx| WithTxEnv { tx_env: tx.to_tx_env(), tx: Arc::new(tx) });
let _ = sender.send((idx, 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 prewarming and execution tasks.
let (prewarm_tx, prewarm_rx) = mpsc::channel();
let (execute_tx, execute_rx) = mpsc::channel();
// to the execution task in order.
self.executor.spawn_blocking(move || {
let mut next_for_execution = 0;
let mut queue = BTreeMap::new();
while let Ok((idx, tx)) = rx.recv() {
// only send Ok(_) variants to prewarming task
if let Ok(tx) = &tx {
let _ = prewarm_tx.send(tx.clone());
}
while let Ok((idx, tx)) = ooo_rx.recv() {
if next_for_execution == idx {
let _ = execute_tx.send(tx);
next_for_execution += 1;
@@ -372,7 +414,8 @@ where
transaction_count_hint: usize,
provider_builder: StateProviderBuilder<N, P>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
) -> CacheTaskHandle
bal: Option<Arc<BlockAccessList>>,
) -> CacheTaskHandle<N::Receipt>
where
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
{
@@ -382,14 +425,13 @@ where
transactions = mpsc::channel().1;
}
let saved_cache = self.cache_for(env.parent_hash);
let cache = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
let saved_cache = self.disable_state_cache.not().then(|| self.cache_for(env.parent_hash));
// configure prewarming
let prewarm_ctx = PrewarmContext {
env,
evm_config: self.evm_config.clone(),
saved_cache,
saved_cache: saved_cache.clone(),
provider: provider_builder,
metrics: PrewarmMetrics::default(),
terminate_execution: Arc::new(AtomicBool::new(false)),
@@ -410,11 +452,16 @@ where
{
let to_prewarm_task = to_prewarm_task.clone();
self.executor.spawn_blocking(move || {
prewarm_task.run(transactions, to_prewarm_task);
let mode = if let Some(bal) = bal {
PrewarmMode::BlockAccessList(bal)
} else {
PrewarmMode::Transactions(transactions)
};
prewarm_task.run(mode, to_prewarm_task);
});
}
CacheTaskHandle { cache, to_prewarm_task: Some(to_prewarm_task), cache_metrics }
CacheTaskHandle { saved_cache, to_prewarm_task: Some(to_prewarm_task) }
}
/// Returns the cache for the given parent hash.
@@ -547,12 +594,15 @@ where
}
/// Handle to all the spawned tasks.
///
/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the
/// caching task without cloning the expensive `BundleState`.
#[derive(Debug)]
pub struct PayloadHandle<Tx, Err> {
pub struct PayloadHandle<Tx, Err, R> {
/// Channel for evm state updates
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
// must include the receiver of the state root wired to the sparse trie
prewarm_handle: CacheTaskHandle,
prewarm_handle: CacheTaskHandle<R>,
/// Stream of block transactions
transactions: mpsc::Receiver<Result<Tx, Err>>,
/// Receiver for the state root
@@ -561,7 +611,7 @@ pub struct PayloadHandle<Tx, Err> {
_span: Span,
}
impl<Tx, Err> PayloadHandle<Tx, Err> {
impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
/// Awaits the state root
///
/// # Panics
@@ -590,19 +640,19 @@ impl<Tx, Err> PayloadHandle<Tx, Err> {
move |source: StateChangeSource, state: &EvmState| {
if let Some(sender) = &to_multi_proof {
let _ = sender.send(MultiProofMessage::StateUpdate(source, state.clone()));
let _ = sender.send(MultiProofMessage::StateUpdate(source.into(), state.clone()));
}
}
}
/// Returns a clone of the caches used by prewarming
pub(super) fn caches(&self) -> StateExecutionCache {
self.prewarm_handle.cache.clone()
pub(super) fn caches(&self) -> Option<StateExecutionCache> {
self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.cache().clone())
}
/// Returns a clone of the cache metrics used by prewarming
pub(super) fn cache_metrics(&self) -> CachedStateMetrics {
self.prewarm_handle.cache_metrics.clone()
pub(super) fn cache_metrics(&self) -> Option<CachedStateMetrics> {
self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.metrics().clone())
}
/// Terminates the pre-warming transaction processing.
@@ -614,9 +664,14 @@ impl<Tx, Err> PayloadHandle<Tx, Err> {
/// Terminates the entire caching task.
///
/// If the [`BundleState`] is provided it will update the shared cache.
pub(super) fn terminate_caching(&mut self, block_output: Option<&BundleState>) {
self.prewarm_handle.terminate_caching(block_output)
/// If the [`ExecutionOutcome`] is provided it will update the shared cache using its
/// bundle state. Using `Arc<ExecutionOutcome>` allows sharing with the main execution
/// path without cloning the expensive `BundleState`.
pub(super) fn terminate_caching(
&mut self,
execution_outcome: Option<Arc<ExecutionOutcome<R>>>,
) {
self.prewarm_handle.terminate_caching(execution_outcome)
}
/// Returns iterator yielding transactions from the stream.
@@ -628,17 +683,18 @@ impl<Tx, Err> PayloadHandle<Tx, Err> {
}
/// Access to the spawned [`PrewarmCacheTask`].
///
/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the
/// prewarm task without cloning the expensive `BundleState`.
#[derive(Debug)]
pub(crate) struct CacheTaskHandle {
pub(crate) struct CacheTaskHandle<R> {
/// The shared cache the task operates with.
cache: StateExecutionCache,
/// Metrics for the caches
cache_metrics: CachedStateMetrics,
saved_cache: Option<SavedCache>,
/// Channel to the spawned prewarm task if any
to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent>>,
to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<R>>>,
}
impl CacheTaskHandle {
impl<R: Send + Sync + 'static> CacheTaskHandle<R> {
/// Terminates the pre-warming transaction processing.
///
/// Note: This does not terminate the task yet.
@@ -650,20 +706,25 @@ impl CacheTaskHandle {
/// Terminates the entire pre-warming task.
///
/// If the [`BundleState`] is provided it will update the shared cache.
pub(super) fn terminate_caching(&mut self, block_output: Option<&BundleState>) {
/// If the [`ExecutionOutcome`] is provided it will update the shared cache using its
/// bundle state. Using `Arc<ExecutionOutcome>` avoids cloning the expensive `BundleState`.
pub(super) fn terminate_caching(
&mut self,
execution_outcome: Option<Arc<ExecutionOutcome<R>>>,
) {
if let Some(tx) = self.to_prewarm_task.take() {
// Only clone when we have an active task and a state to send
let event = PrewarmTaskEvent::Terminate { block_output: block_output.cloned() };
let event = PrewarmTaskEvent::Terminate { execution_outcome };
let _ = tx.send(event);
}
}
}
impl Drop for CacheTaskHandle {
impl<R> Drop for CacheTaskHandle<R> {
fn drop(&mut self) {
// Ensure we always terminate on drop
self.terminate_caching(None);
// Ensure we always terminate on drop - send None without needing Send + Sync bounds
if let Some(tx) = self.to_prewarm_task.take() {
let _ = tx.send(PrewarmTaskEvent::Terminate { execution_outcome: None });
}
}
}
@@ -716,6 +777,8 @@ impl ExecutionCache {
cache
.as_ref()
// Check `is_available()` to ensure no other tasks (e.g., prewarming) currently hold
// a reference to this cache. We can only reuse it when we have exclusive access.
.filter(|c| c.executed_block_hash() == parent_hash && c.is_available())
.cloned()
}
@@ -972,12 +1035,10 @@ mod tests {
nonce: rng.random::<u64>(),
code_hash: KECCAK_EMPTY,
code: Some(Default::default()),
storage_id: None,
},
storage,
status: AccountStatus::Touched,
transaction_id: 0,
..Default::default()
};
state_update.insert(address, account);
@@ -1050,19 +1111,17 @@ mod tests {
let provider_factory = BlockchainProvider::new(factory).unwrap();
let mut handle =
payload_processor.spawn(
Default::default(),
(
core::iter::empty::<
Result<Recovered<TransactionSigned>, core::convert::Infallible>,
>(),
std::convert::identity,
),
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
OverlayStateProviderFactory::new(provider_factory),
&TreeConfig::default(),
);
let mut handle = payload_processor.spawn(
Default::default(),
(
Vec::<Result<Recovered<TransactionSigned>, core::convert::Infallible>>::new(),
std::convert::identity,
),
StateProviderBuilder::new(provider_factory.clone(), genesis_hash, None),
OverlayStateProviderFactory::new(provider_factory),
&TreeConfig::default(),
None, // No BAL for test
);
let mut state_hook = handle.state_hook();

View File

@@ -1,16 +1,16 @@
//! Multiproof task related functionality.
use crate::tree::payload_processor::bal::bal_to_hashed_post_state;
use alloy_eip7928::BlockAccessList;
use alloy_evm::block::StateChangeSource;
use alloy_primitives::{
keccak256,
map::{B256Set, HashSet},
B256,
};
use alloy_primitives::{keccak256, map::HashSet, B256};
use crossbeam_channel::{unbounded, Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
use dashmap::DashMap;
use derive_more::derive::Deref;
use metrics::{Gauge, Histogram};
use rayon::prelude::*;
use reth_metrics::Metrics;
use reth_provider::AccountReader;
use reth_revm::state::EvmState;
use reth_trie::{
added_removed_keys::MultiAddedRemovedKeys, DecodedMultiProof, HashedPostState, HashedStorage,
@@ -20,12 +20,35 @@ use reth_trie_parallel::{
proof::ParallelProof,
proof_task::{
AccountMultiproofInput, ProofResultContext, ProofResultMessage, ProofWorkerHandle,
StorageProofInput,
},
};
use std::{collections::BTreeMap, mem, ops::DerefMut, sync::Arc, time::Instant};
use tracing::{debug, error, instrument, trace};
/// Source of state changes, either from EVM execution or from a Block Access List.
#[derive(Clone, Copy)]
pub enum Source {
/// State changes from EVM execution.
Evm(StateChangeSource),
/// State changes from Block Access List (EIP-7928).
BlockAccessList,
}
impl std::fmt::Debug for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Evm(source) => source.fmt(f),
Self::BlockAccessList => f.write_str("BlockAccessList"),
}
}
}
impl From<StateChangeSource> for Source {
fn from(source: StateChangeSource) -> Self {
Self::Evm(source)
}
}
/// Maximum number of targets to batch together for prefetch batching.
/// Prefetches are just proof requests (no state merging), so we allow a higher cap than state
/// updates
@@ -82,7 +105,7 @@ pub(super) enum MultiProofMessage {
/// Prefetch proof targets
PrefetchProofs(MultiProofTargets),
/// New state update from transaction execution with its source
StateUpdate(StateChangeSource, EvmState),
StateUpdate(Source, EvmState),
/// State update that can be applied to the sparse trie without any new proofs.
///
/// It can be the case when all accounts and storage slots from the state update were already
@@ -93,6 +116,11 @@ pub(super) enum MultiProofMessage {
/// The state update that was used to calculate the proof
state: 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.
/// No further messages are expected after receiving this variant.
BlockAccessList(Arc<BlockAccessList>),
/// Signals state update stream end.
///
/// This is triggered by block execution, indicating that no additional state updates are
@@ -138,11 +166,6 @@ impl ProofSequencer {
while let Some(pending) = self.pending_proofs.remove(&current_sequence) {
consecutive_proofs.push(pending);
current_sequence += 1;
// if we don't have the next number, stop collecting
if !self.pending_proofs.contains_key(&current_sequence) {
break;
}
}
self.next_to_deliver += consecutive_proofs.len() as u64;
@@ -178,109 +201,44 @@ impl Drop for StateHookSender {
}
pub(crate) fn evm_state_to_hashed_post_state(update: EvmState) -> HashedPostState {
let mut hashed_state = HashedPostState::with_capacity(update.len());
update.into_par_iter()
.filter_map(|(address, account)| {
if !account.is_touched() {
return None;
}
for (address, account) in update {
if account.is_touched() {
let hashed_address = keccak256(address);
trace!(target: "engine::tree::payload_processor::multiproof", ?address, ?hashed_address, "Adding account to state update");
let destroyed = account.is_selfdestructed();
let info = if destroyed { None } else { Some(account.info.into()) };
hashed_state.accounts.insert(hashed_address, info);
let mut changed_storage_iter = account
.storage
.into_iter()
.filter(|(_slot, value)| value.is_changed())
.map(|(slot, value)| (keccak256(B256::from(slot)), value.present_value))
.peekable();
let hashed_storage = if destroyed {
Some(HashedStorage::new(true))
} else {
let storage: Vec<_> = account
.storage
.into_iter()
.filter(|(_slot, value)| value.is_changed())
.map(|(slot, value)| (keccak256(B256::from(slot)), value.present_value))
.collect();
if destroyed {
hashed_state.storages.insert(hashed_address, HashedStorage::new(true));
} else if changed_storage_iter.peek().is_some() {
hashed_state
.storages
.insert(hashed_address, HashedStorage::from_iter(false, changed_storage_iter));
}
}
}
if storage.is_empty() {
None
} else {
Some(HashedStorage::from_iter(false, storage))
}
};
hashed_state
}
/// A pending multiproof task, either [`StorageMultiproofInput`] or [`MultiproofInput`].
#[derive(Debug)]
enum PendingMultiproofTask {
/// A storage multiproof task input.
Storage(StorageMultiproofInput),
/// A regular multiproof task input.
Regular(MultiproofInput),
}
impl PendingMultiproofTask {
/// Returns the proof sequence number of the task.
const fn proof_sequence_number(&self) -> u64 {
match self {
Self::Storage(input) => input.proof_sequence_number,
Self::Regular(input) => input.proof_sequence_number,
}
}
/// Returns whether or not the proof targets are empty.
fn proof_targets_is_empty(&self) -> bool {
match self {
Self::Storage(input) => input.proof_targets.is_empty(),
Self::Regular(input) => input.proof_targets.is_empty(),
}
}
/// Destroys the input and sends a [`MultiProofMessage::EmptyProof`] message to the sender.
fn send_empty_proof(self) {
match self {
Self::Storage(input) => input.send_empty_proof(),
Self::Regular(input) => input.send_empty_proof(),
}
}
}
impl From<StorageMultiproofInput> for PendingMultiproofTask {
fn from(input: StorageMultiproofInput) -> Self {
Self::Storage(input)
}
}
impl From<MultiproofInput> for PendingMultiproofTask {
fn from(input: MultiproofInput) -> Self {
Self::Regular(input)
}
}
/// Input parameters for dispatching a dedicated storage multiproof calculation.
#[derive(Debug)]
struct StorageMultiproofInput {
hashed_state_update: HashedPostState,
hashed_address: B256,
proof_targets: B256Set,
proof_sequence_number: u64,
state_root_message_sender: CrossbeamSender<MultiProofMessage>,
multi_added_removed_keys: Arc<MultiAddedRemovedKeys>,
}
impl StorageMultiproofInput {
/// Destroys the input and sends a [`MultiProofMessage::EmptyProof`] message to the sender.
fn send_empty_proof(self) {
let _ = self.state_root_message_sender.send(MultiProofMessage::EmptyProof {
sequence_number: self.proof_sequence_number,
state: self.hashed_state_update,
});
}
Some((hashed_address, info, hashed_storage))
})
.collect()
}
/// Input parameters for dispatching a multiproof calculation.
#[derive(Debug)]
struct MultiproofInput {
source: Option<StateChangeSource>,
source: Option<Source>,
hashed_state_update: HashedPostState,
proof_targets: MultiProofTargets,
proof_sequence_number: u64,
@@ -351,91 +309,18 @@ impl MultiproofManager {
}
/// Dispatches a new multiproof calculation to worker pools.
fn dispatch(&self, input: PendingMultiproofTask) {
fn dispatch(&self, input: MultiproofInput) {
// If there are no proof targets, we can just send an empty multiproof back immediately
if input.proof_targets_is_empty() {
debug!(
sequence_number = input.proof_sequence_number(),
if input.proof_targets.is_empty() {
trace!(
sequence_number = input.proof_sequence_number,
"No proof targets, sending empty multiproof back immediately"
);
input.send_empty_proof();
return;
}
match input {
PendingMultiproofTask::Storage(storage_input) => {
self.dispatch_storage_proof(storage_input);
}
PendingMultiproofTask::Regular(multiproof_input) => {
self.dispatch_multiproof(multiproof_input);
}
}
}
/// Dispatches a single storage proof calculation to worker pool.
fn dispatch_storage_proof(&self, storage_multiproof_input: StorageMultiproofInput) {
let StorageMultiproofInput {
hashed_state_update,
hashed_address,
proof_targets,
proof_sequence_number,
multi_added_removed_keys,
state_root_message_sender: _,
} = storage_multiproof_input;
let storage_targets = proof_targets.len();
trace!(
target: "engine::tree::payload_processor::multiproof",
proof_sequence_number,
?proof_targets,
storage_targets,
"Dispatching storage proof to workers"
);
let start = Instant::now();
// Create prefix set from targets
let prefix_set = reth_trie::prefix_set::PrefixSetMut::from(
proof_targets.iter().map(reth_trie::Nibbles::unpack),
);
let prefix_set = prefix_set.freeze();
// Build computation input (data only)
let input = StorageProofInput::new(
hashed_address,
prefix_set,
proof_targets,
true, // with_branch_node_masks
Some(multi_added_removed_keys),
);
// Dispatch to storage worker
if let Err(e) = self.proof_worker_handle.dispatch_storage_proof(
input,
ProofResultContext::new(
self.proof_result_tx.clone(),
proof_sequence_number,
hashed_state_update,
start,
),
) {
error!(target: "engine::tree::payload_processor::multiproof", ?e, "Failed to dispatch storage proof");
return;
}
self.metrics
.active_storage_workers_histogram
.record(self.proof_worker_handle.active_storage_workers() as f64);
self.metrics
.active_account_workers_histogram
.record(self.proof_worker_handle.active_account_workers() as f64);
self.metrics
.pending_storage_multiproofs_histogram
.record(self.proof_worker_handle.pending_storage_tasks() as f64);
self.metrics
.pending_account_multiproofs_histogram
.record(self.proof_worker_handle.pending_account_tasks() as f64);
self.dispatch_multiproof(input);
}
/// Signals that a multiproof calculation has finished.
@@ -713,8 +598,9 @@ impl MultiProofTask {
proof_worker_handle: ProofWorkerHandle,
to_sparse_trie: std::sync::mpsc::Sender<SparseTrieUpdate>,
chunk_size: Option<usize>,
tx: CrossbeamSender<MultiProofMessage>,
rx: CrossbeamReceiver<MultiProofMessage>,
) -> Self {
let (tx, rx) = unbounded();
let (proof_result_tx, proof_result_rx) = unbounded();
let metrics = MultiProofTaskMetrics::default();
@@ -782,17 +668,14 @@ impl MultiProofTask {
available_storage_workers,
MultiProofTargets::chunks,
|proof_targets| {
self.multiproof_manager.dispatch(
MultiproofInput {
source: None,
hashed_state_update: Default::default(),
proof_targets,
proof_sequence_number: self.proof_sequencer.next_sequence(),
state_root_message_sender: self.tx.clone(),
multi_added_removed_keys: Some(multi_added_removed_keys.clone()),
}
.into(),
);
self.multiproof_manager.dispatch(MultiproofInput {
source: None,
hashed_state_update: Default::default(),
proof_targets,
proof_sequence_number: self.proof_sequencer.next_sequence(),
state_root_message_sender: self.tx.clone(),
multi_added_removed_keys: Some(multi_added_removed_keys.clone()),
});
},
);
self.metrics.prefetch_proof_chunks_histogram.record(num_chunks as f64);
@@ -883,9 +766,19 @@ impl MultiProofTask {
skip(self, update),
fields(accounts = update.len(), chunks = 0)
)]
fn on_state_update(&mut self, source: StateChangeSource, update: EvmState) -> u64 {
fn on_state_update(&mut self, source: Source, update: EvmState) -> u64 {
let hashed_state_update = evm_state_to_hashed_post_state(update);
self.on_hashed_state_update(source, hashed_state_update)
}
/// Processes a hashed state update and dispatches multiproofs as needed.
///
/// Returns the number of state updates dispatched (both `EmptyProof` and regular multiproofs).
fn on_hashed_state_update(
&mut self,
source: Source,
hashed_state_update: HashedPostState,
) -> u64 {
// Update removed keys based on the state update.
self.multi_added_removed_keys.update_with_state(&hashed_state_update);
@@ -930,17 +823,14 @@ impl MultiProofTask {
);
spawned_proof_targets.extend_ref(&proof_targets);
self.multiproof_manager.dispatch(
MultiproofInput {
source: Some(source),
hashed_state_update,
proof_targets,
proof_sequence_number: self.proof_sequencer.next_sequence(),
state_root_message_sender: self.tx.clone(),
multi_added_removed_keys: Some(multi_added_removed_keys.clone()),
}
.into(),
);
self.multiproof_manager.dispatch(MultiproofInput {
source: Some(source),
hashed_state_update,
proof_targets,
proof_sequence_number: self.proof_sequencer.next_sequence(),
state_root_message_sender: self.tx.clone(),
multi_added_removed_keys: Some(multi_added_removed_keys.clone()),
});
},
);
self.metrics
@@ -982,12 +872,16 @@ impl MultiProofTask {
/// This preserves ordering without requeuing onto the channel.
///
/// Returns `true` if done, `false` to continue.
fn process_multiproof_message(
fn process_multiproof_message<P>(
&mut self,
msg: MultiProofMessage,
ctx: &mut MultiproofBatchCtx,
batch_metrics: &mut MultiproofBatchMetrics,
) -> bool {
provider: &P,
) -> bool
where
P: AccountReader,
{
match msg {
// Prefetch proofs: batch consecutive prefetch requests up to target/message limits
MultiProofMessage::PrefetchProofs(targets) => {
@@ -1045,7 +939,7 @@ impl MultiProofTask {
let storage_targets =
merged_targets.values().map(|slots| slots.len()).sum::<usize>();
batch_metrics.prefetch_proofs_requested += self.on_prefetch_proof(merged_targets);
debug!(
trace!(
target: "engine::tree::payload_processor::multiproof",
account_targets,
storage_targets,
@@ -1135,7 +1029,7 @@ impl MultiProofTask {
let batch_len = merged_update.len();
batch_metrics.state_update_proofs_requested +=
self.on_state_update(batch_source, merged_update);
debug!(
trace!(
target: "engine::tree::payload_processor::multiproof",
?batch_source,
len = batch_len,
@@ -1146,6 +1040,56 @@ impl MultiProofTask {
false
}
// Process Block Access List (BAL) - complete state changes provided upfront
MultiProofMessage::BlockAccessList(bal) => {
trace!(target: "engine::tree::payload_processor::multiproof", "processing MultiProofMessage::BAL");
if ctx.first_update_time.is_none() {
self.metrics
.first_update_wait_time_histogram
.record(ctx.start.elapsed().as_secs_f64());
ctx.first_update_time = Some(Instant::now());
debug!(target: "engine::tree::payload_processor::multiproof", "Started state root calculation from BAL");
}
// Convert BAL to HashedPostState and process it
match bal_to_hashed_post_state(&bal, provider) {
Ok(hashed_state) => {
debug!(
target: "engine::tree::payload_processor::multiproof",
accounts = hashed_state.accounts.len(),
storages = hashed_state.storages.len(),
"Processing BAL state update"
);
// Use BlockAccessList as source for BAL-derived state updates
batch_metrics.state_update_proofs_requested +=
self.on_hashed_state_update(Source::BlockAccessList, hashed_state);
}
Err(err) => {
error!(target: "engine::tree::payload_processor::multiproof", ?err, "Failed to convert BAL to hashed state");
return true;
}
}
// Mark updates as finished since BAL provides complete state
ctx.updates_finished_time = Some(Instant::now());
// Check if we're done (might need to wait for proofs to complete)
if self.is_done(
batch_metrics.proofs_processed,
batch_metrics.state_update_proofs_requested,
batch_metrics.prefetch_proofs_requested,
ctx.updates_finished(),
) {
debug!(
target: "engine::tree::payload_processor::multiproof",
"BAL processed and all proofs complete, ending calculation"
);
return true;
}
false
}
// Signal that no more state updates will arrive
MultiProofMessage::FinishedStateUpdates => {
trace!(target: "engine::tree::payload_processor::multiproof", "processing MultiProofMessage::FinishedStateUpdates");
@@ -1238,7 +1182,10 @@ impl MultiProofTask {
target = "engine::tree::payload_processor::multiproof",
skip_all
)]
pub(crate) fn run(mut self) {
pub(crate) fn run<P>(mut self, provider: P)
where
P: AccountReader,
{
let mut ctx = MultiproofBatchCtx::new(Instant::now());
let mut batch_metrics = MultiproofBatchMetrics::default();
@@ -1248,7 +1195,7 @@ impl MultiProofTask {
trace!(target: "engine::tree::payload_processor::multiproof", "entering main channel receiving loop");
if let Some(msg) = ctx.pending_msg.take() {
if self.process_multiproof_message(msg, &mut ctx, &mut batch_metrics) {
if self.process_multiproof_message(msg, &mut ctx, &mut batch_metrics, &provider) {
break 'main;
}
continue;
@@ -1271,7 +1218,7 @@ impl MultiProofTask {
// Convert ProofResultMessage to SparseTrieUpdate
match proof_result.result {
Ok(proof_result_data) => {
debug!(
trace!(
target: "engine::tree::payload_processor::multiproof",
sequence = proof_result.sequence_number,
total_proofs = batch_metrics.proofs_processed,
@@ -1323,7 +1270,7 @@ impl MultiProofTask {
}
};
if self.process_multiproof_message(msg, &mut ctx, &mut batch_metrics) {
if self.process_multiproof_message(msg, &mut ctx, &mut batch_metrics, &provider) {
break 'main;
}
}
@@ -1359,6 +1306,9 @@ impl MultiProofTask {
/// Context for multiproof message batching loop.
///
/// Contains processing state that persists across loop iterations.
///
/// Used by `process_multiproof_message` to batch consecutive same-type messages received via
/// `try_recv` for efficient processing.
struct MultiproofBatchCtx {
/// Buffers a non-matching message type encountered during batching.
/// Processed first in next iteration to preserve ordering while allowing same-type
@@ -1374,7 +1324,7 @@ struct MultiproofBatchCtx {
/// Reusable buffer for accumulating prefetch targets during batching.
accumulated_prefetch_targets: Vec<MultiProofTargets>,
/// Reusable buffer for accumulating state updates during batching.
accumulated_state_updates: Vec<(StateChangeSource, EvmState)>,
accumulated_state_updates: Vec<(Source, EvmState)>,
}
impl MultiproofBatchCtx {
@@ -1492,34 +1442,44 @@ where
/// are safe to merge because they originate from the same logical execution and can be
/// coalesced to amortize proof work.
fn can_batch_state_update(
batch_source: StateChangeSource,
batch_source: Source,
batch_update: &EvmState,
next_source: StateChangeSource,
next_source: Source,
next_update: &EvmState,
) -> bool {
if !same_state_change_source(batch_source, next_source) {
if !same_source(batch_source, next_source) {
return false;
}
match (batch_source, next_source) {
(StateChangeSource::PreBlock(_), StateChangeSource::PreBlock(_)) |
(StateChangeSource::PostBlock(_), StateChangeSource::PostBlock(_)) => {
batch_update == next_update
}
(
Source::Evm(StateChangeSource::PreBlock(_)),
Source::Evm(StateChangeSource::PreBlock(_)),
) |
(
Source::Evm(StateChangeSource::PostBlock(_)),
Source::Evm(StateChangeSource::PostBlock(_)),
) => batch_update == next_update,
_ => true,
}
}
/// Checks whether two state change sources refer to the same origin.
fn same_state_change_source(lhs: StateChangeSource, rhs: StateChangeSource) -> bool {
/// Checks whether two sources refer to the same origin.
fn same_source(lhs: Source, rhs: Source) -> bool {
match (lhs, rhs) {
(StateChangeSource::Transaction(a), StateChangeSource::Transaction(b)) => a == b,
(StateChangeSource::PreBlock(a), StateChangeSource::PreBlock(b)) => {
mem::discriminant(&a) == mem::discriminant(&b)
}
(StateChangeSource::PostBlock(a), StateChangeSource::PostBlock(b)) => {
mem::discriminant(&a) == mem::discriminant(&b)
}
(
Source::Evm(StateChangeSource::Transaction(a)),
Source::Evm(StateChangeSource::Transaction(b)),
) => a == b,
(
Source::Evm(StateChangeSource::PreBlock(a)),
Source::Evm(StateChangeSource::PreBlock(b)),
) => mem::discriminant(&a) == mem::discriminant(&b),
(
Source::Evm(StateChangeSource::PostBlock(a)),
Source::Evm(StateChangeSource::PostBlock(b)),
) => mem::discriminant(&a) == mem::discriminant(&b),
(Source::BlockAccessList, Source::BlockAccessList) => true,
_ => false,
}
}
@@ -1539,16 +1499,18 @@ fn estimate_evm_state_targets(state: &EvmState) -> usize {
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::map::B256Set;
use crate::tree::cached_state::{CachedStateProvider, ExecutionCacheBuilder};
use alloy_eip7928::{AccountChanges, BalanceChange};
use alloy_primitives::{map::B256Set, Address};
use reth_provider::{
providers::OverlayStateProviderFactory, test_utils::create_test_provider_factory,
BlockReader, DatabaseProviderFactory, PruneCheckpointReader, StageCheckpointReader,
TrieReader,
BlockReader, DatabaseProviderFactory, LatestStateProvider, PruneCheckpointReader,
StageCheckpointReader, StateProviderBox, TrieReader,
};
use reth_trie::MultiProof;
use reth_trie_parallel::proof_task::{ProofTaskCtx, ProofWorkerHandle};
use revm_primitives::{B256, U256};
use std::sync::OnceLock;
use std::sync::{Arc, OnceLock};
use tokio::runtime::{Handle, Runtime};
/// Get a handle to the test runtime, creating it if necessary
@@ -1575,8 +1537,23 @@ mod tests {
let task_ctx = ProofTaskCtx::new(overlay_factory);
let proof_handle = ProofWorkerHandle::new(rt_handle, task_ctx, 1, 1);
let (to_sparse_trie, _receiver) = std::sync::mpsc::channel();
let (tx, rx) = crossbeam_channel::unbounded();
MultiProofTask::new(proof_handle, to_sparse_trie, Some(1))
MultiProofTask::new(proof_handle, to_sparse_trie, Some(1), tx, rx)
}
fn create_cached_provider<F>(factory: F) -> CachedStateProvider<StateProviderBox>
where
F: DatabaseProviderFactory<
Provider: BlockReader + TrieReader + StageCheckpointReader + PruneCheckpointReader,
> + Clone
+ Send
+ 'static,
{
let db_provider = factory.database_provider_ro().unwrap();
let state_provider: StateProviderBox = Box::new(LatestStateProvider::new(db_provider));
let cache = ExecutionCacheBuilder::default().build_caches(1000);
CachedStateProvider::new(state_provider, cache, Default::default())
}
#[test]
@@ -2083,9 +2060,7 @@ mod tests {
nonce: 1,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2101,9 +2076,7 @@ mod tests {
nonce: 2,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2113,8 +2086,8 @@ mod tests {
let source = StateChangeSource::Transaction(0);
let tx = task.state_root_message_sender();
tx.send(MultiProofMessage::StateUpdate(source, update1.clone())).unwrap();
tx.send(MultiProofMessage::StateUpdate(source, update2.clone())).unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), update1.clone())).unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), update2.clone())).unwrap();
let proofs_requested =
if let Ok(MultiProofMessage::StateUpdate(_src, update)) = task.rx.recv() {
@@ -2133,7 +2106,7 @@ mod tests {
assert!(merged_update.contains_key(&addr1));
assert!(merged_update.contains_key(&addr2));
task.on_state_update(source, merged_update)
task.on_state_update(source.into(), merged_update)
} else {
panic!("Expected StateUpdate message");
};
@@ -2163,9 +2136,7 @@ mod tests {
nonce: 1,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2179,20 +2150,20 @@ mod tests {
// Queue: A1 (immediate dispatch), B1 (batched), A2 (should become pending)
let tx = task.state_root_message_sender();
tx.send(MultiProofMessage::StateUpdate(source_a, create_state_update(addr_a1, 100)))
tx.send(MultiProofMessage::StateUpdate(source_a.into(), create_state_update(addr_a1, 100)))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(source_b, create_state_update(addr_b1, 200)))
tx.send(MultiProofMessage::StateUpdate(source_b.into(), create_state_update(addr_b1, 200)))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(source_a, create_state_update(addr_a2, 300)))
tx.send(MultiProofMessage::StateUpdate(source_a.into(), create_state_update(addr_a2, 300)))
.unwrap();
let mut pending_msg: Option<MultiProofMessage> = None;
if let Ok(MultiProofMessage::StateUpdate(first_source, _)) = task.rx.recv() {
assert!(same_state_change_source(first_source, source_a));
assert!(same_source(first_source, source_a.into()));
// Simulate batching loop for remaining messages
let mut accumulated_updates: Vec<(StateChangeSource, EvmState)> = Vec::new();
let mut accumulated_updates: Vec<(Source, EvmState)> = Vec::new();
let mut accumulated_targets = 0usize;
loop {
@@ -2240,7 +2211,7 @@ mod tests {
assert_eq!(accumulated_updates.len(), 1, "Should only batch matching sources");
let batch_source = accumulated_updates[0].0;
assert!(same_state_change_source(batch_source, source_b));
assert!(same_source(batch_source, source_b.into()));
let batch_source = accumulated_updates[0].0;
let mut merged_update = accumulated_updates.remove(0).1;
@@ -2248,10 +2219,7 @@ mod tests {
merged_update.extend(next_update);
}
assert!(
same_state_change_source(batch_source, source_b),
"Batch should use matching source"
);
assert!(same_source(batch_source, source_b.into()), "Batch should use matching source");
assert!(merged_update.contains_key(&addr_b1));
assert!(!merged_update.contains_key(&addr_a1));
assert!(!merged_update.contains_key(&addr_a2));
@@ -2261,7 +2229,7 @@ mod tests {
match pending_msg {
Some(MultiProofMessage::StateUpdate(pending_source, pending_update)) => {
assert!(same_state_change_source(pending_source, source_a));
assert!(same_source(pending_source, source_a.into()));
assert!(pending_update.contains_key(&addr_a2));
}
other => panic!("Expected pending StateUpdate with source_a, got {:?}", other),
@@ -2291,9 +2259,7 @@ mod tests {
nonce: 1,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2306,17 +2272,20 @@ mod tests {
// Queue: first update dispatched immediately, next two should not merge
let tx = task.state_root_message_sender();
tx.send(MultiProofMessage::StateUpdate(source, create_state_update(addr1, 100))).unwrap();
tx.send(MultiProofMessage::StateUpdate(source, create_state_update(addr2, 200))).unwrap();
tx.send(MultiProofMessage::StateUpdate(source, create_state_update(addr3, 300))).unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), create_state_update(addr1, 100)))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), create_state_update(addr2, 200)))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), create_state_update(addr3, 300)))
.unwrap();
let mut pending_msg: Option<MultiProofMessage> = None;
if let Ok(MultiProofMessage::StateUpdate(first_source, first_update)) = task.rx.recv() {
assert!(same_state_change_source(first_source, source));
assert!(same_source(first_source, source.into()));
assert!(first_update.contains_key(&addr1));
let mut accumulated_updates: Vec<(StateChangeSource, EvmState)> = Vec::new();
let mut accumulated_updates: Vec<(Source, EvmState)> = Vec::new();
let mut accumulated_targets = 0usize;
loop {
@@ -2368,7 +2337,7 @@ mod tests {
"Second pre-block update should not merge with a different payload"
);
let (batched_source, batched_update) = accumulated_updates.remove(0);
assert!(same_state_change_source(batched_source, source));
assert!(same_source(batched_source, source.into()));
assert!(batched_update.contains_key(&addr2));
assert!(!batched_update.contains_key(&addr3));
@@ -2418,9 +2387,7 @@ mod tests {
nonce: 1,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2437,9 +2404,7 @@ mod tests {
nonce: 2,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2452,8 +2417,8 @@ mod tests {
let tx = task.state_root_message_sender();
tx.send(MultiProofMessage::PrefetchProofs(targets1)).unwrap();
tx.send(MultiProofMessage::PrefetchProofs(targets2)).unwrap();
tx.send(MultiProofMessage::StateUpdate(source, state_update1)).unwrap();
tx.send(MultiProofMessage::StateUpdate(source, state_update2)).unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), state_update1)).unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), state_update2)).unwrap();
tx.send(MultiProofMessage::PrefetchProofs(targets3.clone())).unwrap();
// Step 1: Receive and batch PrefetchProofs (should get targets1 + targets2)
@@ -2520,6 +2485,7 @@ mod tests {
use revm_state::Account;
let test_provider_factory = create_test_provider_factory();
let test_provider = create_cached_provider(test_provider_factory.clone());
let mut task = create_test_state_root_task(test_provider_factory);
// Queue: Prefetch1, StateUpdate, Prefetch2
@@ -2540,9 +2506,7 @@ mod tests {
nonce: 1,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2553,7 +2517,7 @@ mod tests {
let tx = task.state_root_message_sender();
tx.send(MultiProofMessage::PrefetchProofs(prefetch1)).unwrap();
tx.send(MultiProofMessage::StateUpdate(source, state_update)).unwrap();
tx.send(MultiProofMessage::StateUpdate(source.into(), state_update)).unwrap();
tx.send(MultiProofMessage::PrefetchProofs(prefetch2.clone())).unwrap();
let mut ctx = MultiproofBatchCtx::new(Instant::now());
@@ -2562,12 +2526,22 @@ mod tests {
// First message: Prefetch1 batches; StateUpdate becomes pending.
let first = task.rx.recv().unwrap();
assert!(matches!(first, MultiProofMessage::PrefetchProofs(_)));
assert!(!task.process_multiproof_message(first, &mut ctx, &mut batch_metrics));
assert!(!task.process_multiproof_message(
first,
&mut ctx,
&mut batch_metrics,
&test_provider
));
let pending = ctx.pending_msg.take().expect("pending message captured");
assert!(matches!(pending, MultiProofMessage::StateUpdate(_, _)));
// Pending message should be handled before the next select loop.
assert!(!task.process_multiproof_message(pending, &mut ctx, &mut batch_metrics));
assert!(!task.process_multiproof_message(
pending,
&mut ctx,
&mut batch_metrics,
&test_provider
));
// Prefetch2 should now be in pending_msg (captured by StateUpdate's batching loop).
match ctx.pending_msg.take() {
@@ -2625,9 +2599,7 @@ mod tests {
nonce: 1,
code_hash: Default::default(),
code: Default::default(),
storage_id: Some(0),
},
original_info: Default::default(),
transaction_id: Default::default(),
storage: Default::default(),
status: revm_state::AccountStatus::Touched,
@@ -2641,12 +2613,21 @@ mod tests {
// Queue: [Prefetch1, State1, State2, State3, Prefetch2]
let tx = task.state_root_message_sender();
tx.send(MultiProofMessage::PrefetchProofs(prefetch1.clone())).unwrap();
tx.send(MultiProofMessage::StateUpdate(source, create_state_update(state_addr1, 100)))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(source, create_state_update(state_addr2, 200)))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(source, create_state_update(state_addr3, 300)))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(
source.into(),
create_state_update(state_addr1, 100),
))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(
source.into(),
create_state_update(state_addr2, 200),
))
.unwrap();
tx.send(MultiProofMessage::StateUpdate(
source.into(),
create_state_update(state_addr3, 300),
))
.unwrap();
tx.send(MultiProofMessage::PrefetchProofs(prefetch2.clone())).unwrap();
// Simulate the state-machine loop behavior
@@ -2719,4 +2700,44 @@ mod tests {
_ => panic!("Prefetch2 was lost!"),
}
}
/// Verifies that BAL messages are processed correctly and generate state updates.
#[test]
fn test_bal_message_processing() {
let test_provider_factory = create_test_provider_factory();
let test_provider = create_cached_provider(test_provider_factory.clone());
let mut task = create_test_state_root_task(test_provider_factory);
// Create a simple BAL with one account change
let account_address = Address::random();
let account_changes = AccountChanges {
address: account_address,
balance_changes: vec![BalanceChange::new(0, U256::from(1000))],
nonce_changes: vec![],
code_changes: vec![],
storage_changes: vec![],
storage_reads: vec![],
};
let bal = Arc::new(vec![account_changes]);
let mut ctx = MultiproofBatchCtx::new(Instant::now());
let mut batch_metrics = MultiproofBatchMetrics::default();
let should_finish = task.process_multiproof_message(
MultiProofMessage::BlockAccessList(bal),
&mut ctx,
&mut batch_metrics,
&test_provider,
);
// BAL should mark updates as finished
assert!(ctx.updates_finished_time.is_some());
// Should have dispatched state update proofs
assert!(batch_metrics.state_update_proofs_requested > 0);
// Should need to wait for the results of those proofs to arrive
assert!(!should_finish, "Should continue waiting for proofs");
}
}

View File

@@ -14,25 +14,31 @@
use crate::tree::{
cached_state::{CachedStateProvider, SavedCache},
payload_processor::{
executor::WorkloadExecutor, multiproof::MultiProofMessage,
bal::{total_slots, BALSlotIter},
executor::WorkloadExecutor,
multiproof::MultiProofMessage,
ExecutionCache as PayloadExecutionCache,
},
precompile_cache::{CachedPrecompile, PrecompileCacheMap},
ExecutionEnv, StateProviderBuilder,
};
use alloy_consensus::transaction::TxHashRef;
use alloy_eip7928::BlockAccessList;
use alloy_eips::Typed2718;
use alloy_evm::Database;
use alloy_primitives::{keccak256, map::B256Set, B256};
use crossbeam_channel::Sender as CrossbeamSender;
use metrics::{Counter, Gauge, Histogram};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, SpecFor};
use reth_execution_types::ExecutionOutcome;
use reth_metrics::Metrics;
use reth_primitives_traits::NodePrimitives;
use reth_provider::{BlockReader, StateProviderFactory, StateReader};
use reth_revm::{database::StateProviderDatabase, db::BundleState, state::EvmState};
use reth_provider::{AccountReader, BlockReader, StateProvider, StateProviderFactory, StateReader};
use reth_revm::{database::StateProviderDatabase, state::EvmState};
use reth_trie::MultiProofTargets;
use std::{
ops::Range,
sync::{
atomic::{AtomicBool, Ordering},
mpsc::{self, channel, Receiver, Sender},
@@ -42,6 +48,14 @@ use std::{
};
use tracing::{debug, debug_span, instrument, trace, warn, Span};
/// Determines the prewarming mode: transaction-based or BAL-based.
pub(super) 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>),
}
/// A wrapper for transactions that includes their index in the block.
#[derive(Clone)]
struct IndexedTransaction<Tx> {
@@ -86,7 +100,7 @@ where
/// Sender to emit evm state outcome messages, if any.
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
/// Receiver for events produced by tx execution
actions_rx: Receiver<PrewarmTaskEvent>,
actions_rx: Receiver<PrewarmTaskEvent<N::Receipt>>,
/// Parent span for tracing
parent_span: Span,
}
@@ -105,7 +119,7 @@ where
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
transaction_count_hint: usize,
max_concurrency: usize,
) -> (Self, Sender<PrewarmTaskEvent>) {
) -> (Self, Sender<PrewarmTaskEvent<N::Receipt>>) {
let (actions_tx, actions_rx) = channel();
trace!(
@@ -135,8 +149,11 @@ where
/// For Optimism chains, special handling is applied to the first transaction if it's a
/// deposit transaction (type 0x7E/126) which sets critical metadata that affects all
/// subsequent transactions in the block.
fn spawn_all<Tx>(&self, pending: mpsc::Receiver<Tx>, actions_tx: Sender<PrewarmTaskEvent>)
where
fn spawn_all<Tx>(
&self,
pending: mpsc::Receiver<Tx>,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
) where
Tx: ExecutableTxFor<Evm> + Clone + Send + 'static,
{
let executor = self.executor.clone();
@@ -160,12 +177,7 @@ where
};
// Initialize worker handles container
let mut handles = Vec::with_capacity(workers_needed);
// Only spawn initial workers as needed
for i in 0..workers_needed {
handles.push(ctx.spawn_worker(i, &executor, actions_tx.clone(), done_tx.clone()));
}
let handles = ctx.clone().spawn_workers(workers_needed, &executor, actions_tx.clone(), done_tx.clone());
// Distribute transactions to workers
let mut tx_index = 0usize;
@@ -248,38 +260,123 @@ where
///
/// This method is called from `run()` only after all execution tasks are complete.
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
fn save_cache(self, state: BundleState) {
fn save_cache(self, execution_outcome: Arc<ExecutionOutcome<N::Receipt>>) {
let start = Instant::now();
let Self { execution_cache, ctx: PrewarmContext { env, metrics, saved_cache, .. }, .. } =
self;
let hash = env.hash;
debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
// Perform all cache operations atomically under the lock
execution_cache.update_with_guard(|cached| {
// consumes the `SavedCache` held by the prewarming task, which releases its usage guard
let (caches, cache_metrics) = saved_cache.split();
let new_cache = SavedCache::new(hash, caches, cache_metrics);
if let Some(saved_cache) = saved_cache {
debug!(target: "engine::caching", parent_hash=?hash, "Updating execution cache");
// Perform all cache operations atomically under the lock
execution_cache.update_with_guard(|cached| {
// consumes the `SavedCache` held by the prewarming task, which releases its usage
// guard
let (caches, cache_metrics) = saved_cache.split();
let new_cache = SavedCache::new(hash, caches, cache_metrics);
// Insert state into cache while holding the lock
if new_cache.cache().insert_state(&state).is_err() {
// Clear the cache on error to prevent having a polluted cache
*cached = None;
debug!(target: "engine::caching", "cleared execution cache on update error");
return;
}
// Insert state into cache while holding the lock
// Access the BundleState through the shared ExecutionOutcome
if new_cache.cache().insert_state(execution_outcome.state()).is_err() {
// Clear the cache on error to prevent having a polluted cache
*cached = None;
debug!(target: "engine::caching", "cleared execution cache on update error");
return;
}
new_cache.update_metrics();
new_cache.update_metrics();
// Replace the shared cache with the new one; the previous cache (if any) is dropped.
*cached = Some(new_cache);
});
// Replace the shared cache with the new one; the previous cache (if any) is
// dropped.
*cached = Some(new_cache);
});
let elapsed = start.elapsed();
debug!(target: "engine::caching", parent_hash=?hash, elapsed=?elapsed, "Updated execution cache");
let elapsed = start.elapsed();
debug!(target: "engine::caching", parent_hash=?hash, elapsed=?elapsed, "Updated execution cache");
metrics.cache_saving_duration.set(elapsed.as_secs_f64());
metrics.cache_saving_duration.set(elapsed.as_secs_f64());
}
}
/// Runs BAL-based prewarming by spawning workers to prefetch storage slots.
///
/// Divides the total slots across `max_concurrency` workers, each responsible for
/// prefetching a range of slots from the BAL.
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
fn run_bal_prewarm(
&self,
bal: Arc<BlockAccessList>,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
) {
// Only prefetch if we have a cache to populate
if self.ctx.saved_cache.is_none() {
trace!(
target: "engine::tree::payload_processor::prewarm",
"Skipping BAL prewarm - no cache available"
);
let _ =
actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
return;
}
let total_slots = total_slots(&bal);
trace!(
target: "engine::tree::payload_processor::prewarm",
total_slots,
max_concurrency = self.max_concurrency,
"Starting BAL prewarm"
);
if total_slots == 0 {
// No slots to prefetch, signal completion immediately
let _ =
actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
return;
}
let (done_tx, done_rx) = mpsc::channel();
// Calculate number of workers needed (at most max_concurrency)
let workers_needed = total_slots.min(self.max_concurrency);
// Calculate slots per worker
let slots_per_worker = total_slots / workers_needed;
let remainder = total_slots % workers_needed;
// Spawn workers with their assigned ranges
for i in 0..workers_needed {
let start = i * slots_per_worker + i.min(remainder);
let extra = if i < remainder { 1 } else { 0 };
let end = start + slots_per_worker + extra;
self.ctx.spawn_bal_worker(
i,
&self.executor,
Arc::clone(&bal),
start..end,
done_tx.clone(),
);
}
// Drop our handle to done_tx so we can detect completion
drop(done_tx);
// Wait for all workers to complete
let mut completed_workers = 0;
while done_rx.recv().is_ok() {
completed_workers += 1;
}
trace!(
target: "engine::tree::payload_processor::prewarm",
completed_workers,
"All BAL prewarm workers completed"
);
// Signal that execution has finished
let _ = actions_tx.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: 0 });
}
/// Executes the task.
@@ -293,15 +390,24 @@ where
name = "prewarm and caching",
skip_all
)]
pub(super) fn run(
pub(super) fn run<Tx>(
self,
pending: mpsc::Receiver<impl ExecutableTxFor<Evm> + Clone + Send + 'static>,
actions_tx: Sender<PrewarmTaskEvent>,
) {
// spawn execution tasks.
self.spawn_all(pending, actions_tx);
mode: PrewarmMode<Tx>,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
) where
Tx: ExecutableTxFor<Evm> + Clone + Send + 'static,
{
// Spawn execution tasks based on mode
match mode {
PrewarmMode::Transactions(pending) => {
self.spawn_all(pending, actions_tx);
}
PrewarmMode::BlockAccessList(bal) => {
self.run_bal_prewarm(bal, actions_tx);
}
}
let mut final_block_output = None;
let mut final_execution_outcome = None;
let mut finished_execution = false;
while let Ok(event) = self.actions_rx.recv() {
match event {
@@ -314,9 +420,9 @@ where
// completed executing a set of transactions
self.send_multi_proof_targets(proof_targets);
}
PrewarmTaskEvent::Terminate { block_output } => {
PrewarmTaskEvent::Terminate { execution_outcome } => {
trace!(target: "engine::tree::payload_processor::prewarm", "Received termination signal");
final_block_output = Some(block_output);
final_execution_outcome = Some(execution_outcome);
if finished_execution {
// all tasks are done, we can exit, which will save caches and exit
@@ -330,7 +436,7 @@ where
finished_execution = true;
if final_block_output.is_some() {
if final_execution_outcome.is_some() {
// all tasks are done, we can exit, which will save caches and exit
break
}
@@ -340,9 +446,9 @@ where
debug!(target: "engine::tree::payload_processor::prewarm", "Completed prewarm execution");
// save caches and finish
if let Some(Some(state)) = final_block_output {
self.save_cache(state);
// save caches and finish using the shared ExecutionOutcome
if let Some(Some(execution_outcome)) = final_execution_outcome {
self.save_cache(execution_outcome);
}
}
}
@@ -356,7 +462,7 @@ where
{
pub(super) env: ExecutionEnv<Evm>,
pub(super) evm_config: Evm,
pub(super) saved_cache: SavedCache,
pub(super) saved_cache: Option<SavedCache>,
/// Provider to obtain the state
pub(super) provider: StateProviderBuilder<N, P>,
pub(super) metrics: PrewarmMetrics,
@@ -384,10 +490,10 @@ where
metrics,
terminate_execution,
precompile_cache_disabled,
mut precompile_cache_map,
precompile_cache_map,
} = self;
let state_provider = match provider.build() {
let mut state_provider = match provider.build() {
Ok(provider) => provider,
Err(err) => {
trace!(
@@ -400,10 +506,15 @@ where
};
// Use the caches to create a new provider with caching
let caches = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
let state_provider =
CachedStateProvider::new_with_caches(state_provider, caches, cache_metrics);
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(state_provider, caches, cache_metrics)
// ensure we pre-warm the cache
.prewarm(),
);
}
let state_provider = StateProviderDatabase::new(state_provider);
@@ -445,7 +556,7 @@ where
fn transact_batch<Tx>(
self,
txs: mpsc::Receiver<IndexedTransaction<Tx>>,
sender: Sender<PrewarmTaskEvent>,
sender: Sender<PrewarmTaskEvent<N::Receipt>>,
done_tx: Sender<()>,
) where
Tx: ExecutableTxFor<Evm>,
@@ -508,7 +619,8 @@ where
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);
let targets = multiproof_targets_from_state(res.state);
let storage_targets = targets.storage_targets_count();
metrics.prefetch_storage_targets.record(storage_targets as f64);
let _ = sender.send(PrewarmTaskEvent::Outcome { proof_targets: Some(targets) });
drop(_enter);
@@ -522,74 +634,181 @@ where
}
/// Spawns a worker task for transaction execution and returns its sender channel.
fn spawn_worker<Tx>(
&self,
idx: usize,
executor: &WorkloadExecutor,
actions_tx: Sender<PrewarmTaskEvent>,
fn spawn_workers<Tx>(
self,
workers_needed: usize,
task_executor: &WorkloadExecutor,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
done_tx: Sender<()>,
) -> mpsc::Sender<IndexedTransaction<Tx>>
) -> Vec<mpsc::Sender<IndexedTransaction<Tx>>>
where
Tx: ExecutableTxFor<Evm> + Send + 'static,
{
let (tx, rx) = mpsc::channel();
let mut handles = Vec::with_capacity(workers_needed);
let mut receivers = Vec::with_capacity(workers_needed);
for _ in 0..workers_needed {
let (tx, rx) = mpsc::channel();
handles.push(tx);
receivers.push(rx);
}
// Spawn a separate task spawning workers in parallel.
let executor = task_executor.clone();
let span = Span::current();
task_executor.spawn_blocking(move || {
let _enter = span.entered();
for (idx, rx) in receivers.into_iter().enumerate() {
let ctx = self.clone();
let actions_tx = actions_tx.clone();
let done_tx = done_tx.clone();
let span = debug_span!(target: "engine::tree::payload_processor::prewarm", "prewarm worker", idx);
executor.spawn_blocking(move || {
let _enter = span.entered();
ctx.transact_batch(rx, actions_tx, done_tx);
});
}
});
handles
}
/// Spawns a worker task for BAL slot prefetching.
///
/// The worker iterates over the specified range of slots in the BAL and ensures
/// each slot is loaded into the cache by accessing it through the state provider.
fn spawn_bal_worker(
&self,
idx: usize,
executor: &WorkloadExecutor,
bal: Arc<BlockAccessList>,
range: Range<usize>,
done_tx: Sender<()>,
) {
let ctx = self.clone();
let span =
debug_span!(target: "engine::tree::payload_processor::prewarm", "prewarm worker", idx);
let span = debug_span!(
target: "engine::tree::payload_processor::prewarm",
"bal prewarm worker",
idx,
range_start = range.start,
range_end = range.end
);
executor.spawn_blocking(move || {
let _enter = span.entered();
ctx.transact_batch(rx, actions_tx, done_tx);
ctx.prefetch_bal_slots(bal, range, done_tx);
});
}
tx
/// Prefetches storage slots from a BAL range into the cache.
///
/// This iterates through the specified range of slots and accesses them via the state
/// provider to populate the cache.
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
fn prefetch_bal_slots(
self,
bal: Arc<BlockAccessList>,
range: Range<usize>,
done_tx: Sender<()>,
) {
let Self { saved_cache, provider, metrics, .. } = self;
// Build state provider
let state_provider = match provider.build() {
Ok(provider) => provider,
Err(err) => {
trace!(
target: "engine::tree::payload_processor::prewarm",
%err,
"Failed to build state provider in BAL prewarm thread"
);
let _ = done_tx.send(());
return;
}
};
// Wrap with cache (guaranteed to be Some since run_bal_prewarm checks)
let saved_cache = saved_cache.expect("BAL prewarm should only run with cache");
let caches = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
let state_provider = CachedStateProvider::new(state_provider, caches, cache_metrics);
let start = Instant::now();
// Track last seen address to avoid fetching the same account multiple times.
let mut last_address = None;
// Iterate through the assigned range of slots
for (address, slot) in BALSlotIter::new(&bal, range.clone()) {
// Fetch the account if this is a different address than the last one
if last_address != Some(address) {
let _ = state_provider.basic_account(&address);
last_address = Some(address);
}
// Access the slot to populate the cache
let _ = state_provider.storage(address, slot);
}
let elapsed = start.elapsed();
trace!(
target: "engine::tree::payload_processor::prewarm",
?range,
elapsed_ms = elapsed.as_millis(),
"BAL prewarm worker completed"
);
// Signal completion
let _ = done_tx.send(());
metrics.bal_slot_iteration_duration.record(elapsed.as_secs_f64());
}
}
/// Returns a set of [`MultiProofTargets`] and the total amount of storage targets, based on the
/// given state.
fn multiproof_targets_from_state(state: EvmState) -> (MultiProofTargets, usize) {
let mut targets = MultiProofTargets::with_capacity(state.len());
let mut storage_targets = 0;
for (addr, account) in state {
// if the account was not touched, or if the account was selfdestructed, do not
// fetch proofs for it
//
// Since selfdestruct can only happen in the same transaction, we can skip
// prefetching proofs for selfdestructed accounts
//
// See: https://eips.ethereum.org/EIPS/eip-6780
if !account.is_touched() || account.is_selfdestructed() {
continue
}
let mut storage_set =
B256Set::with_capacity_and_hasher(account.storage.len(), Default::default());
for (key, slot) in account.storage {
// do nothing if unchanged
if !slot.is_changed() {
continue
fn multiproof_targets_from_state(state: EvmState) -> MultiProofTargets {
state
.into_par_iter()
.filter_map(|(address, account)| {
// if the account was not touched, or if the account was selfdestructed, do not
// fetch proofs for it
//
// Since selfdestruct can only happen in the same transaction, we can skip
// prefetching proofs for selfdestructed accounts
//
// See: https://eips.ethereum.org/EIPS/eip-6780
if !account.is_touched() || account.is_selfdestructed() {
return None;
}
storage_set.insert(keccak256(B256::new(key.to_be_bytes())));
}
let hashed_address = keccak256(address);
storage_targets += storage_set.len();
targets.insert(keccak256(addr), storage_set);
}
let storage_set: B256Set = account
.storage
.into_iter()
.filter(|(_, slot)| slot.is_changed())
.map(|(key, _)| keccak256(B256::new(key.to_be_bytes())))
.collect();
(targets, storage_targets)
Some((hashed_address, storage_set))
})
.collect()
}
/// The events the pre-warm task can handle.
pub(super) enum PrewarmTaskEvent {
///
/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the main
/// execution path without cloning the expensive `BundleState`.
pub(super) enum PrewarmTaskEvent<R> {
/// Forcefully terminate all remaining transaction execution.
TerminateTransactionExecution,
/// Forcefully terminate the task on demand and update the shared cache with the given output
/// before exiting.
Terminate {
/// The final block state output.
block_output: Option<BundleState>,
/// The final execution outcome. Using `Arc` allows sharing with the main execution
/// path without cloning the expensive `BundleState`.
execution_outcome: Option<Arc<ExecutionOutcome<R>>>,
},
/// The outcome of a pre-warm task
Outcome {
@@ -621,4 +840,6 @@ pub(crate) struct PrewarmMetrics {
pub(crate) cache_saving_duration: Gauge,
/// Counter for transaction execution errors during prewarming
pub(crate) transaction_errors: Counter,
/// A histogram of BAL slot iteration duration during prefetching
pub(crate) bal_slot_iteration_duration: Histogram,
}

View File

@@ -166,8 +166,7 @@ where
// Update storage slots with new values and calculate storage roots.
let span = tracing::Span::current();
let (tx, rx) = mpsc::channel();
state
let results: Vec<_> = state
.storages
.into_iter()
.map(|(address, storage)| (address, storage, trie.take_storage_trie(&address)))
@@ -217,13 +216,7 @@ where
SparseStateTrieResult::Ok((address, storage_trie))
})
.for_each_init(
|| tx.clone(),
|tx, result| {
let _ = tx.send(result);
},
);
drop(tx);
.collect();
// Defer leaf removals until after updates/additions, so that we don't delete an intermediate
// branch node during a removal and then re-add that branch back during a later leaf addition.
@@ -235,7 +228,7 @@ where
let _enter =
tracing::debug_span!(target: "engine::tree::payload_processor::sparse_trie", "account trie")
.entered();
for result in rx {
for result in results {
let (address, storage_trie) = result?;
trie.insert_storage_trie(address, storage_trie);

View File

@@ -11,9 +11,11 @@ use crate::tree::{
StateProviderDatabase, TreeConfig,
};
use alloy_consensus::transaction::Either;
use alloy_eip7928::BlockAccessList;
use alloy_eips::{eip1898::BlockWithParent, NumHash};
use alloy_evm::Evm;
use alloy_primitives::B256;
use rayon::prelude::*;
use reth_chain_state::{CanonicalInMemoryState, DeferredTrieData, ExecutedBlock};
use reth_consensus::{ConsensusError, FullConsensus};
use reth_engine_primitives::{
@@ -33,12 +35,13 @@ use reth_primitives_traits::{
};
use reth_provider::{
providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockReader,
DatabaseProviderFactory, ExecutionOutcome, HashedPostStateProvider, ProviderError,
PruneCheckpointReader, StageCheckpointReader, StateProvider, StateProviderFactory, StateReader,
StateRootProvider, TrieReader,
DatabaseProviderFactory, DatabaseProviderROFactory, ExecutionOutcome, HashedPostStateProvider,
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider,
StateProviderFactory, StateReader, TrieReader,
};
use reth_revm::db::State;
use reth_trie::{updates::TrieUpdates, HashedPostState, TrieInputSorted};
use reth_storage_errors::db::DatabaseError;
use reth_trie::{updates::TrieUpdates, HashedPostState, StateRoot, TrieInputSorted};
use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError};
use revm_primitives::Address;
use std::{
@@ -220,7 +223,7 @@ where
.map_err(NewPayloadError::other)?
.into();
let iter = Either::Left(iter.into_iter().map(Either::Left));
let iter = Either::Left(iter.into_par_iter().map(Either::Left));
let convert = move |tx| {
let Either::Left(tx) = tx else { unreachable!() };
convert(tx).map(Either::Left).map_err(Either::Left)
@@ -230,8 +233,9 @@ where
Ok((iter, Box::new(convert) as Box<dyn Fn(_) -> _ + Send + Sync + 'static>))
}
BlockOrPayload::Block(block) => {
let iter =
Either::Right(block.body().clone_transactions().into_iter().map(Either::Right));
let iter = Either::Right(
block.body().clone_transactions().into_par_iter().map(Either::Right),
);
let convert = move |tx: Either<_, N::SignedTx>| {
let Either::Right(tx) = tx else { unreachable!() };
tx.try_into_recovered().map(Either::Right).map_err(Either::Right)
@@ -368,10 +372,11 @@ where
)
.into())
};
let state_provider = ensure_ok!(provider_builder.build());
let mut state_provider = ensure_ok!(provider_builder.build());
drop(_enter);
// fetch parent block
// Fetch parent block. This goes to memory most of the time unless the parent block is
// beyond the in-memory buffer.
let Some(parent_block) = ensure_ok!(self.sealed_header_by_hash(parent_hash, ctx.state()))
else {
return Err(InsertBlockError::new(
@@ -396,9 +401,17 @@ where
"Decided which state root algorithm to run"
);
// use prewarming background task
// Get an iterator over the transactions in the payload
let txs = self.tx_iterator_for(&input)?;
// Extract the BAL, if valid and available
let block_access_list = ensure_ok!(input
.block_access_list()
.transpose()
// Eventually gets converted to a `InsertBlockErrorKind::Other`
.map_err(Box::<dyn std::error::Error + Send + Sync>::from))
.map(Arc::new);
// Spawn the appropriate processor based on strategy
let mut handle = ensure_ok!(self.spawn_payload_processor(
env.clone(),
@@ -407,25 +420,22 @@ where
parent_hash,
ctx.state(),
strategy,
block_access_list,
));
// Use cached state provider before executing, used in execution after prewarming threads
// complete
let state_provider = CachedStateProvider::new_with_caches(
state_provider,
handle.caches(),
handle.cache_metrics(),
);
if let Some((caches, cache_metrics)) = handle.caches().zip(handle.cache_metrics()) {
state_provider =
Box::new(CachedStateProvider::new(state_provider, caches, cache_metrics));
};
if self.config.state_provider_metrics() {
state_provider = Box::new(InstrumentedStateProvider::new(state_provider, "engine"));
}
// Execute the block and handle any execution errors
let (output, senders) = match if self.config.state_provider_metrics() {
let state_provider = InstrumentedStateProvider::from_state_provider(&state_provider);
let result = self.execute_block(&state_provider, env, &input, &mut handle);
state_provider.record_total_latency();
result
} else {
self.execute_block(&state_provider, env, &input, &mut handle)
} {
let (output, senders) = match self.execute_block(state_provider, env, &input, &mut handle) {
Ok(output) => output,
Err(err) => return self.handle_execution_error(input, err, &parent_block),
};
@@ -509,7 +519,7 @@ where
}
let (root, updates) = ensure_ok_post_block!(
state_provider.state_root_with_updates(hashed_state.clone()),
self.compute_state_root_serial(block.parent_hash(), &hashed_state, ctx.state()),
block
);
(root, updates, root_time.elapsed())
@@ -539,17 +549,14 @@ where
.into())
}
// terminate prewarming task with good state output
handle.terminate_caching(Some(&output.state));
// Create ExecutionOutcome and wrap in Arc for sharing with both the caching task
// and the deferred trie task. This avoids cloning the expensive BundleState.
let execution_outcome = Arc::new(ExecutionOutcome::from((output, block_num_hash.number)));
Ok(self.spawn_deferred_trie_task(
block,
output,
block_num_hash.number,
&ctx,
hashed_state,
trie_output,
))
// Terminate prewarming task with the shared execution outcome
handle.terminate_caching(Some(Arc::clone(&execution_outcome)));
Ok(self.spawn_deferred_trie_task(block, execution_outcome, &ctx, hashed_state, trie_output))
}
/// Return sealed block header from database or in-memory state by hash.
@@ -592,24 +599,22 @@ where
state_provider: S,
env: ExecutionEnv<Evm>,
input: &BlockOrPayload<T>,
handle: &mut PayloadHandle<impl ExecutableTxFor<Evm>, Err>,
handle: &mut PayloadHandle<impl ExecutableTxFor<Evm>, Err, N::Receipt>,
) -> Result<(BlockExecutionOutput<N::Receipt>, Vec<Address>), InsertBlockErrorKind>
where
S: StateProvider,
S: StateProvider + Send,
Err: core::error::Error + Send + Sync + 'static,
V: PayloadValidator<T, Block = N::Block>,
T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>,
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
{
debug!(target: "engine::tree::payload_validator", "Executing block");
let mut db = State::builder()
.with_database(StateProviderDatabase::new(&state_provider))
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.with_bal_builder() //TODO
.without_state_clear()
.build();
db.bal_state.bal_index = 0;
db.bal_state.bal_builder = Some(revm::state::bal::Bal::new());
let evm = self.evm_config.evm_with_env(&mut db, env.evm_env.clone());
let ctx =
@@ -638,6 +643,7 @@ where
let (output, senders) = self.metrics.execute_metered(
executor,
handle.iter_transactions().map(|res| res.map_err(BlockExecutionError::other)),
input.transaction_count(),
state_hook,
)?;
let execution_finish = Instant::now();
@@ -652,8 +658,6 @@ where
///
/// Returns `Ok(_)` if computed successfully.
/// Returns `Err(_)` if error was encountered during computation.
/// `Err(ProviderError::ConsistentView(_))` can be safely ignored and fallback computation
/// should be used instead.
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
fn compute_state_root_parallel(
&self,
@@ -683,6 +687,36 @@ where
ParallelStateRoot::new(factory, prefix_sets).incremental_root_with_updates()
}
/// Compute state root for the given hashed post state in serial.
fn compute_state_root_serial(
&self,
parent_hash: B256,
hashed_state: &HashedPostState,
state: &EngineApiTreeState<N>,
) -> ProviderResult<(B256, TrieUpdates)> {
let (mut input, block_hash) = self.compute_trie_input(parent_hash, state)?;
// Extend state overlay with current block's sorted state.
input.prefix_sets.extend(hashed_state.construct_prefix_sets());
let sorted_hashed_state = hashed_state.clone_into_sorted();
Arc::make_mut(&mut input.state).extend_ref(&sorted_hashed_state);
let TrieInputSorted { nodes, state, .. } = input;
let prefix_sets = hashed_state.construct_prefix_sets();
let factory = OverlayStateProviderFactory::new(self.provider.clone())
.with_block_hash(Some(block_hash))
.with_trie_overlay(Some(nodes))
.with_hashed_state_overlay(Some(state));
let provider = factory.database_provider_ro()?;
Ok(StateRoot::new(&provider, &provider)
.with_prefix_sets(prefix_sets.freeze())
.root_with_updates()
.map_err(Into::<DatabaseError>::into)?)
}
/// Validates the block after execution.
///
/// This performs:
@@ -777,10 +811,12 @@ where
parent_hash: B256,
state: &EngineApiTreeState<N>,
strategy: StateRootStrategy,
block_access_list: Option<Arc<BlockAccessList>>,
) -> Result<
PayloadHandle<
impl ExecutableTxFor<Evm> + use<N, P, Evm, V, T>,
impl core::error::Error + Send + Sync + 'static + use<N, P, Evm, V, T>,
N::Receipt,
>,
InsertBlockErrorKind,
> {
@@ -806,12 +842,14 @@ where
.record(trie_input_start.elapsed().as_secs_f64());
let spawn_start = Instant::now();
let handle = self.payload_processor.spawn(
env,
txs,
provider_builder,
multiproof_provider_factory,
&self.config,
block_access_list,
);
// record prewarming initialization duration
@@ -824,8 +862,12 @@ where
}
StateRootStrategy::Parallel | StateRootStrategy::Synchronous => {
let start = Instant::now();
let handle =
self.payload_processor.spawn_cache_exclusive(env, txs, provider_builder);
let handle = self.payload_processor.spawn_cache_exclusive(
env,
txs,
provider_builder,
block_access_list,
);
// Record prewarming initialization duration
self.metrics
@@ -874,7 +916,7 @@ where
/// Note: Use state root task only if prefix sets are empty, otherwise proof generation is
/// too expensive because it requires walking all paths in every proof.
const fn plan_state_root_computation(&self) -> StateRootStrategy {
if self.config.state_root_fallback() || !self.config.has_enough_parallelism() {
if self.config.state_root_fallback() {
StateRootStrategy::Synchronous
} else if self.config.use_state_root_task() {
StateRootStrategy::StateRootTask
@@ -1013,8 +1055,7 @@ where
fn spawn_deferred_trie_task(
&self,
block: RecoveredBlock<N::Block>,
output: BlockExecutionOutput<N::Receipt>,
block_number: u64,
execution_outcome: Arc<ExecutionOutcome<N::Receipt>>,
ctx: &TreeCtx<'_, N>,
hashed_state: HashedPostState,
trie_output: TrieUpdates,
@@ -1064,7 +1105,7 @@ where
ExecutedBlock::with_deferred_trie_data(
Arc::new(block),
Arc::new(ExecutionOutcome::from((output, block_number))),
execution_outcome,
deferred_trie_data,
)
}
@@ -1245,4 +1286,21 @@ impl<T: PayloadTypes> BlockOrPayload<T> {
Self::Block(_) => "block",
}
}
/// Returns the block access list if available.
pub const fn block_access_list(&self) -> Option<Result<BlockAccessList, alloy_rlp::Error>> {
// TODO decode and return `BlockAccessList`
None
}
/// Returns the number of transactions in the payload or block.
pub fn transaction_count(&self) -> usize
where
T::ExecutionData: ExecutionPayload,
{
match self {
Self::Payload(payload) => payload.transaction_count(),
Self::Block(block) => block.transaction_count(),
}
}
}

View File

@@ -1,50 +1,58 @@
//! Contains a precompile cache backed by `schnellru::LruMap` (LRU by length).
use alloy_primitives::Bytes;
use parking_lot::Mutex;
use dashmap::DashMap;
use moka::policy::EvictionPolicy;
use reth_evm::precompiles::{DynPrecompile, Precompile, PrecompileInput};
use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
use revm_primitives::Address;
use schnellru::LruMap;
use std::{
collections::HashMap,
hash::{Hash, Hasher},
sync::Arc,
};
use std::{hash::Hash, sync::Arc};
/// Default max cache size for [`PrecompileCache`]
const MAX_CACHE_SIZE: u32 = 10_000;
/// Stores caches for each precompile.
#[derive(Debug, Clone, Default)]
pub struct PrecompileCacheMap<S>(HashMap<Address, PrecompileCache<S>>)
pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>>>)
where
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
impl<S> PrecompileCacheMap<S>
where
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
{
pub(crate) fn cache_for_address(&mut self, address: Address) -> PrecompileCache<S> {
pub(crate) fn cache_for_address(&self, address: Address) -> PrecompileCache<S> {
// Try just using `.get` first to avoid acquiring a write lock.
if let Some(cache) = self.0.get(&address) {
return cache.clone();
}
// Otherwise, fallback to `.entry` and initialize the cache.
//
// This should be very rare as caches for all precompiles will be initialized as soon as
// first EVM is created.
self.0.entry(address).or_default().clone()
}
}
/// Cache for precompiles, for each input stores the result.
///
/// [`LruMap`] requires a mutable reference on `get` since it updates the LRU order,
/// so we use a [`Mutex`] instead of an `RwLock`.
#[derive(Debug, Clone)]
pub struct PrecompileCache<S>(Arc<Mutex<LruMap<CacheKey<S>, CacheEntry>>>)
pub struct PrecompileCache<S>(
moka::sync::Cache<Bytes, CacheEntry<S>, alloy_primitives::map::DefaultHashBuilder>,
)
where
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone;
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
impl<S> Default for PrecompileCache<S>
where
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
{
fn default() -> Self {
Self(Arc::new(Mutex::new(LruMap::new(schnellru::ByLength::new(MAX_CACHE_SIZE)))))
Self(
moka::sync::CacheBuilder::new(MAX_CACHE_SIZE as u64)
.initial_capacity(MAX_CACHE_SIZE as usize)
.eviction_policy(EvictionPolicy::lru())
.build_with_hasher(Default::default()),
)
}
}
@@ -52,63 +60,31 @@ impl<S> PrecompileCache<S>
where
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static,
{
fn get(&self, key: &CacheKeyRef<'_, S>) -> Option<CacheEntry> {
self.0.lock().get(key).cloned()
fn get(&self, input: &[u8], spec: S) -> Option<CacheEntry<S>> {
self.0.get(input).filter(|e| e.spec == spec)
}
/// Inserts the given key and value into the cache, returning the new cache size.
fn insert(&self, key: CacheKey<S>, value: CacheEntry) -> usize {
let mut cache = self.0.lock();
cache.insert(key, value);
cache.len()
}
}
/// Cache key, spec id and precompile call input. spec id is included in the key to account for
/// precompile repricing across fork activations.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey<S>((S, Bytes));
impl<S> CacheKey<S> {
const fn new(spec_id: S, input: Bytes) -> Self {
Self((spec_id, input))
}
}
/// Cache key reference, used to avoid cloning the input bytes when looking up using a [`CacheKey`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheKeyRef<'a, S>((S, &'a [u8]));
impl<'a, S> CacheKeyRef<'a, S> {
const fn new(spec_id: S, input: &'a [u8]) -> Self {
Self((spec_id, input))
}
}
impl<S: PartialEq> PartialEq<CacheKey<S>> for CacheKeyRef<'_, S> {
fn eq(&self, other: &CacheKey<S>) -> bool {
self.0 .0 == other.0 .0 && self.0 .1 == other.0 .1.as_ref()
}
}
impl<'a, S: Hash> Hash for CacheKeyRef<'a, S> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0 .0.hash(state);
self.0 .1.hash(state);
fn insert(&self, input: Bytes, value: CacheEntry<S>) -> usize {
self.0.insert(input, value);
self.0.entry_count() as usize
}
}
/// Cache entry, precompile successful output.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheEntry(PrecompileOutput);
pub struct CacheEntry<S> {
output: PrecompileOutput,
spec: S,
}
impl CacheEntry {
impl<S> CacheEntry<S> {
const fn gas_used(&self) -> u64 {
self.0.gas_used
self.output.gas_used
}
fn to_precompile_result(&self) -> PrecompileResult {
Ok(self.0.clone())
Ok(self.output.clone())
}
}
@@ -190,9 +166,7 @@ where
}
fn call(&self, input: PrecompileInput<'_>) -> PrecompileResult {
let key = CacheKeyRef::new(self.spec_id.clone(), input.data);
if let Some(entry) = &self.cache.get(&key) {
if let Some(entry) = &self.cache.get(input.data, self.spec_id.clone()) {
self.increment_by_one_precompile_cache_hits();
if input.gas >= entry.gas_used() {
return entry.to_precompile_result()
@@ -204,8 +178,10 @@ where
match &result {
Ok(output) => {
let key = CacheKey::new(self.spec_id.clone(), Bytes::copy_from_slice(calldata));
let size = self.cache.insert(key, CacheEntry(output.clone()));
let size = self.cache.insert(
Bytes::copy_from_slice(calldata),
CacheEntry { output: output.clone(), spec: self.spec_id.clone() },
);
self.set_precompile_cache_size_metric(size as f64);
self.increment_by_one_precompile_cache_misses();
}
@@ -246,39 +222,20 @@ impl CachedPrecompileMetrics {
#[cfg(test)]
mod tests {
use std::hash::DefaultHasher;
use super::*;
use reth_evm::{EthEvmFactory, Evm, EvmEnv, EvmFactory};
use reth_revm::db::EmptyDB;
use revm::{context::TxEnv, precompile::PrecompileOutput};
use revm_primitives::hardfork::SpecId;
#[test]
fn test_cache_key_ref_hash() {
let key1 = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
let key2 = CacheKeyRef::new(SpecId::PRAGUE, b"test_input");
assert!(PartialEq::eq(&key2, &key1));
let mut hasher = DefaultHasher::new();
key1.hash(&mut hasher);
let hash1 = hasher.finish();
let mut hasher = DefaultHasher::new();
key2.hash(&mut hasher);
let hash2 = hasher.finish();
assert_eq!(hash1, hash2);
}
#[test]
fn test_precompile_cache_basic() {
let dyn_precompile: DynPrecompile = (|_input: PrecompileInput<'_>| -> PrecompileResult {
Ok(PrecompileOutput {
gas_used: 0,
gas_refunded: 0,
bytes: Bytes::default(),
reverted: false,
gas_refunded: 0,
})
})
.into();
@@ -288,17 +245,16 @@ mod tests {
let output = PrecompileOutput {
gas_used: 50,
gas_refunded: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"cached_result"),
reverted: false,
gas_refunded: 0,
};
let key = CacheKey::new(SpecId::PRAGUE, b"test_input".into());
let expected = CacheEntry(output);
cache.cache.insert(key, expected.clone());
let input = b"test_input";
let expected = CacheEntry { output, spec: SpecId::PRAGUE };
cache.cache.insert(input.into(), expected.clone());
let key = CacheKeyRef::new(SpecId::PRAGUE, b"test_input");
let actual = cache.cache.get(&key).unwrap();
let actual = cache.cache.get(input, SpecId::PRAGUE).unwrap();
assert_eq!(actual, expected);
}
@@ -312,7 +268,7 @@ mod tests {
let address1 = Address::repeat_byte(1);
let address2 = Address::repeat_byte(2);
let mut cache_map = PrecompileCacheMap::default();
let cache_map = PrecompileCacheMap::default();
// create the first precompile with a specific output
let precompile1: DynPrecompile = (PrecompileId::custom("custom"), {
@@ -321,9 +277,9 @@ mod tests {
Ok(PrecompileOutput {
gas_used: 5000,
gas_refunded: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_1"),
reverted: false,
gas_refunded: 0,
})
}
})
@@ -336,9 +292,9 @@ mod tests {
Ok(PrecompileOutput {
gas_used: 7000,
gas_refunded: 0,
bytes: alloy_primitives::Bytes::copy_from_slice(b"output_from_precompile_2"),
reverted: false,
gas_refunded: 0,
})
}
})

View File

@@ -4,7 +4,7 @@ use crate::{
tree::{
payload_validator::{BasicEngineValidator, TreeCtx, ValidationOutcome},
persistence_state::CurrentPersistenceAction,
TreeConfig,
PersistTarget, TreeConfig,
},
};
@@ -285,7 +285,8 @@ impl TestHarness {
let fcu_state = self.fcu_state(block_hash);
let (tx, rx) = oneshot::channel();
self.tree
let _ = self
.tree
.on_engine_message(FromEngine::Request(
BeaconEngineMessage::ForkchoiceUpdated {
state: fcu_state,
@@ -498,7 +499,7 @@ fn test_tree_persist_block_batch() {
// process the message
let msg = test_harness.tree.try_recv_engine_message().unwrap().unwrap();
test_harness.tree.on_engine_message(msg).unwrap();
let _ = test_harness.tree.on_engine_message(msg).unwrap();
// we now should receive the other batch
let msg = test_harness.tree.try_recv_engine_message().unwrap().unwrap();
@@ -577,7 +578,7 @@ async fn test_engine_request_during_backfill() {
.with_backfill_state(BackfillSyncState::Active);
let (tx, rx) = oneshot::channel();
test_harness
let _ = test_harness
.tree
.on_engine_message(FromEngine::Request(
BeaconEngineMessage::ForkchoiceUpdated {
@@ -658,7 +659,7 @@ async fn test_holesky_payload() {
TestHarness::new(HOLESKY.clone()).with_backfill_state(BackfillSyncState::Active);
let (tx, rx) = oneshot::channel();
test_harness
let _ = test_harness
.tree
.on_engine_message(FromEngine::Request(
BeaconEngineMessage::NewPayload {
@@ -883,7 +884,8 @@ async fn test_get_canonical_blocks_to_persist() {
.with_persistence_threshold(persistence_threshold)
.with_memory_block_buffer_target(memory_block_buffer_target);
let blocks_to_persist = test_harness.tree.get_canonical_blocks_to_persist().unwrap();
let blocks_to_persist =
test_harness.tree.get_canonical_blocks_to_persist(PersistTarget::Threshold).unwrap();
let expected_blocks_to_persist_length: usize =
(canonical_head_number - memory_block_buffer_target - last_persisted_block_number)
@@ -902,7 +904,8 @@ async fn test_get_canonical_blocks_to_persist() {
assert!(test_harness.tree.state.tree_state.sealed_header_by_hash(&fork_block_hash).is_some());
let blocks_to_persist = test_harness.tree.get_canonical_blocks_to_persist().unwrap();
let blocks_to_persist =
test_harness.tree.get_canonical_blocks_to_persist(PersistTarget::Threshold).unwrap();
assert_eq!(blocks_to_persist.len(), expected_blocks_to_persist_length);
// check that the fork block is not included in the blocks to persist
@@ -981,7 +984,7 @@ async fn test_engine_tree_live_sync_transition_required_blocks_requested() {
let backfill_tip_block = main_chain[(backfill_finished_block_number - 1) as usize].clone();
// add block to mock provider to enable persistence clean up.
test_harness.provider.add_block(backfill_tip_block.hash(), backfill_tip_block.into_block());
test_harness.tree.on_engine_message(FromEngine::Event(backfill_finished)).unwrap();
let _ = test_harness.tree.on_engine_message(FromEngine::Event(backfill_finished)).unwrap();
let event = test_harness.from_tree_rx.recv().await.unwrap();
match event {
@@ -991,7 +994,7 @@ async fn test_engine_tree_live_sync_transition_required_blocks_requested() {
_ => panic!("Unexpected event: {event:#?}"),
}
test_harness
let _ = test_harness
.tree
.on_engine_message(FromEngine::DownloadedBlocks(vec![main_chain
.last()
@@ -1047,7 +1050,7 @@ async fn test_fcu_with_canonical_ancestor_updates_latest_block() {
// Send FCU to the canonical ancestor
let (tx, rx) = oneshot::channel();
test_harness
let _ = test_harness
.tree
.on_engine_message(FromEngine::Request(
BeaconEngineMessage::ForkchoiceUpdated {
@@ -1943,4 +1946,53 @@ mod forkchoice_updated_tests {
.unwrap();
assert!(result.is_some(), "OpStack should handle canonical head");
}
/// Test that engine termination persists all blocks and signals completion.
#[test]
fn test_engine_termination_with_everything_persisted() {
let chain_spec = MAINNET.clone();
let mut test_block_builder = TestBlockBuilder::eth().with_chain_spec((*chain_spec).clone());
// Create 10 blocks to persist
let blocks: Vec<_> = test_block_builder.get_executed_blocks(1..11).collect();
let canonical_tip = blocks.last().unwrap().recovered_block().number;
let test_harness = TestHarness::new(chain_spec).with_blocks(blocks);
// Create termination channel
let (terminate_tx, mut terminate_rx) = oneshot::channel();
let to_tree_tx = test_harness.to_tree_tx.clone();
let action_rx = test_harness.action_rx;
// Spawn tree in background thread
std::thread::Builder::new()
.name("Engine Task".to_string())
.spawn(|| test_harness.tree.run())
.unwrap();
// Send terminate request
to_tree_tx
.send(FromEngine::Event(FromOrchestrator::Terminate { tx: terminate_tx }))
.unwrap();
// Handle persistence actions until termination completes
let mut last_persisted_number = 0;
loop {
if terminate_rx.try_recv().is_ok() {
break;
}
if let Ok(PersistenceAction::SaveBlocks(saved_blocks, sender)) =
action_rx.recv_timeout(std::time::Duration::from_millis(100))
{
if let Some(last) = saved_blocks.last() {
last_persisted_number = last.recovered_block().number;
}
sender.send(saved_blocks.last().map(|b| b.recovered_block().num_hash())).unwrap();
}
}
// Ensure we persisted right to the tip
assert_eq!(last_persisted_number, canonical_tip);
}
}

View File

@@ -16,7 +16,7 @@ reth-primitives-traits.workspace = true
reth-errors.workspace = true
reth-chainspec.workspace = true
reth-fs-util.workspace = true
reth-engine-primitives.workspace = true
reth-engine-primitives = { workspace = true, features = ["std"] }
reth-engine-tree.workspace = true
reth-evm.workspace = true
reth-revm.workspace = true
@@ -27,9 +27,6 @@ reth-payload-primitives.workspace = true
alloy-rpc-types-engine.workspace = true
alloy-consensus.workspace = true
# revm
revm.workspace = true
# async
tokio = { workspace = true, default-features = false }
tokio-util.workspace = true

View File

@@ -283,12 +283,8 @@ where
let mut state = State::builder()
.with_database_ref(StateProviderDatabase::new(&state_provider))
.with_bundle_update()
.with_bal_builder()
.build();
state.bal_state.bal_index = 0;
state.bal_state.bal_builder = Some(revm::state::bal::Bal::new());
let ctx = evm_config.context_for_block(&reorg_target).map_err(RethError::other)?;
let evm = evm_config.evm_for_block(&mut state, &reorg_target).map_err(RethError::other)?;
let mut builder = evm_config.create_block_builder(evm, &reorg_target_parent, ctx);

View File

@@ -150,6 +150,12 @@ where
let era1_id = Era1Id::new(&config.network, start_block, block_count as u32)
.with_hash(historical_root);
let era1_id = if config.max_blocks_per_file == MAX_BLOCKS_PER_ERA1 as u64 {
era1_id
} else {
era1_id.with_era_count()
};
debug!("Final file name {}", era1_id.to_file_name());
let file_path = config.dir.join(era1_id.to_file_name());
let file = std::fs::File::create(&file_path)?;

View File

@@ -116,7 +116,7 @@ where
/// these stages that this work has already been done. Otherwise, there might be some conflict with
/// database integrity.
pub fn save_stage_checkpoints<P>(
provider: &P,
provider: P,
from: BlockNumber,
to: BlockNumber,
processed: u64,
@@ -170,18 +170,14 @@ where
<P as NodePrimitivesProvider>::Primitives: NodePrimitives<BlockHeader = BH, BlockBody = BB>,
{
let reader = open(meta)?;
let iter =
reader
.iter()
.map(Box::new(decode)
as Box<dyn Fn(Result<BlockTuple, E2sError>) -> eyre::Result<(BH, BB)>>);
let iter = reader.iter().map(decode as fn(_) -> _);
let iter = ProcessIter { iter, era: meta };
process_iter(iter, writer, provider, hash_collector, block_numbers)
}
type ProcessInnerIter<R, BH, BB> =
Map<BlockTupleIterator<R>, Box<dyn Fn(Result<BlockTuple, E2sError>) -> eyre::Result<(BH, BB)>>>;
Map<BlockTupleIterator<R>, fn(Result<BlockTuple, E2sError>) -> eyre::Result<(BH, BB)>>;
/// An iterator that wraps era file extraction. After the final item [`EraMeta::mark_as_processed`]
/// is called to ensure proper cleanup.
@@ -309,7 +305,7 @@ where
writer.append_header(&header, &hash)?;
// Write bodies to database.
provider.append_block_bodies(vec![(header.number(), Some(body))])?;
provider.append_block_bodies(vec![(header.number(), Some(&body))])?;
hash_collector.insert(hash, number)?;
}

View File

@@ -24,7 +24,7 @@ fn test_export_with_genesis_only() {
assert!(file_path.exists(), "Exported file should exist on disk");
let file_name = file_path.file_name().unwrap().to_str().unwrap();
assert!(
file_name.starts_with("mainnet-00000-00001-"),
file_name.starts_with("mainnet-00000-"),
"File should have correct prefix with era format"
);
assert!(file_name.ends_with(".era1"), "File should have correct extension");

View File

@@ -30,8 +30,11 @@ pub trait EraFileFormat: Sized {
/// Era file identifiers
pub trait EraFileId: Clone {
/// Convert to standardized file name
fn to_file_name(&self) -> String;
/// File type for this identifier
const FILE_TYPE: EraFileType;
/// Number of items, slots for `era`, blocks for `era1`, per era
const ITEMS_PER_ERA: u64;
/// Get the network name
fn network_name(&self) -> &str;
@@ -41,6 +44,43 @@ pub trait EraFileId: Clone {
/// Get the count of items
fn count(&self) -> u32;
/// Get the optional hash identifier
fn hash(&self) -> Option<[u8; 4]>;
/// Whether to include era count in filename
fn include_era_count(&self) -> bool;
/// Calculate era number
fn era_number(&self) -> u64 {
self.start_number() / Self::ITEMS_PER_ERA
}
/// Calculate the number of eras spanned per file.
///
/// If the user can decide how many slots/blocks per era file there are, we need to calculate
/// it. Most of the time it should be 1, but it can never be more than 2 eras per file
/// as there is a maximum of 8192 slots/blocks per era file.
fn era_count(&self) -> u64 {
if self.count() == 0 {
return 0;
}
let first_era = self.era_number();
let last_number = self.start_number() + self.count() as u64 - 1;
let last_era = last_number / Self::ITEMS_PER_ERA;
last_era - first_era + 1
}
/// Convert to standardized file name.
fn to_file_name(&self) -> String {
Self::FILE_TYPE.format_filename(
self.network_name(),
self.era_number(),
self.hash(),
self.include_era_count(),
self.era_count(),
)
}
}
/// [`StreamReader`] for reading era-format files
@@ -154,6 +194,37 @@ impl EraFileType {
}
}
/// Generate era file name.
///
/// Standard format: `<config-name>-<era-number>-<short-historical-root>.<ext>`
/// See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md#file-name>
///
/// With era count (for custom exports):
/// `<config-name>-<era-number>-<era-count>-<short-historical-root>.<ext>`
pub fn format_filename(
&self,
network_name: &str,
era_number: u64,
hash: Option<[u8; 4]>,
include_era_count: bool,
era_count: u64,
) -> String {
let hash = format_hash(hash);
if include_era_count {
format!(
"{}-{:05}-{:05}-{}{}",
network_name,
era_number,
era_count,
hash,
self.extension()
)
} else {
format!("{}-{:05}-{}{}", network_name, era_number, hash, self.extension())
}
}
/// Detect file type from URL
/// By default, it assumes `Era` type
pub fn from_url(url: &str) -> Self {
@@ -164,3 +235,11 @@ impl EraFileType {
}
}
}
/// Format hash as hex string, or placeholder if none
pub fn format_hash(hash: Option<[u8; 4]>) -> String {
match hash {
Some(h) => format!("{:02x}{:02x}{:02x}{:02x}", h[0], h[1], h[2], h[3]),
None => "00000000".to_string(),
}
}

View File

@@ -59,11 +59,36 @@
//! Ok(())
//! }
//! ```
use crate::e2s::{error::E2sError, types::Entry};
use snap::{read::FrameDecoder, write::FrameEncoder};
use std::io::{Read, Write};
/// Maximum allowed decompressed size for a signed beacon block SSZ payload.
const MAX_DECOMPRESSED_SIGNED_BEACON_BLOCK_BYTES: usize = 256 * 1024 * 1024; // 256 MiB
/// Maximum allowed decompressed size for a beacon state SSZ payload.
const MAX_DECOMPRESSED_BEACON_STATE_BYTES: usize = 2 * 1024 * 1024 * 1024; // 2 GiB
fn decompress_snappy_bounded(
compressed: &[u8],
max_decompressed_bytes: usize,
what: &str,
) -> Result<Vec<u8>, E2sError> {
let mut decoder = FrameDecoder::new(compressed).take(max_decompressed_bytes as u64);
let mut decompressed = Vec::new();
Read::read_to_end(&mut decoder, &mut decompressed)
.map_err(|e| E2sError::SnappyDecompression(format!("Failed to decompress {what}: {e}")))?;
if decompressed.len() >= max_decompressed_bytes {
return Err(E2sError::SnappyDecompression(format!(
"Failed to decompress {what}: decompressed data exceeded limit of {max_decompressed_bytes} bytes"
)));
}
Ok(decompressed)
}
/// `CompressedSignedBeaconBlock` record type: [0x01, 0x00]
pub const COMPRESSED_SIGNED_BEACON_BLOCK: [u8; 2] = [0x01, 0x00];
@@ -104,13 +129,11 @@ impl CompressedSignedBeaconBlock {
/// Decompress to get the original ssz-encoded signed beacon block
pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
let mut decoder = FrameDecoder::new(self.data.as_slice());
let mut decompressed = Vec::new();
Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
E2sError::SnappyDecompression(format!("Failed to decompress signed beacon block: {e}"))
})?;
Ok(decompressed)
decompress_snappy_bounded(
self.data.as_slice(),
MAX_DECOMPRESSED_SIGNED_BEACON_BLOCK_BYTES,
"signed beacon block",
)
}
/// Convert to an [`Entry`]
@@ -168,13 +191,11 @@ impl CompressedBeaconState {
/// Decompress to get the original ssz-encoded beacon state
pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
let mut decoder = FrameDecoder::new(self.data.as_slice());
let mut decompressed = Vec::new();
Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
E2sError::SnappyDecompression(format!("Failed to decompress beacon state: {e}"))
})?;
Ok(decompressed)
decompress_snappy_bounded(
self.data.as_slice(),
MAX_DECOMPRESSED_BEACON_STATE_BYTES,
"beacon state",
)
}
/// Convert to an [`Entry`]
@@ -260,4 +281,15 @@ mod tests {
let result = CompressedBeaconState::from_entry(&invalid_entry);
assert!(result.is_err());
}
#[test]
fn test_bounded_decompression_rejects_oversized_output() {
let ssz_data = vec![42u8; 1024];
let compressed = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
let err =
decompress_snappy_bounded(compressed.data.as_slice(), 100, "beacon state").unwrap_err();
assert!(format!("{err:?}").contains("exceeded limit"));
}
}

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