Compare commits

...

158 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
214 changed files with 5611 additions and 2367 deletions

View File

@@ -24,7 +24,8 @@ jobs:
prepare-hive:
if: github.repository == 'paradigmxyz/reth'
timeout-minutes: 45
runs-on: depot-ubuntu-latest-16
runs-on:
group: Reth
steps:
- uses: actions/checkout@v6
- name: Checkout hive tests
@@ -178,7 +179,8 @@ jobs:
- prepare-reth
- prepare-hive
name: run ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
runs-on: depot-ubuntu-latest-16
runs-on:
group: Reth
permissions:
issues: write
steps:

View File

@@ -277,7 +277,7 @@ jobs:
- 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

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

826
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -497,33 +497,33 @@ 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.25.0", default-features = false }
@@ -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"
@@ -693,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"
@@ -730,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"

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

@@ -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

@@ -103,6 +103,7 @@ asm-keccak = [
"reth-node-ethereum/asm-keccak",
]
keccak-cache-global = [
"reth-node-core/keccak-cache-global",
"reth-node-ethereum/keccak-cache-global",
]
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();

View File

@@ -970,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;

View File

@@ -301,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

@@ -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

@@ -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

@@ -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})"
)]

View File

@@ -89,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) => {
@@ -107,7 +107,7 @@ where
debug!(
target: "consensus::debug-client",
url=%self.url,
"Re-estbalishing block subscription",
"Re-establishing block subscription",
);
}
}

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();
@@ -530,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();

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

@@ -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

@@ -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 = || {
@@ -529,6 +530,7 @@ mod tests {
let _result = metrics.execute_metered::<_, EmptyDB>(
executor,
input.clone_transactions_recovered().map(Ok::<_, BlockExecutionError>),
input.transaction_count(),
state_hook,
);
@@ -585,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

@@ -64,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;
@@ -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.
@@ -2569,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);

View File

@@ -2,16 +2,122 @@
use alloy_consensus::constants::KECCAK_EMPTY;
use alloy_eip7928::BlockAccessList;
use alloy_primitives::{keccak256, U256};
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 fn bal_to_hashed_post_state<P>(
pub(crate) fn bal_to_hashed_post_state<P>(
bal: &BlockAccessList,
provider: &P,
provider: P,
) -> Result<HashedPostState, ProviderError>
where
P: AccountReader,
@@ -20,7 +126,10 @@ where
for account_changes in bal {
let address = account_changes.address;
let hashed_address = keccak256(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);
@@ -39,12 +148,14 @@ where
None
};
// Only fetch account from provider if we're missing any field
let existing_account = if balance.is_none() || nonce.is_none() || code_hash.is_none() {
provider.basic_account(&address)?
} 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 {
@@ -58,6 +169,7 @@ where
}),
};
let hashed_address = keccak256(address);
hashed_state.accounts.insert(hashed_address, Some(account));
// Process storage changes
@@ -75,9 +187,7 @@ where
}
}
if !storage_map.storage.is_empty() {
hashed_state.storages.insert(hashed_address, storage_map);
}
hashed_state.storages.insert(hashed_address, storage_map);
}
}
@@ -315,4 +425,117 @@ mod tests {
// 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,11 +3,11 @@
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,
@@ -30,7 +30,9 @@ use reth_evm::{
};
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::{
@@ -44,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,7 +54,7 @@ use std::{
},
time::Instant,
};
use tracing::{debug, debug_span, error, instrument, warn, Span};
use tracing::{debug, debug_span, instrument, warn, Span};
pub mod bal;
mod configured_sparse_trie;
@@ -236,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);
@@ -257,49 +285,27 @@ 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());
// Handle BAL-based optimization if available
let prewarm_handle = if let Some(bal) = bal {
// When BAL is present, skip spawning prewarm tasks entirely and send BAL to multiproof
debug!(target: "engine::tree::payload_processor", "BAL present, skipping prewarm tasks");
// Send BAL message immediately to MultiProofTask
if let Some(ref sender) = to_multi_proof &&
let Err(err) = sender.send(MultiProofMessage::BlockAccessList(bal))
{
// In this case state root validation will simply fail
error!(target: "engine::tree::payload_processor", ?err, "Failed to send BAL to MultiProofTask");
}
// Spawn minimal cache-only task without prewarming
self.spawn_caching_with(
env,
prewarm_rx,
transaction_count_hint,
provider_builder.clone(),
None, // Don't send proof targets when BAL is present
)
} else {
// Normal path: spawn with full prewarming
self.spawn_caching_with(
env,
prewarm_rx,
transaction_count_hint,
provider_builder.clone(),
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();
// 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);
});
@@ -327,13 +333,14 @@ where
env: ExecutionEnv<Evm>,
transactions: I,
provider_builder: StateProviderBuilder<N, P>,
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,
@@ -407,6 +414,7 @@ where
transaction_count_hint: usize,
provider_builder: StateProviderBuilder<N, P>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
bal: Option<Arc<BlockAccessList>>,
) -> CacheTaskHandle<N::Receipt>
where
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
@@ -417,20 +425,13 @@ where
transactions = mpsc::channel().1;
}
let (saved_cache, cache, cache_metrics) = if self.disable_state_cache {
(None, None, None)
} else {
let saved_cache = self.cache_for(env.parent_hash);
let cache = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
(Some(saved_cache), Some(cache), Some(cache_metrics))
};
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)),
@@ -451,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.
@@ -641,12 +647,12 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
/// Returns a clone of the caches used by prewarming
pub(super) fn caches(&self) -> Option<StateExecutionCache> {
self.prewarm_handle.cache.clone()
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) -> Option<CachedStateMetrics> {
self.prewarm_handle.cache_metrics.clone()
self.prewarm_handle.saved_cache.as_ref().map(|cache| cache.metrics().clone())
}
/// Terminates the pre-warming transaction processing.
@@ -683,9 +689,7 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
#[derive(Debug)]
pub(crate) struct CacheTaskHandle<R> {
/// The shared cache the task operates with.
cache: Option<StateExecutionCache>,
/// Metrics for the caches
cache_metrics: Option<CachedStateMetrics>,
saved_cache: Option<SavedCache>,
/// Channel to the spawned prewarm task if any
to_prewarm_task: Option<std::sync::mpsc::Sender<PrewarmTaskEvent<R>>>,
}

View File

@@ -3,15 +3,12 @@
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;
@@ -23,7 +20,6 @@ use reth_trie_parallel::{
proof::ParallelProof,
proof_task::{
AccountMultiproofInput, ProofResultContext, ProofResultMessage, ProofWorkerHandle,
StorageProofInput,
},
};
use std::{collections::BTreeMap, mem, ops::DerefMut, sync::Arc, time::Instant};
@@ -205,103 +201,38 @@ 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.
@@ -378,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() {
if input.proof_targets.is_empty() {
trace!(
sequence_number = input.proof_sequence_number(),
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.
@@ -740,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();
@@ -809,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);
@@ -967,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
@@ -1200,7 +1053,7 @@ impl MultiProofTask {
}
// Convert BAL to HashedPostState and process it
match bal_to_hashed_post_state(&bal, &provider) {
match bal_to_hashed_post_state(&bal, provider) {
Ok(hashed_state) => {
debug!(
target: "engine::tree::payload_processor::multiproof",
@@ -1646,12 +1499,13 @@ fn estimate_evm_state_targets(state: &EvmState) -> usize {
#[cfg(test)]
mod tests {
use super::*;
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};
@@ -1683,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]
@@ -2616,7 +2485,7 @@ mod tests {
use revm_state::Account;
let test_provider_factory = create_test_provider_factory();
let test_provider = test_provider_factory.latest().unwrap();
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
@@ -2836,7 +2705,7 @@ mod tests {
#[test]
fn test_bal_message_processing() {
let test_provider_factory = create_test_provider_factory();
let test_provider = test_provider_factory.latest().unwrap();
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

View File

@@ -14,26 +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_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},
@@ -43,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> {
@@ -164,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;
@@ -291,6 +299,86 @@ where
}
}
/// 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.
///
/// This will execute the transactions until all transactions have been processed or the task
@@ -302,13 +390,22 @@ 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>,
mode: PrewarmMode<Tx>,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
) {
// spawn execution tasks.
self.spawn_all(pending, actions_tx);
) 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_execution_outcome = None;
let mut finished_execution = false;
@@ -522,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);
@@ -536,63 +634,166 @@ where
}
/// Spawns a worker task for transaction execution and returns its sender channel.
fn spawn_worker<Tx>(
&self,
idx: usize,
executor: &WorkloadExecutor,
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.
@@ -639,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

@@ -435,8 +435,7 @@ where
}
// Execute the block and handle any execution errors
let (output, senders) = match 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),
};
@@ -603,7 +602,7 @@ where
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>>,
@@ -644,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();
@@ -862,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
@@ -1288,4 +1292,15 @@ impl<T: PayloadTypes> BlockOrPayload<T> {
// 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

@@ -2,6 +2,7 @@
use alloy_primitives::Bytes;
use dashmap::DashMap;
use moka::policy::EvictionPolicy;
use reth_evm::precompiles::{DynPrecompile, Precompile, PrecompileInput};
use revm::precompile::{PrecompileId, PrecompileOutput, PrecompileResult};
use revm_primitives::Address;
@@ -49,6 +50,7 @@ where
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()),
)
}

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

@@ -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"));
}
}

View File

@@ -252,7 +252,7 @@ impl CompressedBody {
let mut encoder = FrameEncoder::new(&mut compressed);
Write::write_all(&mut encoder, rlp_data).map_err(|e| {
E2sError::SnappyCompression(format!("Failed to compress header: {e}"))
E2sError::SnappyCompression(format!("Failed to compress body: {e}"))
})?;
encoder.flush().map_err(|e| {
@@ -339,7 +339,7 @@ impl CompressedReceipts {
let mut encoder = FrameEncoder::new(&mut compressed);
Write::write_all(&mut encoder, rlp_data).map_err(|e| {
E2sError::SnappyCompression(format!("Failed to compress header: {e}"))
E2sError::SnappyCompression(format!("Failed to compress receipts: {e}"))
})?;
encoder.flush().map_err(|e| {

View File

@@ -1,4 +1,8 @@
use crate::{interface::Commands, Cli};
use crate::{
interface::{Commands, NoSubCmd},
Cli,
};
use clap::Subcommand;
use eyre::{eyre, Result};
use reth_chainspec::{ChainSpec, EthChainSpec, Hardforks};
use reth_cli::chainspec::ChainSpecParser;
@@ -18,20 +22,26 @@ use std::{fmt, sync::Arc};
/// A wrapper around a parsed CLI that handles command execution.
#[derive(Debug)]
pub struct CliApp<Spec: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> {
cli: Cli<Spec, Ext, Rpc>,
pub struct CliApp<
Spec: ChainSpecParser,
Ext: clap::Args + fmt::Debug,
Rpc: RpcModuleValidator,
SubCmd: Subcommand + fmt::Debug = NoSubCmd,
> {
cli: Cli<Spec, Ext, Rpc, SubCmd>,
runner: Option<CliRunner>,
layers: Option<Layers>,
guard: Option<FileWorkerGuard>,
}
impl<C, Ext, Rpc> CliApp<C, Ext, Rpc>
impl<C, Ext, Rpc, SubCmd> CliApp<C, Ext, Rpc, SubCmd>
where
C: ChainSpecParser,
Ext: clap::Args + fmt::Debug,
Rpc: RpcModuleValidator,
SubCmd: ExtendedCommand + Subcommand + fmt::Debug,
{
pub(crate) fn new(cli: Cli<C, Ext, Rpc>) -> Self {
pub(crate) fn new(cli: Cli<C, Ext, Rpc, SubCmd>) -> Self {
Self { cli, runner: None, layers: Some(Layers::new()), guard: None }
}
@@ -98,9 +108,9 @@ where
self.init_tracing(&runner)?;
// Install the prometheus recorder to be sure to record all metrics
let _ = install_prometheus_recorder();
install_prometheus_recorder();
run_commands_with::<C, Ext, Rpc, N>(self.cli, runner, components, launcher)
run_commands_with::<C, Ext, Rpc, N, SubCmd>(self.cli, runner, components, launcher)
}
/// Initializes tracing with the configured options.
@@ -117,8 +127,8 @@ where
/// Run CLI commands with the provided runner, components and launcher.
/// This is the shared implementation used by both `CliApp` and Cli methods.
pub(crate) fn run_commands_with<C, Ext, Rpc, N>(
cli: Cli<C, Ext, Rpc>,
pub(crate) fn run_commands_with<C, Ext, Rpc, N, SubCmd>(
cli: Cli<C, Ext, Rpc, SubCmd>,
runner: CliRunner,
components: impl CliComponentsBuilder<N>,
launcher: impl AsyncFnOnce(
@@ -131,6 +141,7 @@ where
Ext: clap::Args + fmt::Debug,
Rpc: RpcModuleValidator,
N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
SubCmd: ExtendedCommand + Subcommand + fmt::Debug,
{
match cli.command {
Commands::Node(command) => {
@@ -167,9 +178,19 @@ where
#[cfg(feature = "dev")]
Commands::TestVectors(command) => runner.run_until_ctrl_c(command.execute()),
Commands::ReExecute(command) => runner.run_until_ctrl_c(command.execute::<N>(components)),
Commands::Ext(command) => command.execute(runner),
}
}
/// A trait for extension subcommands that can be added to the CLI.
///
/// Consumers implement this trait for their custom subcommands to define
/// how they should be executed.
pub trait ExtendedCommand {
/// Execute the extension command with the provided CLI runner.
fn execute(self, runner: CliRunner) -> Result<()>;
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -37,10 +37,11 @@ pub struct Cli<
C: ChainSpecParser = EthereumChainSpecParser,
Ext: clap::Args + fmt::Debug = NoArgs,
Rpc: RpcModuleValidator = DefaultRpcModuleValidator,
SubCmd: Subcommand + fmt::Debug = NoSubCmd,
> {
/// The command to run
#[command(subcommand)]
pub command: Commands<C, Ext>,
pub command: Commands<C, Ext, SubCmd>,
/// The logging configuration for the CLI.
#[command(flatten)]
@@ -71,15 +72,18 @@ impl Cli {
}
}
impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator> Cli<C, Ext, Rpc> {
impl<
C: ChainSpecParser,
Ext: clap::Args + fmt::Debug,
Rpc: RpcModuleValidator,
SubCmd: crate::app::ExtendedCommand + Subcommand + fmt::Debug,
> Cli<C, Ext, Rpc, SubCmd>
{
/// Configures the CLI and returns a [`CliApp`] instance.
///
/// This method is used to prepare the CLI for execution by wrapping it in a
/// [`CliApp`] that can be further configured before running.
pub fn configure(self) -> CliApp<C, Ext, Rpc>
where
C: ChainSpecParser<ChainSpec = ChainSpec>,
{
pub fn configure(self) -> CliApp<C, Ext, Rpc, SubCmd> {
CliApp::new(self)
}
@@ -208,10 +212,10 @@ impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator>
let _guard = self.init_tracing(&runner, Layers::new())?;
// Install the prometheus recorder to be sure to record all metrics
let _ = install_prometheus_recorder();
install_prometheus_recorder();
// Use the shared standalone function to avoid duplication
run_commands_with::<C, Ext, Rpc, N>(self, runner, components, launcher)
run_commands_with::<C, Ext, Rpc, N, SubCmd>(self, runner, components, launcher)
}
/// Initializes tracing with the configured options.
@@ -245,7 +249,11 @@ impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, Rpc: RpcModuleValidator>
/// Commands to be executed
#[derive(Debug, Subcommand)]
pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
pub enum Commands<
C: ChainSpecParser,
Ext: clap::Args + fmt::Debug,
SubCmd: Subcommand + fmt::Debug = NoSubCmd,
> {
/// Start the node
#[command(name = "node")]
Node(Box<node::NodeCommand<C, Ext>>),
@@ -291,9 +299,27 @@ pub enum Commands<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> {
/// Re-execute blocks in parallel to verify historical sync correctness.
#[command(name = "re-execute")]
ReExecute(re_execute::Command<C>),
/// Extension subcommands provided by consumers.
#[command(flatten)]
Ext(SubCmd),
}
impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Commands<C, Ext> {
/// A no-op subcommand type for when no extension subcommands are needed.
///
/// This is the default type parameter for `Commands` when consumers don't need
/// to add custom subcommands.
#[derive(Debug, Subcommand)]
pub enum NoSubCmd {}
impl crate::app::ExtendedCommand for NoSubCmd {
fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
match self {}
}
}
impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug, SubCmd: Subcommand + fmt::Debug>
Commands<C, Ext, SubCmd>
{
/// Returns the underlying chain being used for commands
pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
match self {
@@ -313,6 +339,7 @@ impl<C: ChainSpecParser, Ext: clap::Args + fmt::Debug> Commands<C, Ext> {
Self::Config(_) => None,
Self::Prune(cmd) => cmd.chain_spec(),
Self::ReExecute(cmd) => cmd.chain_spec(),
Self::Ext(_) => None,
}
}
}
@@ -536,4 +563,100 @@ mod tests {
_ => panic!("Expected Stage command"),
};
}
#[test]
fn test_extensible_subcommands() {
use crate::app::ExtendedCommand;
use reth_cli_runner::CliRunner;
use reth_rpc_server_types::DefaultRpcModuleValidator;
use std::sync::atomic::{AtomicBool, Ordering};
#[derive(Debug, Subcommand)]
enum CustomCommands {
/// A custom hello command
#[command(name = "hello")]
Hello {
/// Name to greet
#[arg(long)]
name: String,
},
/// Another custom command
#[command(name = "goodbye")]
Goodbye,
}
static EXECUTED: AtomicBool = AtomicBool::new(false);
impl ExtendedCommand for CustomCommands {
fn execute(self, _runner: CliRunner) -> eyre::Result<()> {
match self {
Self::Hello { name } => {
assert_eq!(name, "world");
EXECUTED.store(true, Ordering::SeqCst);
Ok(())
}
Self::Goodbye => Ok(()),
}
}
}
// Test parsing the custom "hello" command
let cli = Cli::<
EthereumChainSpecParser,
NoArgs,
DefaultRpcModuleValidator,
CustomCommands,
>::try_parse_from(["reth", "hello", "--name", "world"])
.unwrap();
match &cli.command {
Commands::Ext(CustomCommands::Hello { name }) => {
assert_eq!(name, "world");
}
_ => panic!("Expected Ext(Hello) command"),
}
// Test parsing the custom "goodbye" command
let cli = Cli::<
EthereumChainSpecParser,
NoArgs,
DefaultRpcModuleValidator,
CustomCommands,
>::try_parse_from(["reth", "goodbye"])
.unwrap();
match &cli.command {
Commands::Ext(CustomCommands::Goodbye) => {}
_ => panic!("Expected Ext(Goodbye) command"),
}
// Test that built-in commands still work alongside custom ones
let cli = Cli::<
EthereumChainSpecParser,
NoArgs,
DefaultRpcModuleValidator,
CustomCommands,
>::try_parse_from(["reth", "node"])
.unwrap();
match &cli.command {
Commands::Node(_) => {}
_ => panic!("Expected Node command"),
}
// Test executing the custom command
let cli = Cli::<
EthereumChainSpecParser,
NoArgs,
DefaultRpcModuleValidator,
CustomCommands,
>::try_parse_from(["reth", "hello", "--name", "world"])
.unwrap();
if let Commands::Ext(cmd) = cli.command {
let runner = CliRunner::try_default_runtime().unwrap();
cmd.execute(runner).unwrap();
assert!(EXECUTED.load(Ordering::SeqCst), "Custom command should have been executed");
}
}
}

View File

@@ -14,8 +14,8 @@ pub mod app;
pub mod chainspec;
pub mod interface;
pub use app::CliApp;
pub use interface::{Cli, Commands};
pub use app::{CliApp, ExtendedCommand};
pub use interface::{Cli, Commands, NoSubCmd};
#[cfg(test)]
mod test {

View File

@@ -87,9 +87,7 @@ fn verify_receipts<R: Receipt>(
logs_bloom,
expected_receipts_root,
expected_logs_bloom,
)?;
Ok(())
)
}
/// Compare the calculated receipts root with the expected receipts root, also compare

View File

@@ -91,6 +91,7 @@ asm-keccak = [
]
keccak-cache-global = [
"alloy-primitives/keccak-cache-global",
"reth-node-core/keccak-cache-global",
]
js-tracer = [
"reth-node-builder/js-tracer",

View File

@@ -153,9 +153,9 @@ where
let PayloadConfig { parent_header, attributes } = config;
let state_provider = client.state_by_block_hash(parent_header.hash())?;
let state = StateProviderDatabase::new(&state_provider);
let state = StateProviderDatabase::new(state_provider.as_ref());
let mut db =
State::builder().with_database(cached_reads.as_db_mut(state)).with_bundle_update().build();
State::builder().with_database_ref(cached_reads.as_db(state)).with_bundle_update().build();
let mut builder = evm_config
.builder_for_next_block(
@@ -211,6 +211,8 @@ where
let is_osaka = chain_spec.is_osaka_active_at_timestamp(attributes.timestamp);
let withdrawals_rlp_length = attributes.withdrawals().length();
while let Some(pool_tx) = best_txs.next() {
// ensure we still have capacity for this transaction
if cumulative_gas_used + pool_tx.gas_limit() > block_gas_limit {
@@ -232,10 +234,10 @@ where
// convert tx to a signed transaction
let tx = pool_tx.to_consensus();
let estimated_block_size_with_tx = block_transactions_rlp_length +
tx.inner().length() +
attributes.withdrawals().length() +
1024; // 1Kb of overhead for the block header
let tx_rlp_len = tx.inner().length();
let estimated_block_size_with_tx =
block_transactions_rlp_length + tx_rlp_len + withdrawals_rlp_length + 1024; // 1Kb of overhead for the block header
if is_osaka && estimated_block_size_with_tx > MAX_RLP_BLOCK_SIZE {
best_txs.mark_invalid(
@@ -336,7 +338,7 @@ where
}
}
block_transactions_rlp_length += tx.inner().length();
block_transactions_rlp_length += tx_rlp_len;
// update and add to total fees
let miner_fee =
@@ -358,7 +360,8 @@ where
return Ok(BuildOutcome::Aborted { fees: total_fees, cached_reads })
}
let BlockBuilderOutcome { execution_result, block, .. } = builder.finish(&state_provider)?;
let BlockBuilderOutcome { execution_result, block, .. } =
builder.finish(state_provider.as_ref())?;
let requests = chain_spec
.is_prague_active_at_timestamp(attributes.timestamp)

View File

@@ -258,10 +258,7 @@ impl<T: TxTy> IsTyped2718 for Receipt<T> {
impl<T: TxTy> InMemorySize for Receipt<T> {
fn size(&self) -> usize {
self.tx_type.size() +
core::mem::size_of::<bool>() +
core::mem::size_of::<u64>() +
self.logs.iter().map(|log| log.size()).sum::<usize>()
size_of::<Self>() + self.logs.iter().map(|log| log.size()).sum::<usize>()
}
}

View File

@@ -81,6 +81,7 @@ arbitrary = [
]
keccak-cache-global = [
"reth-node-ethereum?/keccak-cache-global",
"reth-node-core?/keccak-cache-global",
]
test-utils = [
"reth-chainspec/test-utils",

View File

@@ -61,8 +61,6 @@ pub use alloy_evm::{
*,
};
pub use alloy_evm::block::state_changes as state_change;
/// A complete configuration of EVM for Reth.
///
/// This trait encapsulates complete configuration required for transaction execution and block
@@ -258,7 +256,7 @@ pub trait ConfigureEvm: Clone + Debug + Send + Sync + Unpin {
attributes: Self::NextBlockEnvCtx,
) -> Result<ExecutionCtxFor<'_, Self>, Self::Error>;
/// Returns a [`TxEnv`] from a transaction and [`Address`].
/// Returns a [`TxEnv`] from a transaction.
fn tx_env(&self, transaction: impl IntoTxEnv<TxEnvFor<Self>>) -> TxEnvFor<Self> {
transaction.into_tx_env()
}

View File

@@ -6,7 +6,6 @@ use reth_primitives_traits::{Block, RecoveredBlock};
use std::time::Instant;
/// Executor metrics.
// TODO(onbjerg): add sload/sstore
#[derive(Metrics, Clone)]
#[metrics(scope = "sync.execution")]
pub struct ExecutorMetrics {

View File

@@ -7,8 +7,8 @@ use alloy_eips::{eip1898::ForkBlock, eip2718::Encodable2718, BlockNumHash};
use alloy_primitives::{Address, BlockHash, BlockNumber, TxHash};
use core::{fmt, ops::RangeInclusive};
use reth_primitives_traits::{
transaction::signed::SignedTransaction, Block, BlockBody, NodePrimitives, RecoveredBlock,
SealedHeader,
transaction::signed::SignedTransaction, Block, BlockBody, IndexedTx, NodePrimitives,
RecoveredBlock, SealedHeader,
};
use reth_trie_common::updates::TrieUpdates;
use revm::database::BundleState;
@@ -41,6 +41,13 @@ pub struct Chain<N: NodePrimitives = reth_ethereum_primitives::EthPrimitives> {
trie_updates: Option<TrieUpdates>,
}
type ChainTxReceiptMeta<'a, N> = (
&'a RecoveredBlock<<N as NodePrimitives>::Block>,
IndexedTx<'a, <N as NodePrimitives>::Block>,
&'a <N as NodePrimitives>::Receipt,
&'a [<N as NodePrimitives>::Receipt],
);
impl<N: NodePrimitives> Default for Chain<N> {
fn default() -> Self {
Self {
@@ -185,6 +192,24 @@ impl<N: NodePrimitives> Chain<N> {
self.blocks_iter().zip(self.block_receipts_iter())
}
/// Finds a transaction by hash and returns it along with its corresponding receipt data.
///
/// Returns `None` if the transaction is not found in this chain.
pub fn find_transaction_and_receipt_by_hash(
&self,
tx_hash: TxHash,
) -> Option<ChainTxReceiptMeta<'_, N>> {
for (block, receipts) in self.blocks_and_receipts() {
let Some(indexed_tx) = block.find_indexed(tx_hash) else {
continue;
};
let receipt = receipts.get(indexed_tx.index())?;
return Some((block, indexed_tx, receipt, receipts.as_slice()));
}
None
}
/// Get the block at which this chain forked.
pub fn fork_block(&self) -> ForkBlock {
let first = self.first();

View File

@@ -144,7 +144,12 @@ impl<T> ExecutionOutcome<T> {
bundle: BundleState,
results: Vec<BlockExecutionResult<T>>,
) -> Self {
let mut value = Self { bundle, first_block, receipts: Vec::new(), requests: Vec::new() };
let mut value = Self {
bundle,
first_block,
receipts: Vec::with_capacity(results.len()),
requests: Vec::with_capacity(results.len()),
};
for result in results {
value.receipts.push(result.receipts);
value.requests.push(result.requests);
@@ -337,7 +342,7 @@ impl<T> ExecutionOutcome<T> {
/// Prepends present the state with the given `BundleState`.
/// It adds changes from the given state but does not override any existing changes.
///
/// Reverts and receipts are not updated.
/// Reverts and receipts are not updated.
pub fn prepend_state(&mut self, mut other: BundleState) {
let other_len = other.reverts.len();
// take this bundle

View File

@@ -118,8 +118,6 @@ where
results.push(executor.execute_one(&block)?);
execution_duration += execute_start.elapsed();
// TODO(alexey): report gas metrics using `block.header.gas_used`
// Seal the block back and save it
blocks.push(block);
// Check if we should commit now

View File

@@ -13,7 +13,7 @@ use reth_evm_ethereum::EthEvmConfig;
use reth_node_api::NodePrimitives;
use reth_primitives_traits::{Block as _, RecoveredBlock};
use reth_provider::{
providers::ProviderNodeTypes, BlockWriter as _, ExecutionOutcome, LatestStateProviderRef,
providers::ProviderNodeTypes, BlockWriter as _, ExecutionOutcome, LatestStateProvider,
ProviderFactory,
};
use reth_revm::database::StateProviderDatabase;
@@ -69,7 +69,7 @@ where
// Execute the block to produce a block execution output
let mut block_execution_output = EthEvmConfig::ethereum(chain_spec)
.batch_executor(StateProviderDatabase::new(LatestStateProviderRef::new(&provider)))
.batch_executor(StateProviderDatabase::new(LatestStateProvider::new(provider)))
.execute(block)?;
block_execution_output.state.reverts.sort();
@@ -203,8 +203,8 @@ where
let provider = provider_factory.provider()?;
let evm_config = EthEvmConfig::new(chain_spec);
let executor = evm_config
.batch_executor(StateProviderDatabase::new(LatestStateProviderRef::new(&provider)));
let executor =
evm_config.batch_executor(StateProviderDatabase::new(LatestStateProvider::new(provider)));
let mut execution_outcome = executor.execute_batch(vec![&block1, &block2])?;
execution_outcome.state_mut().reverts.sort();

View File

@@ -1303,7 +1303,7 @@ mod tests {
.try_recover()
.unwrap();
let provider_rw = provider_factory.database_provider_rw().unwrap();
provider_rw.insert_block(block.clone()).unwrap();
provider_rw.insert_block(&block).unwrap();
provider_rw.commit().unwrap();
let provider = BlockchainProvider::new(provider_factory).unwrap();

View File

@@ -354,10 +354,10 @@ where
/// canonical chain.
///
/// Possible situations are:
/// - ExEx is behind the node head (`node_head.number < exex_head.number`). Backfill from the
/// - ExEx is behind the node head (`exex_head.number < node_head.number`). Backfill from the
/// node database.
/// - ExEx is at the same block number as the node head (`node_head.number ==
/// exex_head.number`). Nothing to do.
/// - ExEx is at the same block number as the node head (`exex_head.number ==
/// node_head.number`). Nothing to do.
fn check_backfill(&mut self) -> eyre::Result<()> {
let backfill_job_factory =
BackfillJobFactory::new(self.evm_config.clone(), self.provider.clone());
@@ -481,12 +481,12 @@ mod tests {
&mut rng,
genesis_block.number + 1,
BlockParams { parent: Some(genesis_hash), tx_count: Some(0), ..Default::default() },
);
let provider_rw = provider_factory.provider_rw()?;
provider_rw.insert_block(node_head_block.clone().try_recover()?)?;
provider_rw.commit()?;
)
.try_recover()?;
let node_head = node_head_block.num_hash();
let provider_rw = provider_factory.provider_rw()?;
provider_rw.insert_block(&node_head_block)?;
provider_rw.commit()?;
let exex_head =
ExExHead { block: BlockNumHash { number: genesis_block.number, hash: genesis_hash } };
@@ -613,7 +613,7 @@ mod tests {
.try_recover()?;
let node_head = node_head_block.num_hash();
let provider_rw = provider.database_provider_rw()?;
provider_rw.insert_block(node_head_block)?;
provider_rw.insert_block(&node_head_block)?;
provider_rw.commit()?;
let node_head_notification = ExExNotification::ChainCommitted {
new: Arc::new(

View File

@@ -116,6 +116,11 @@ impl BlockCache {
self.committed_blocks.insert(block.hash(), (file_id, cached_block));
}
let first_block_number = committed_chain.first().number();
self.lowest_committed_block_height = Some(
self.lowest_committed_block_height
.map_or(first_block_number, |lowest| lowest.min(first_block_number)),
);
self.highest_committed_block_height = Some(committed_chain.tip().number());
}
}

View File

@@ -212,13 +212,13 @@ impl Discv4ConfigBuilder {
self
}
/// Whether to enforce expiration timestamps in messages.
/// Whether to enable EIP-868
pub const fn enable_eip868(&mut self, enable_eip868: bool) -> &mut Self {
self.config.enable_eip868 = enable_eip868;
self
}
/// Whether to enable EIP-868
/// Whether to enforce expiration timestamps in messages.
pub const fn enforce_expiration_timestamps(
&mut self,
enforce_expiration_timestamps: bool,

View File

@@ -24,7 +24,7 @@ pub fn discv4_id_to_discv5_id(peer_id: PeerId) -> Result<NodeId, secp256k1::Erro
Ok(id2pk(peer_id)?.into())
}
/// Converts a [`PeerId`] to a [`reth_network_peers::PeerId`].
/// Converts a [`PeerId`] to a [`discv5::libp2p_identity::PeerId`].
pub fn discv4_id_to_multiaddr_id(
peer_id: PeerId,
) -> Result<discv5::libp2p_identity::PeerId, secp256k1::Error> {

View File

@@ -77,7 +77,7 @@ pub struct Discv5 {
metrics: Discv5Metrics,
/// Returns the _local_ [`NodeRecord`] this service was started with.
// Note: we must track this separately because the `discv5::Discv5` does not necessarily
// provide this via it's [`local_enr`](discv5::Discv5::local_ner()) This is intended for
// provide this via its [`local_enr`](discv5::Discv5::local_enr()). This is intended for
// obtaining the port this service was launched at
local_node_record: NodeRecord,
}

View File

@@ -122,10 +122,10 @@ impl TreeRootEntry {
Ok(Self { enr_root, link_root, sequence_number, signature })
}
/// Returns the _unsigned_ content pairs of the entry:
/// Returns the _unsigned_ content of the entry used for signing:
///
/// ```text
/// e=<enr-root> l=<link-root> seq=<sequence-number> sig=<signature>
/// enrtree-root:v1 e=<enr-root> l=<link-root> seq=<sequence-number>
/// ```
fn content(&self) -> String {
format!(

View File

@@ -312,7 +312,6 @@ impl ECIES {
/// Create a new ECIES client with the given static secret key and remote peer ID.
pub fn new_client(secret_key: SecretKey, remote_id: PeerId) -> Result<Self, ECIESError> {
// TODO(rand): use rng for nonce
let mut rng = rng();
let nonce = B256::random();
let ephemeral_secret_key = SecretKey::new(&mut rng);

View File

@@ -409,6 +409,13 @@ impl NewPooledTransactionHashes68 {
}
}
/// Shrinks the capacity of the message vectors as much as possible.
pub fn shrink_to_fit(&mut self) {
self.hashes.shrink_to_fit();
self.sizes.shrink_to_fit();
self.types.shrink_to_fit()
}
/// Consumes and appends a transaction
pub fn with_transaction<T: SignedTransaction>(mut self, tx: &T) -> Self {
self.push(tx);

View File

@@ -151,13 +151,16 @@ impl Discovery {
self.discovery_listeners.retain_mut(|listener| listener.send(event.clone()).is_ok());
}
/// Updates the `eth:ForkId` field in discv4.
/// Updates the `eth:ForkId` field in discv4/discv5.
pub(crate) fn update_fork_id(&self, fork_id: ForkId) {
if let Some(discv4) = &self.discv4 {
// use forward-compatible forkid entry
discv4.set_eip868_rlp(b"eth".to_vec(), EnrForkIdEntry::from(fork_id))
}
// todo: update discv5 enr
if let Some(discv5) = &self.discv5 {
discv5
.encode_and_set_eip868_in_local_enr(b"eth".to_vec(), EnrForkIdEntry::from(fork_id))
}
}
/// Bans the [`IpAddr`] in the discovery service.

View File

@@ -179,7 +179,7 @@ impl<N: NetworkPrimitives> StateFetcher<N> {
let request = self.queued_requests.pop_front().expect("not empty");
let Some(peer_id) = self.next_best_peer(request.best_peer_requirements()) else {
// need to put back the the request
// need to put back the request
self.queued_requests.push_front(request);
return PollAction::NoPeersAvailable
};
@@ -397,7 +397,7 @@ impl Peer {
/// A peer has a "better range" if:
/// 1. It can fully cover the requested range while the other cannot
/// 2. None can fully cover the range, but this peer has lower start value
/// 3. If a peer doesnt announce a range we assume it has full history, but check the other's
/// 3. If a peer doesn't announce a range we assume it has full history, but check the other's
/// range and treat that as better if it can cover the range
fn has_better_range(&self, other: &Self, range: &RangeInclusive<u64>) -> bool {
let self_range = self.range();

View File

@@ -539,9 +539,12 @@ impl<N: NetworkPrimitives> SessionManager<N> {
let version = conn.version();
// Configure the interval at which the range information is updated, starting with
// ETH69
// ETH69. We use interval_at to delay the first tick, avoiding sending
// BlockRangeUpdate immediately after connection (which can cause issues with
// peers that don't properly handle the message).
let range_update_interval = (conn.version() >= EthVersion::Eth69).then(|| {
let mut interval = tokio::time::interval(RANGE_UPDATE_INTERVAL);
let start = tokio::time::Instant::now() + RANGE_UPDATE_INTERVAL;
let mut interval = tokio::time::interval_at(start, RANGE_UPDATE_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
interval
});

View File

@@ -860,9 +860,8 @@ where
peer_id: PeerId,
propagation_mode: PropagationMode,
) -> Option<PropagatedTransactions> {
trace!(target: "net::tx", ?peer_id, "Propagating transactions to peer");
let peer = self.peers.get_mut(&peer_id)?;
trace!(target: "net::tx", ?peer_id, "Propagating transactions to peer");
let mut propagated = PropagatedTransactions::default();
// filter all transactions unknown to the peer
@@ -1932,8 +1931,14 @@ impl PooledTransactionsHashesBuilder {
fn build(self) -> NewPooledTransactionHashes {
match self {
Self::Eth66(msg) => msg.into(),
Self::Eth68(msg) => msg.into(),
Self::Eth66(mut msg) => {
msg.0.shrink_to_fit();
msg.into()
}
Self::Eth68(mut msg) => {
msg.shrink_to_fit();
msg.into()
}
}
}
}

View File

@@ -13,9 +13,7 @@ pub type BodyDownloaderResult<B> = DownloadResult<Vec<BlockResponse<B>>>;
/// A downloader represents a distinct strategy for submitting requests to download block bodies,
/// while a [`BodiesClient`][crate::bodies::client::BodiesClient] represents a client capable of
/// fulfilling these requests.
pub trait BodyDownloader:
Send + Sync + Stream<Item = BodyDownloaderResult<Self::Block>> + Unpin
{
pub trait BodyDownloader: Send + Stream<Item = BodyDownloaderResult<Self::Block>> + Unpin {
/// The Block type this downloader supports
type Block: Block + 'static;

View File

@@ -37,6 +37,14 @@ where
Self::Empty(_) => None,
}
}
/// Return the reference to the response body
pub const fn body(&self) -> Option<&B::Body> {
match self {
Self::Full(block) => Some(block.body()),
Self::Empty(_) => None,
}
}
}
impl<B: Block> InMemorySize for BlockResponse<B> {

View File

@@ -100,9 +100,7 @@ impl<H: BlockHeader + Sealable> HeaderSyncGap<H> {
}
}
/// Validate whether the header is valid in relation to it's parent
///
/// Returns Ok(false) if the
/// Validate whether the header is valid in relation to its parent.
pub fn validate_header_download<H: BlockHeader>(
consensus: &dyn HeaderValidator<H>,
header: &SealedHeader<H>,

View File

@@ -66,8 +66,9 @@ use reth_node_metrics::{
};
use reth_provider::{
providers::{NodeTypesForProvider, ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
BlockHashReader, BlockNumReader, ProviderError, ProviderFactory, ProviderResult,
StageCheckpointReader, StaticFileProviderBuilder, StaticFileProviderFactory,
BlockHashReader, BlockNumReader, DatabaseProviderFactory, ProviderError, ProviderFactory,
ProviderResult, RocksDBProviderFactory, StageCheckpointReader, StaticFileProviderBuilder,
StaticFileProviderFactory,
};
use reth_prune::{PruneModes, PrunerBuilder};
use reth_rpc_builder::config::RethRpcServerConfig;
@@ -453,11 +454,7 @@ impl<R, ChainSpec: EthChainSpec> LaunchContextWith<Attached<WithConfigs<ChainSpe
where
Pool: TransactionPool + Unpin,
{
if let Some(interval) = self.node_config().dev.block_time {
MiningMode::interval(interval)
} else {
MiningMode::instant(pool, self.node_config().dev.block_max_transactions)
}
self.node_config().dev_mining_mode(pool)
}
}
@@ -501,20 +498,54 @@ where
)?
.with_prune_modes(self.prune_modes());
// Check for consistency between database and static files. If it fails, it unwinds to
// the first block that's consistent between database and static files.
if let Some(unwind_target) =
factory.static_file_provider().check_consistency(&factory.provider()?)?
{
// Keep MDBX, static files, and RocksDB aligned. If any check fails, unwind to the
// earliest consistent block.
//
// Order matters:
// 1) heal static files (no pruning)
// 2) check RocksDB (needs static-file tx data)
// 3) check static-file checkpoints vs MDBX (may prune)
//
// Compute one unwind target and run a single unwind.
let provider_ro = factory.database_provider_ro()?;
// Step 1: heal file-level inconsistencies (no pruning)
factory.static_file_provider().check_file_consistency(&provider_ro)?;
// Step 2: RocksDB consistency check (needs static files tx data)
let rocksdb_unwind = factory.rocksdb_provider().check_consistency(&provider_ro)?;
// Step 3: Static file checkpoint consistency (may prune)
let static_file_unwind = factory
.static_file_provider()
.check_consistency(&provider_ro)?
.map(|target| match target {
PipelineTarget::Unwind(block) => block,
PipelineTarget::Sync(_) => unreachable!("check_consistency returns Unwind"),
});
// Take the minimum block number to ensure all storage layers are consistent.
let unwind_target = [rocksdb_unwind, static_file_unwind].into_iter().flatten().min();
if let Some(unwind_block) = unwind_target {
// Highly unlikely to happen, and given its destructive nature, it's better to panic
// instead.
// instead. Unwinding to 0 would leave MDBX with a huge free list size.
let inconsistency_source = match (rocksdb_unwind, static_file_unwind) {
(Some(_), Some(_)) => "RocksDB and static file",
(Some(_), None) => "RocksDB",
(None, Some(_)) => "static file",
(None, None) => unreachable!(),
};
assert_ne!(
unwind_target,
PipelineTarget::Unwind(0),
"A static file <> database inconsistency was found that would trigger an unwind to block 0"
unwind_block, 0,
"A {} inconsistency was found that would trigger an unwind to block 0",
inconsistency_source
);
info!(target: "reth::cli", unwind_target = %unwind_target, "Executing an unwind after a failed storage consistency check.");
let unwind_target = PipelineTarget::Unwind(unwind_block);
info!(target: "reth::cli", %unwind_target, %inconsistency_source, "Executing unwind after consistency check.");
let (_tip_tx, tip_rx) = watch::channel(B256::ZERO);
@@ -548,7 +579,7 @@ where
}),
);
rx.await?.inspect_err(|err| {
error!(target: "reth::cli", unwind_target = %unwind_target, %err, "failed to run unwind")
error!(target: "reth::cli", %unwind_target, %inconsistency_source, %err, "failed to run unwind")
})?;
}
@@ -617,7 +648,7 @@ where
target_triple: version_metadata().vergen_cargo_target_triple.as_ref(),
build_profile: version_metadata().build_profile_name.as_ref(),
},
ChainSpecInfo { name: self.left().config.chain.chain().to_string() },
ChainSpecInfo { name: self.chain_id().to_string() },
self.task_executor().clone(),
Hooks::builder()
.with_hook({
@@ -938,9 +969,13 @@ where
///
/// A target block hash if the pipeline is inconsistent, otherwise `None`.
pub fn check_pipeline_consistency(&self) -> ProviderResult<Option<B256>> {
// We skip the era stage if it's not enabled
let era_enabled = self.era_import_source().is_some();
let mut all_stages =
StageId::ALL.into_iter().filter(|id| era_enabled || id != &StageId::Era);
// Get the expected first stage based on config.
let first_stage =
if self.era_import_source().is_some() { StageId::Era } else { StageId::Headers };
let first_stage = all_stages.next().expect("there must be at least one stage");
// If no target was provided, check if the stages are congruent - check if the
// checkpoint of the last stage matches the checkpoint of the first.
@@ -950,20 +985,28 @@ where
.unwrap_or_default()
.block_number;
// Skip the first stage as we've already retrieved it and comparing all other checkpoints
// against it.
for stage_id in StageId::ALL.iter().skip(1) {
// Compare all other stages against the first
for stage_id in all_stages {
let stage_checkpoint = self
.blockchain_db()
.get_stage_checkpoint(*stage_id)?
.get_stage_checkpoint(stage_id)?
.unwrap_or_default()
.block_number;
// If the checkpoint of any stage is less than the checkpoint of the first stage,
// retrieve and return the block hash of the latest header and use it as the target.
debug!(
target: "consensus::engine",
first_stage_id = %first_stage,
first_stage_checkpoint,
stage_id = %stage_id,
stage_checkpoint = stage_checkpoint,
"Checking stage against first stage",
);
if stage_checkpoint < first_stage_checkpoint {
debug!(
target: "consensus::engine",
first_stage_id = %first_stage,
first_stage_checkpoint,
inconsistent_stage_id = %stage_id,
inconsistent_stage_checkpoint = stage_checkpoint,
@@ -1048,7 +1091,13 @@ where
}
/// Spawns the [`EthStatsService`] service if configured.
pub async fn spawn_ethstats(&self) -> eyre::Result<()> {
pub async fn spawn_ethstats<St>(&self, mut engine_events: St) -> eyre::Result<()>
where
St: Stream<Item = reth_engine_primitives::ConsensusEngineEvent<PrimitivesTy<T::Types>>>
+ Send
+ Unpin
+ 'static,
{
let Some(url) = self.node_config().debug.ethstats.as_ref() else { return Ok(()) };
let network = self.components().network().clone();
@@ -1058,7 +1107,37 @@ where
info!(target: "reth::cli", "Starting EthStats service at {}", url);
let ethstats = EthStatsService::new(url, network, provider, pool).await?;
tokio::spawn(async move { ethstats.run().await });
// If engine events are provided, spawn listener for new payload reporting
let ethstats_for_events = ethstats.clone();
let task_executor = self.task_executor().clone();
task_executor.spawn(Box::pin(async move {
while let Some(event) = engine_events.next().await {
use reth_engine_primitives::ConsensusEngineEvent;
match event {
ConsensusEngineEvent::ForkBlockAdded(executed, duration) |
ConsensusEngineEvent::CanonicalBlockAdded(executed, duration) => {
let block_hash = executed.recovered_block.num_hash().hash;
let block_number = executed.recovered_block.num_hash().number;
if let Err(e) = ethstats_for_events
.report_new_payload(block_hash, block_number, duration)
.await
{
debug!(
target: "ethstats",
"Failed to report new payload: {}", e
);
}
}
_ => {
// Ignore other event types for ethstats reporting
}
}
}
}));
// Spawn main ethstats service
task_executor.spawn(Box::pin(async move { ethstats.run().await }));
Ok(())
}

View File

@@ -9,7 +9,7 @@ use crate::{
NodeBuilderWithComponents, NodeComponents, NodeComponentsBuilder, NodeHandle, NodeTypesAdapter,
};
use alloy_consensus::BlockHeader;
use futures::{stream_select, StreamExt};
use futures::{stream_select, FutureExt, StreamExt};
use reth_chainspec::{EthChainSpec, EthereumHardforks};
use reth_engine_service::service::{ChainEvent, EngineService};
use reth_engine_tree::{
@@ -270,7 +270,7 @@ impl EngineNodeLauncher {
} = add_ons.launch_add_ons(add_ons_ctx).await?;
// Create engine shutdown handle
let (engine_shutdown, mut shutdown_rx) = EngineShutdown::new();
let (engine_shutdown, shutdown_rx) = EngineShutdown::new();
// Run consensus engine to completion
let initial_target = ctx.initial_backfill_target()?;
@@ -290,7 +290,7 @@ impl EngineNodeLauncher {
let startup_sync_state_idle = ctx.node_config().debug.startup_sync_state_idle;
info!(target: "reth::cli", "Starting consensus engine");
ctx.task_executor().spawn_critical("consensus engine", Box::pin(async move {
let consensus_engine = async move {
if let Some(initial_target) = initial_target {
debug!(target: "reth::cli", %initial_target, "start backfill sync");
// network_handle's sync state is already initialized at Syncing
@@ -300,8 +300,11 @@ impl EngineNodeLauncher {
}
let mut res = Ok(());
let mut shutdown_rx = shutdown_rx.fuse();
// advance the chain and await payloads built locally to add into the engine api tree handler to prevent re-execution if that block is received as payload from the CL
// advance the chain and await payloads built locally to add into the engine api
// tree handler to prevent re-execution if that block is received as payload from
// the CL
loop {
tokio::select! {
shutdown_req = &mut shutdown_rx => {
@@ -343,19 +346,21 @@ impl EngineNodeLauncher {
if let Some(head) = ev.canonical_header() {
// Once we're progressing via live sync, we can consider the node is not syncing anymore
network_handle.update_sync_state(SyncState::Idle);
let head_block = Head {
let head_block = Head {
number: head.number(),
hash: head.hash(),
difficulty: head.difficulty(),
timestamp: head.timestamp(),
total_difficulty: chainspec.final_paris_total_difficulty().filter(|_| chainspec.is_paris_active_at_block(head.number())).unwrap_or_default(),
total_difficulty: chainspec.final_paris_total_difficulty()
.filter(|_| chainspec.is_paris_active_at_block(head.number()))
.unwrap_or_default(),
};
network_handle.update_status(head_block);
let updated = BlockRangeUpdate {
earliest: provider.earliest_block_number().unwrap_or_default(),
latest:head.number(),
latest_hash:head.hash()
latest: head.number(),
latest_hash: head.hash(),
};
network_handle.update_block_range(updated);
}
@@ -367,7 +372,10 @@ impl EngineNodeLauncher {
}
let _ = exit.send(res);
}));
};
ctx.task_executor().spawn_critical("consensus engine", Box::pin(consensus_engine));
let engine_events_for_ethstats = engine_events.new_listener();
let full_node = FullNode {
evm_config: ctx.components().evm_config().clone(),
@@ -389,7 +397,7 @@ impl EngineNodeLauncher {
// Notify on node started
on_node_started.on_event(FullNode::clone(&full_node))?;
ctx.spawn_ethstats().await?;
ctx.spawn_ethstats(engine_events_for_ethstats).await?;
let handle = NodeHandle {
node_exit_future: NodeExitFuture::new(

View File

@@ -80,7 +80,7 @@ tokio.workspace = true
# Features for vergen to generate correct env vars
jemalloc = ["reth-cli-util/jemalloc"]
asm-keccak = ["alloy-primitives/asm-keccak"]
# Feature to enable opentelemetry export
keccak-cache-global = ["alloy-primitives/keccak-cache-global"]
otlp = ["reth-tracing/otlp"]
min-error-logs = ["tracing/release_max_level_error"]

View File

@@ -61,6 +61,20 @@ pub struct LogArgs {
)]
pub journald_filter: String,
/// Emit traces to samply. Only useful when profiling.
#[arg(long = "log.samply", global = true, hide = true)]
pub samply: bool,
/// The filter to use for traces emitted to samply.
#[arg(
long = "log.samply.filter",
value_name = "FILTER",
global = true,
default_value = "debug",
hide = true
)]
pub samply_filter: String,
/// Sets whether or not the formatter emits ANSI terminal escape codes for colors and other
/// text formatting.
#[arg(
@@ -129,6 +143,11 @@ impl LogArgs {
tracer = tracer.with_file(file, info);
}
if self.samply {
let config = self.layer_info(LogFormat::Terminal, self.samply_filter.clone(), false);
tracer = tracer.with_samply(config);
}
let guard = tracer.init_with_layers(layers)?;
Ok(guard)
}

View File

@@ -3,8 +3,8 @@ use crate::{
credentials::EthstatsCredentials,
error::EthStatsError,
events::{
AuthMsg, BlockMsg, BlockStats, HistoryMsg, LatencyMsg, NodeInfo, NodeStats, PendingMsg,
PendingStats, PingMsg, StatsMsg, TxStats, UncleStats,
AuthMsg, BlockMsg, BlockStats, HistoryMsg, LatencyMsg, NodeInfo, NodeStats, PayloadMsg,
PayloadStats, PendingMsg, PendingStats, PingMsg, StatsMsg, TxStats, UncleStats,
},
};
use alloy_consensus::{BlockHeader, Sealable};
@@ -50,7 +50,7 @@ const READ_TIMEOUT: Duration = Duration::from_secs(30);
/// authentication, stats reporting, block notifications, and connection management.
/// It maintains a persistent `WebSocket` connection and automatically reconnects
/// when the connection is lost.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct EthStatsService<Network, Provider, Pool> {
/// Authentication credentials for the `EthStats` server
credentials: EthstatsCredentials,
@@ -347,6 +347,42 @@ where
Ok(())
}
/// Report new payload information to the `EthStats` server
///
/// Sends information about payload processing time and block details
/// to the server for monitoring purposes.
pub async fn report_new_payload(
&self,
block_hash: alloy_primitives::B256,
block_number: u64,
processing_time: Duration,
) -> Result<(), EthStatsError> {
let conn = self.conn.read().await;
let conn = conn.as_ref().ok_or(EthStatsError::NotConnected)?;
let payload_stats = PayloadStats {
number: U256::from(block_number),
hash: block_hash,
processing_time: processing_time.as_millis() as u64,
};
let payload_msg =
PayloadMsg { id: self.credentials.node_id.clone(), payload: payload_stats };
debug!(
target: "ethstats",
"Reporting new payload: block={}, hash={:?}, processing_time={}ms",
block_number,
block_hash,
processing_time.as_millis()
);
let message = payload_msg.generate_new_payload_message();
conn.write_json(&message).await?;
Ok(())
}
/// Convert a block to `EthStats` block statistics format
///
/// Extracts relevant information from a block and formats it according

View File

@@ -281,3 +281,66 @@ impl PingMsg {
.to_string()
}
}
/// Information reported about a new payload processing event.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PayloadStats {
/// Block number of the payload
pub number: U256,
/// Hash of the payload block
pub hash: B256,
/// Time taken to validate the payload in milliseconds
#[serde(rename = "processingTime")]
pub processing_time: u64,
}
/// Message containing new payload information to be reported to the ethstats monitoring server.
#[derive(Debug, Serialize, Deserialize)]
pub struct PayloadMsg {
/// The node's unique identifier
pub id: String,
/// The payload information to report
pub payload: PayloadStats,
}
impl PayloadMsg {
/// Generate a new payload message for the ethstats monitoring server.
pub fn generate_new_payload_message(&self) -> String {
serde_json::json!({
"emit": ["new-payload", self]
})
.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{B256, U256};
#[test]
fn test_payload_msg_generation() {
let payload_stats = PayloadStats {
number: U256::from(12345),
hash: B256::from_slice(&[1u8; 32]),
processing_time: 150,
};
let payload_msg = PayloadMsg { id: "test-node".to_string(), payload: payload_stats };
let message = payload_msg.generate_new_payload_message();
let parsed: serde_json::Value = serde_json::from_str(&message).expect("Valid JSON");
assert_eq!(parsed["emit"][0], "new-payload");
assert_eq!(parsed["emit"][1]["id"], "test-node");
assert_eq!(parsed["emit"][1]["payload"]["number"], "0x3039"); // 12345 in hex
assert_eq!(parsed["emit"][1]["payload"]["processingTime"], 150);
// Verify the structure contains all expected fields
assert!(parsed["emit"][1]["payload"]["hash"].is_string());
assert!(parsed["emit"][1]["payload"]["processingTime"].is_number());
}
}

View File

@@ -3,7 +3,7 @@
use eyre::WrapErr;
use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle};
use metrics_util::layers::{PrefixLayer, Stack};
use std::sync::{atomic::AtomicBool, LazyLock};
use std::sync::{atomic::AtomicBool, OnceLock};
/// Installs the Prometheus recorder as the global recorder.
///
@@ -11,14 +11,43 @@ use std::sync::{atomic::AtomicBool, LazyLock};
///
/// Caution: This only configures the global recorder and does not spawn the exporter.
/// Callers must run [`PrometheusRecorder::spawn_upkeep`] manually.
///
/// Use [`init_prometheus_recorder`] to install a custom recorder.
pub fn install_prometheus_recorder() -> &'static PrometheusRecorder {
&PROMETHEUS_RECORDER_HANDLE
PROMETHEUS_RECORDER_HANDLE.get_or_init(|| {
PrometheusRecorder::install().expect("Failed to install Prometheus recorder")
})
}
/// Installs the provided recorder as the global recorder.
///
/// To customize the builder, first construct a recorder with
/// [`PrometheusRecorder::install_with_builder`], then pass it here.
///
/// # Panics
///
/// Panics if a recorder has already been installed.
pub fn init_prometheus_recorder(recorder: PrometheusRecorder) -> &'static PrometheusRecorder {
PROMETHEUS_RECORDER_HANDLE.set(recorder).expect("Prometheus recorder already installed");
PROMETHEUS_RECORDER_HANDLE.get().expect("Prometheus recorder is set")
}
/// The default Prometheus recorder handle. We use a global static to ensure that it is only
/// installed once.
static PROMETHEUS_RECORDER_HANDLE: LazyLock<PrometheusRecorder> =
LazyLock::new(|| PrometheusRecorder::install().unwrap());
static PROMETHEUS_RECORDER_HANDLE: OnceLock<PrometheusRecorder> = OnceLock::new();
/// Installs the Prometheus recorder with a custom builder.
///
/// Returns an error if a recorder has already been installed.
pub fn try_install_prometheus_recorder_with_builder(
builder: PrometheusBuilder,
) -> eyre::Result<&'static PrometheusRecorder> {
let recorder = PrometheusRecorder::install_with_builder(builder)?;
PROMETHEUS_RECORDER_HANDLE
.set(recorder)
.map_err(|_| eyre::eyre!("Prometheus recorder already installed"))?;
Ok(PROMETHEUS_RECORDER_HANDLE.get().expect("recorder is set"))
}
/// A handle to the Prometheus recorder.
///
@@ -75,7 +104,15 @@ impl PrometheusRecorder {
/// Caution: This only configures the global recorder and does not spawn the exporter.
/// Callers must run [`Self::spawn_upkeep`] manually.
pub fn install() -> eyre::Result<Self> {
let recorder = PrometheusBuilder::new().build_recorder();
Self::install_with_builder(PrometheusBuilder::new())
}
/// Installs Prometheus as the metrics recorder with a custom builder.
///
/// Caution: This only configures the global recorder and does not spawn the exporter.
/// Callers must run [`Self::spawn_upkeep`] manually.
pub fn install_with_builder(builder: PrometheusBuilder) -> eyre::Result<Self> {
let recorder = builder.build_recorder();
let handle = recorder.handle();
// Build metrics stack
@@ -98,14 +135,13 @@ mod tests {
// `metrics-exporter-prometheus` dependency version.
#[test]
fn process_metrics() {
// initialize the lazy handle
let _ = &*PROMETHEUS_RECORDER_HANDLE;
let recorder = install_prometheus_recorder();
let process = metrics_process::Collector::default();
process.describe();
process.collect();
let metrics = PROMETHEUS_RECORDER_HANDLE.handle.render();
let metrics = recorder.handle().render();
assert!(metrics.contains("process_cpu_seconds_total"), "{metrics:?}");
}
}

View File

@@ -6,7 +6,7 @@ macro_rules! create_chain_spec {
/// The Optimism $name $environment spec
pub static [<$name:upper _ $environment:upper>]: $crate::LazyLock<alloc::sync::Arc<$crate::OpChainSpec>> = $crate::LazyLock::new(|| {
$crate::OpChainSpec::from_genesis($crate::superchain::configs::read_superchain_genesis($name, $environment)
.expect(&alloc::format!("Can't read {}-{} genesis", $name, $environment)))
.unwrap_or_else(|e| panic!("Can't read {}-{} genesis: {}", $name, $environment, e)))
.into()
});
}

View File

@@ -67,7 +67,7 @@ where
self.init_tracing(&runner)?;
// Install the prometheus recorder to be sure to record all metrics
let _ = install_prometheus_recorder();
install_prometheus_recorder();
let components = |spec: Arc<OpChainSpec>| {
(OpExecutorProvider::optimism(spec.clone()), Arc::new(OpBeaconConsensus::new(spec)))

View File

@@ -41,9 +41,9 @@ where
/// Attempts to submit a new payload to the engine.
///
/// The `TryFrom` conversion will fail if `execution_outcome.state_root` is `B256::ZERO`,
/// in which case this returns the `parent_hash` instead to drive the chain forward.
/// in which case this method uses the `parent_hash` instead to drive the chain forward.
///
/// Returns the block hash to use for FCU (either the new block or parent).
/// Returns the block hash to use for FCU (either the new block's hash or the parent hash).
async fn submit_new_payload(&self, sequence: &FlashBlockCompleteSequence) -> B256 {
let payload = match P::ExecutionData::try_from(sequence) {
Ok(payload) => payload,

View File

@@ -84,14 +84,14 @@ where
}
}
/// Returns the sender half to the received flashblocks.
/// Returns the sender half for the received flashblocks broadcast channel.
pub const fn flashblocks_broadcaster(
&self,
) -> &tokio::sync::broadcast::Sender<Arc<FlashBlock>> {
&self.received_flashblocks_tx
}
/// Returns the sender half to the flashblock sequence.
/// Returns the sender half for the flashblock sequence broadcast channel.
pub const fn block_sequence_broadcaster(
&self,
) -> &tokio::sync::broadcast::Sender<FlashBlockCompleteSequence> {

View File

@@ -7,7 +7,7 @@
//! ## Simple: Create a flashblock sequence for the same block
//!
//! ```ignore
//! let factory = FlashBlockTestFactory::new(2); // 2 second block time
//! let factory = TestFlashBlockFactory::new(); // Default 2 second block time
//! let fb0 = factory.flashblock_at(0).build();
//! let fb1 = factory.flashblock_after(&fb0).build();
//! let fb2 = factory.flashblock_after(&fb1).build();
@@ -16,7 +16,7 @@
//! ## Create flashblocks with transactions
//!
//! ```ignore
//! let factory = FlashBlockTestFactory::new(2);
//! let factory = TestFlashBlockFactory::new();
//! let fb0 = factory.flashblock_at(0).build();
//! let txs = vec![Bytes::from_static(&[1, 2, 3])];
//! let fb1 = factory.flashblock_after(&fb0).transactions(txs).build();
@@ -25,7 +25,7 @@
//! ## Test across multiple blocks (timestamps auto-increment)
//!
//! ```ignore
//! let factory = FlashBlockTestFactory::new(2); // 2 second blocks
//! let factory = TestFlashBlockFactory::new(); // Default 2 second blocks
//!
//! // Block 100 at timestamp 1000000
//! let fb0 = factory.flashblock_at(0).build();
@@ -39,8 +39,8 @@
//! ## Full control with builder
//!
//! ```ignore
//! let factory = FlashBlockTestFactory::new(1);
//! let fb = factory.custom()
//! let factory = TestFlashBlockFactory::new();
//! let fb = factory.builder()
//! .block_number(100)
//! .parent_hash(specific_hash)
//! .state_root(computed_root)
@@ -63,7 +63,7 @@ use op_alloy_rpc_types_engine::{
/// # Examples
///
/// ```ignore
/// let factory = TestFlashBlockFactory::new(2); // 2 second block time
/// let factory = TestFlashBlockFactory::new(); // Default 2 second block time
/// let fb0 = factory.flashblock_at(0).build();
/// let fb1 = factory.flashblock_after(&fb0).build();
/// let fb2 = factory.flashblock_for_next_block(&fb1).build(); // timestamp auto-increments
@@ -79,18 +79,9 @@ pub(crate) struct TestFlashBlockFactory {
}
impl TestFlashBlockFactory {
/// Creates a new builder with the specified block time in seconds.
/// Creates a new factory with a default block time of 2 seconds.
///
/// # Arguments
///
/// * `block_time` - Time between blocks in seconds (e.g., 2 for 2-second blocks)
///
/// # Examples
///
/// ```ignore
/// let factory = TestFlashBlockFactory::new(2); // 2 second blocks
/// let factory_fast = TestFlashBlockFactory::new(1); // 1 second blocks
/// ```
/// Use [`with_block_time`](Self::with_block_time) to customize the block time.
pub(crate) fn new() -> Self {
Self { block_time: 2, base_timestamp: 1_000_000, current_block_number: 100 }
}
@@ -107,7 +98,7 @@ impl TestFlashBlockFactory {
/// # Examples
///
/// ```ignore
/// let factory = TestFlashBlockFactory::new(2);
/// let factory = TestFlashBlockFactory::new();
/// let fb0 = factory.flashblock_at(0).build(); // Simple usage
/// let fb1 = factory.flashblock_at(1).state_root(specific_root).build(); // Customize
/// ```
@@ -123,7 +114,7 @@ impl TestFlashBlockFactory {
/// # Examples
///
/// ```ignore
/// let factory = TestFlashBlockFactory::new(2);
/// let factory = TestFlashBlockFactory::new();
/// let fb0 = factory.flashblock_at(0).build();
/// let fb1 = factory.flashblock_after(&fb0).build(); // Simple
/// let fb2 = factory.flashblock_after(&fb1).transactions(txs).build(); // With txs
@@ -149,7 +140,7 @@ impl TestFlashBlockFactory {
/// # Examples
///
/// ```ignore
/// let factory = TestFlashBlockFactory::new(2); // 2 second blocks
/// let factory = TestFlashBlockFactory::new(); // 2 second blocks
/// let fb0 = factory.flashblock_at(0).build(); // Block 100, timestamp 1000000
/// let fb1 = factory.flashblock_for_next_block(&fb0).build(); // Block 101, timestamp 1000002
/// let fb2 = factory.flashblock_for_next_block(&fb1).transactions(txs).build(); // Customize
@@ -173,7 +164,7 @@ impl TestFlashBlockFactory {
/// # Examples
///
/// ```ignore
/// let factory = TestFlashBlockFactory::new(2);
/// let factory = TestFlashBlockFactory::new();
/// let fb = factory.builder()
/// .index(5)
/// .block_number(200)

View File

@@ -83,6 +83,7 @@ reth-rpc-eth-types.workspace = true
reth-stages-types.workspace = true
alloy-network.workspace = true
alloy-op-hardforks.workspace = true
futures.workspace = true
op-alloy-network.workspace = true
@@ -96,6 +97,7 @@ asm-keccak = [
]
keccak-cache-global = [
"alloy-primitives/keccak-cache-global",
"reth-node-core/keccak-cache-global",
"reth-optimism-node/keccak-cache-global",
]
js-tracer = [

View File

@@ -299,23 +299,16 @@ mod test {
use super::*;
use crate::engine;
use alloy_op_hardforks::BASE_SEPOLIA_JOVIAN_TIMESTAMP;
use alloy_primitives::{b64, Address, B256, B64};
use alloy_rpc_types_engine::PayloadAttributes;
use reth_chainspec::{ChainSpec, ForkCondition, Hardfork};
use reth_chainspec::ChainSpec;
use reth_optimism_chainspec::{OpChainSpec, BASE_SEPOLIA};
use reth_optimism_forks::OpHardfork;
use reth_provider::noop::NoopProvider;
use reth_trie_common::KeccakKeyHasher;
const JOVIAN_TIMESTAMP: u64 = 1744909000;
fn get_chainspec() -> Arc<OpChainSpec> {
let mut base_sepolia_spec = BASE_SEPOLIA.inner.clone();
// TODO: Remove this once we know the Jovian timestamp
base_sepolia_spec
.hardforks
.insert(OpHardfork::Jovian.boxed(), ForkCondition::Timestamp(JOVIAN_TIMESTAMP));
let base_sepolia_spec = BASE_SEPOLIA.inner.clone();
Arc::new(OpChainSpec {
inner: ChainSpec {
@@ -427,7 +420,8 @@ mod test {
fn test_well_formed_attributes_jovian_valid() {
let validator =
OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
let attributes = get_attributes(Some(b64!("0000000000000000")), Some(1), JOVIAN_TIMESTAMP);
let attributes =
get_attributes(Some(b64!("0000000000000000")), Some(1), BASE_SEPOLIA_JOVIAN_TIMESTAMP);
let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
OpEngineTypes,
@@ -442,7 +436,7 @@ mod test {
fn test_malformed_attributes_jovian_with_eip_1559_params_none() {
let validator =
OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
let attributes = get_attributes(None, Some(1), JOVIAN_TIMESTAMP);
let attributes = get_attributes(None, Some(1), BASE_SEPOLIA_JOVIAN_TIMESTAMP);
let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
OpEngineTypes,
@@ -472,7 +466,8 @@ mod test {
fn test_malformed_attributes_post_jovian_with_min_base_fee_none() {
let validator =
OpEngineValidator::new::<KeccakKeyHasher>(get_chainspec(), NoopProvider::default());
let attributes = get_attributes(Some(b64!("0000000000000000")), None, JOVIAN_TIMESTAMP);
let attributes =
get_attributes(Some(b64!("0000000000000000")), None, BASE_SEPOLIA_JOVIAN_TIMESTAMP);
let result = <engine::OpEngineValidator<_, _, _> as EngineApiValidator<
OpEngineTypes,

View File

@@ -76,6 +76,7 @@ arbitrary = [
]
keccak-cache-global = [
"reth-optimism-node?/keccak-cache-global",
"reth-node-core?/keccak-cache-global",
]
test-utils = [
"reth-chainspec/test-utils",

View File

@@ -13,8 +13,11 @@ use crate::{
OpEthApiError, SequencerClient,
};
use alloy_consensus::BlockHeader;
use alloy_eips::BlockNumHash;
use alloy_primitives::{B256, U256};
use alloy_rpc_types_eth::{Filter, Log};
use eyre::WrapErr;
use futures::StreamExt;
use op_alloy_network::Optimism;
use op_alloy_rpc_types_engine::OpFlashblockPayloadBase;
pub use receipt::{OpReceiptBuilder, OpReceiptFieldsBuilder};
@@ -37,7 +40,10 @@ use reth_rpc_eth_api::{
EthApiTypes, FromEvmError, FullEthApiServer, RpcConvert, RpcConverter, RpcNodeCore,
RpcNodeCoreExt, RpcTypes,
};
use reth_rpc_eth_types::{EthStateCache, FeeHistoryCache, GasPriceOracle, PendingBlock};
use reth_rpc_eth_types::{
logs_utils::matching_block_logs_with_tx_hashes, EthStateCache, FeeHistoryCache, GasPriceOracle,
PendingBlock,
};
use reth_storage_api::{BlockReaderIdExt, ProviderHeader};
use reth_tasks::{
pool::{BlockingTaskGuard, BlockingTaskPool},
@@ -50,6 +56,7 @@ use std::{
time::Duration,
};
use tokio::{sync::watch, time};
use tokio_stream::{wrappers::BroadcastStream, Stream};
use tracing::info;
/// Maximum duration to wait for a fresh flashblock when one is being built.
@@ -125,6 +132,52 @@ impl<N: RpcNodeCore, Rpc: RpcConvert> OpEthApi<N, Rpc> {
self.inner.flashblocks.as_ref().map(|f| f.flashblocks_sequence.subscribe())
}
/// Returns a stream of matching flashblock receipts, if any.
///
/// This will yield all new matching receipts received from _new_ flashblocks.
pub fn flashblock_receipts_stream(
&self,
filter: Filter,
) -> Option<impl Stream<Item = Log> + Send + Unpin> {
self.subscribe_received_flashblocks().map(|rx| {
BroadcastStream::new(rx)
.scan(
None::<(u64, u64)>, // state buffers base block number and timestamp
move |state, result| {
let fb = match result.ok() {
Some(fb) => fb,
None => return futures::future::ready(None),
};
// Update state from base flashblock for block level meta data.
if let Some(base) = &fb.base {
*state = Some((base.block_number, base.timestamp));
}
let Some((block_number, timestamp)) = *state else {
// we haven't received a new flashblock sequence yet, so we can skip
// until we receive the first index 0 (base)
return futures::future::ready(Some(Vec::new()))
};
let receipts =
fb.metadata.receipts.iter().map(|(tx, receipt)| (*tx, receipt));
let all_logs = matching_block_logs_with_tx_hashes(
&filter,
BlockNumHash::new(block_number, fb.diff.block_hash),
timestamp,
receipts,
false,
);
futures::future::ready(Some(all_logs))
},
)
.flat_map(futures::stream::iter)
})
}
/// Returns information about the flashblock currently being built, if any.
fn flashblock_build_info(&self) -> Option<FlashBlockBuildInfo> {
self.inner.flashblocks.as_ref().and_then(|f| *f.in_progress_rx.borrow())

View File

@@ -7,15 +7,13 @@ use futures::StreamExt;
use op_alloy_consensus::{transaction::OpTransactionInfo, OpTransaction};
use reth_chain_state::CanonStateSubscriptions;
use reth_optimism_primitives::DepositReceipt;
use reth_primitives_traits::{
BlockBody, Recovered, SignedTransaction, SignerRecoverable, WithEncoded,
};
use reth_primitives_traits::{Recovered, SignedTransaction, SignerRecoverable, WithEncoded};
use reth_rpc_eth_api::{
helpers::{spec::SignersForRpc, EthTransactions, LoadReceipt, LoadTransaction, SpawnBlocking},
try_into_op_tx_info, EthApiTypes as _, FromEthApiError, FromEvmError, RpcConvert, RpcNodeCore,
RpcReceipt, TxInfoMapper,
};
use reth_rpc_eth_types::{EthApiError, TransactionSource};
use reth_rpc_eth_types::{block::convert_transaction_receipt, EthApiError, TransactionSource};
use reth_storage_api::{errors::ProviderError, ProviderTx, ReceiptProvider, TransactionsProvider};
use reth_transaction_pool::{
AddedTransactionOutcome, PoolPooledTx, PoolTransaction, TransactionOrigin, TransactionPool,
@@ -118,11 +116,18 @@ where
canonical_notification = canonical_stream.next() => {
if let Some(notification) = canonical_notification {
let chain = notification.committed();
for block in chain.blocks_iter() {
if block.body().contains_transaction(&hash)
&& let Some(receipt) = this.transaction_receipt(hash).await? {
return Ok(receipt);
}
if let Some((block, tx, receipt, all_receipts)) =
chain.find_transaction_and_receipt_by_hash(hash) &&
let Some(receipt) = convert_transaction_receipt(
block,
all_receipts,
tx,
receipt,
this.converter(),
)
.transpose()?
{
return Ok(receipt);
}
} else {
// Canonical stream ended

View File

@@ -88,7 +88,8 @@ impl<Client, Tx> OpTransactionValidator<Client, Tx> {
impl<Client, Tx> OpTransactionValidator<Client, Tx>
where
Client: ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt,
Client:
ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt + Sync,
Tx: EthPoolTransaction + OpPooledTx,
{
/// Create a new [`OpTransactionValidator`].
@@ -177,7 +178,7 @@ where
&self,
origin: TransactionOrigin,
transaction: Tx,
state: &mut Option<Box<dyn AccountInfoReader>>,
state: &mut Option<Box<dyn AccountInfoReader + Send>>,
) -> TransactionValidationOutcome<Tx> {
if transaction.is_eip4844() {
return TransactionValidationOutcome::Invalid(
@@ -289,7 +290,8 @@ where
impl<Client, Tx> TransactionValidator for OpTransactionValidator<Client, Tx>
where
Client: ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt,
Client:
ChainSpecProvider<ChainSpec: OpHardforks> + StateProviderFactory + BlockReaderIdExt + Sync,
Tx: EthPoolTransaction + OpPooledTx,
{
type Transaction = Tx;

View File

@@ -297,7 +297,7 @@ impl Default for BasicPayloadJobGeneratorConfig {
/// resolved or the deadline is reached, or until the built payload is marked as frozen:
/// [`BuildOutcome::Freeze`]. Once a frozen payload is returned, no additional payloads will be
/// built and this future will wait to be resolved: [`PayloadJob::resolve`] or terminated if the
/// deadline is reached..
/// deadline is reached.
#[derive(Debug)]
pub struct BasicPayloadJob<Tasks, Builder>
where

View File

@@ -2,7 +2,7 @@
use reth_metrics::{metrics::Counter, Metrics};
/// Transaction pool metrics
/// Payload builder metrics
#[derive(Metrics)]
#[metrics(scope = "payloads")]
pub(crate) struct PayloadBuilderMetrics {

View File

@@ -65,7 +65,7 @@ impl PayloadJobGenerator for TestPayloadJobGenerator {
}
}
/// A [`PayloadJobGenerator`] for testing purposes
/// A [`PayloadJob`] for testing purposes
#[derive(Debug)]
pub struct TestPayloadJob {
attr: EthPayloadBuilderAttributes,

View File

@@ -55,6 +55,9 @@ pub trait ExecutionPayload:
/// Returns the total gas consumed by all transactions in this block.
fn gas_used(&self) -> u64;
/// Returns the number of transactions in the payload.
fn transaction_count(&self) -> usize;
}
impl ExecutionPayload for ExecutionData {
@@ -89,6 +92,10 @@ impl ExecutionPayload for ExecutionData {
fn gas_used(&self) -> u64 {
self.payload.as_v1().gas_used
}
fn transaction_count(&self) -> usize {
self.payload.as_v1().transactions.len()
}
}
/// A unified type for handling both execution payloads and payload attributes.
@@ -196,6 +203,10 @@ impl ExecutionPayload for op_alloy_rpc_types_engine::OpExecutionData {
fn gas_used(&self) -> u64 {
self.payload.as_v1().gas_used
}
fn transaction_count(&self) -> usize {
self.payload.as_v1().transactions.len()
}
}
/// Extended functionality for Ethereum execution payloads

View File

@@ -44,7 +44,7 @@ impl<T: Clone> PayloadTransactions for PayloadTransactionsFixed<T> {
/// `PayloadTransactions` iterators and keeps track of the gas for both of iterators.
///
/// We can't use [`Iterator::chain`], because:
/// (a) we need to propagate the `mark_invalid` and `no_updates`
/// (a) we need to propagate the `mark_invalid`
/// (b) we need to keep track of the gas
///
/// Notes that [`PayloadTransactionsChain`] fully drains the first iterator

View File

@@ -268,6 +268,11 @@ pub trait TestBlock: Block<Header: crate::test_utils::TestHeader> {
crate::header::test_utils::TestHeader::set_block_number(self.header_mut(), number);
}
/// Updates the block timestamp.
fn set_timestamp(&mut self, timestamp: u64) {
crate::header::test_utils::TestHeader::set_timestamp(self.header_mut(), timestamp);
}
/// Updates the block state root.
fn set_state_root(&mut self, state_root: alloy_primitives::B256) {
crate::header::test_utils::TestHeader::set_state_root(self.header_mut(), state_root);

View File

@@ -472,7 +472,7 @@ impl<B: Block + Default> Default for RecoveredBlock<B> {
impl<B: Block> InMemorySize for RecoveredBlock<B> {
#[inline]
fn size(&self) -> usize {
self.block.size() + self.senders.len() * core::mem::size_of::<Address>()
self.block.size() + self.senders.capacity() * core::mem::size_of::<Address>()
}
}
@@ -586,6 +586,11 @@ impl<B: crate::test_utils::TestBlock> RecoveredBlock<B> {
self.block.set_block_number(number);
}
/// Updates the block timestamp.
pub fn set_timestamp(&mut self, timestamp: u64) {
self.block.set_timestamp(timestamp);
}
/// Updates the block state root.
pub fn set_state_root(&mut self, state_root: alloy_primitives::B256) {
self.block.set_state_root(state_root);

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