Compare commits

..

269 Commits

Author SHA1 Message Date
Alexey Shekhirin
5a2426490f merge --minimal 2026-01-16 10:40:16 +00:00
Georgios Konstantopoulos
c7cda5bdfe chore: bump fixed-cache to 0.1.5 (#21029)
Co-authored-by: Tempo Agent <agent@tempo.xyz>
2026-01-14 13:18:39 +00:00
Alexey Shekhirin
a5dd7d0106 feat(node): --minimal flag (#20960) 2026-01-13 12:54:26 +00:00
Emilia Hane
61354e6c21 chore(test): use reth_optimism_chainspec::BASE_SEPOLIA in tests (#20988) 2026-01-13 12:07:47 +00:00
DaniPopes
2444533a04 perf: use in-memory length for static files metrics (#20987) 2026-01-13 11:37:00 +00:00
Alexey Shekhirin
9e3720d105 perf(engine): clear cache on hash mismatch instead of creating new 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
8c3c753208 fix split usage 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
6bf64ba793 Merge remote-tracking branch 'origin/main' into alexey/execution-cache-fixed-cache 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
701301724f use sizes in bytes for cache size 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
d7edca95c8 nits 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
b36d588779 fix imports 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
a1341ec875 Merge remote-tracking branch 'origin/main' into alexey/execution-cache-fixed-cache 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
7e93256b01 fixed cache metrics 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
34f0e138f5 remove selfdestruct handling 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
90b51e960e use 64k sizes for caches 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
33236d7bdf check is_prewarm like before 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
64f9ca4d7b handle wipe with dashmap (bad) 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
fa149fcd8c more get_or_try_insert methods 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
50a777e6eb Merge remote-tracking branch 'origin/main' into alexey/execution-cache-fixed-cache 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
bb034ee406 timestamped storage slots for invalidation 2026-01-13 00:12:01 +00:00
Alexey Shekhirin
bf4a697bbb use fixed-cache for accounts and storages 2026-01-13 00:12:01 +00:00
kurahin
8fa01eb62e fix: use global default for rpc_proof_permits CLI flag (#20967) 2026-01-12 23:03:51 +00:00
DaniPopes
c5e00e4aeb perf(db): throttle metrics reporting (#20974)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:44:24 +00:00
joshieDo
98a35cc870 fix: propagate FEATURES to sub-makes (#20975) 2026-01-12 20:03:34 +00:00
YK
46d670eca5 fix(stages): use static files for unwind in SenderRecovery stage (#20972)
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
2026-01-12 19:22:49 +00:00
DaniPopes
25906b7b3e fix(libmdbx): use correct size for freelist u32 values (#20970) 2026-01-12 18:52:03 +00:00
Matthias Seitz
1b3d815cb8 fix(rpc): validate eth_feeHistory newest_block against chain head (#20969)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 18:48:46 +00:00
DaniPopes
23f3f8e820 feat: add tracing-tracy (#20958)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 18:37:37 +00:00
DaniPopes
2663942b50 chore(deps): bump metrics (#20968) 2026-01-12 18:13:38 +00:00
YK
369c629b9b perf(trie): reuse overlay in deferred trie overlay computation (#20774) 2026-01-12 15:04:26 +00:00
GarmashAlex
6fec4603cf refactor(trie): avoid building prefix set for v2 storage proofs (#20898) 2026-01-12 12:49:24 +00:00
DaniPopes
515fd597f3 perf(net): use alloy_primitives::Keccak256 (#20957) 2026-01-12 11:21:27 +00:00
Crypto Nomad
126a7c9570 perf(engine): save one clock read in sparse trie metrics (#20947) 2026-01-12 07:40:30 +00:00
Matthias Seitz
8aeee5018e perf(trie): save one clock read in elapsed time calculation (#20916) 2026-01-12 03:57:54 +00:00
Matthias Seitz
210309ca76 docs: fix typos and incorrect documentation (#20943) 2026-01-12 00:48:01 +01:00
Matthias Seitz
551918b0d8 refactor(engine): defer sparse trie setup to spawned task (#20942) 2026-01-11 23:30:14 +00:00
iPLAY888
89677e1bd9 docs(rpc): fix incorrect transport in with_ipc comment (#20939) 2026-01-11 23:04:32 +00:00
pepes
0e2b3afa3f chore: correct deprecation message for SealedBlockFor (#20929) 2026-01-11 15:08:25 +00:00
David Klank
5d551eab29 perf(payload): remove unnecessary parent_header clone (#20930) 2026-01-11 15:07:51 +00:00
David Klank
12c4c04f7d fix(optimism): add missing Holocene hardfork to DEV_HARDFORKS (#20931) 2026-01-11 15:03:35 +00:00
Matthias Seitz
392f8e6e13 refactor(engine): simplify is_done signature in MultiProofTask (#20906) 2026-01-11 09:46:20 +00:00
Crypto Nomad
1a94d1f091 docs: fix re-export source comments (#20913) 2026-01-10 15:36:03 +00:00
viktorking7
97ae89c7f0 docs: fix dead link (#20914) 2026-01-10 15:18:56 +00:00
Matthias Seitz
a4921119e4 perf(trie): defer consuming remaining storage proof receivers (#20915) 2026-01-10 15:17:20 +00:00
VolodymyrBg
0f3d3695f5 docs: document account_change_sets static files config (#20903) 2026-01-10 09:02:42 +00:00
phrwlk
54355dfc78 docs: fix Performant card link on landing page (#20904) 2026-01-10 08:54:58 +00:00
FT
44a6035fa3 fix: correct typos in error messages and logs (#20894) 2026-01-10 08:54:31 +00:00
Matthias Seitz
746baed2b1 feat(cli): add CliRunnerConfig for configurable graceful shutdown timeout (#20899)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 21:52:03 +00:00
Dan Cline
e86c5fba53 fix(stages): advance sender static file in sender recovery (#20897) 2026-01-09 20:23:17 +00:00
joshieDo
485fa3448d fix: call cancel_all_background_work on RocksDBProviderInner drop (#20895) 2026-01-09 19:53:31 +00:00
DaniPopes
0db3813941 fix(rbc): fail early if node exits while waiting for startup (#20892) 2026-01-09 17:58:04 +00:00
FT
52c2ae3362 docs: fix typos in documentation files (#20890)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-09 18:41:37 +01:00
YK
b1d75f2771 feat(bench-compare): add --wait-for-persistence flag support (#20891) 2026-01-09 16:47:46 +00:00
Matthias Seitz
ef80ee1687 chore: remove env clone (#20889) 2026-01-09 16:42:50 +00:00
radik878
8dacfb3d9c refactor(ecies): avoid duplicate keccak digest in MAC::update_body (#20854) 2026-01-09 15:35:51 +00:00
joshieDo
425a021e3b feat: add edge feature flag to reth (#20841) 2026-01-09 15:33:21 +00:00
Hwangjae Lee
08c0d30ea7 docs(reth): fix outdated comments and document missing features (#20849)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-09 15:32:17 +00:00
かりんとう
84e970e4c9 perf: remove redundant contains_key (#20820) 2026-01-09 15:22:06 +00:00
Fibonacci747
020f20db42 chore: correct StorageHistory prune map size constant name (#20828) 2026-01-09 15:20:02 +00:00
ANtutov
f53929e0c8 docs: clarify bodies downloader set_download_range semantics (#20821) 2026-01-09 15:18:37 +00:00
ethfanWilliam
4a8fbe15e3 chore: remove unused implementation (#20885) 2026-01-09 15:08:06 +00:00
yyhrnk
a59e9832e6 docs: document optional block param for trace_rawTransaction (#20812)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-09 15:04:29 +00:00
YK
07beb76cf7 feat(reth-bench-compare): add persistence-based flow optimization for reth-bench (#20869)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-01-09 14:58:21 +00:00
FT
3ddf0bd729 docs: correct typo in hive.yml workflow comment (#20884) 2026-01-09 14:50:05 +00:00
iPLAY888
c3d92ddfc2 docs(engine): update outdated EthBuiltPayload comment (#20883) 2026-01-09 14:45:11 +00:00
kurahin
c0628dfbff refactor(config): delegate PruneConfig::has_receipts_pruning (#20809) 2026-01-09 14:44:43 +00:00
Sabnock
a2aa1f18df feat(rpc): add debug_getBlockAccessList endpoint for EIP-7928 (#20824)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-09 13:29:37 +00:00
Arun Dhyani
d489f80f6b feat: Add TrieUpdatesSorted and HashedPostStateSorted in all ExEx notifications (#20333)
Co-authored-by: Brian Picciano <me@mediocregopher.com>
2026-01-09 13:06:41 +00:00
Emilia Hane
bf272c9432 chore(consensus): Add trait object error variant to ConsensusError (#20875)
Co-authored-by: leeli <Leeliren@proton.me>
2026-01-09 13:01:22 +00:00
FT
ebb54d0dcc docs: typo in comment (#20879) 2026-01-09 13:00:22 +00:00
Matthias Seitz
1d7367c389 perf(engine): simplify get_prefetch_proof_targets (#20864)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 12:53:28 +00:00
refcell
824ae12d75 feat(exex): Make WAL Block Threshold Configurable (#20867) 2026-01-09 12:45:53 +00:00
Alexey Shekhirin
2db281e51d feat(reth-bench-compare): nP latency mean change percent (#20881) 2026-01-09 11:58:08 +00:00
Brian Picciano
8367ba473e feat(metrics): Add metrics for save_block steps and computed trie input sizes (#20878) 2026-01-09 11:40:35 +00:00
fig
f2abad5f5c perf(engine): destructure leaf to avoid clone() (#20863) 2026-01-09 11:19:49 +00:00
Matthias Seitz
4673d77c03 perf(trie): optimize ChunkedHashedPostState sorting (#20866) 2026-01-09 11:18:28 +00:00
Matthias Seitz
33bcd60348 feat(rpc): add persisted block subscription (#20877)
Co-authored-by: cakevm <cakevm@proton.me>
2026-01-09 10:37:46 +00:00
Matthias Seitz
8a9b5d90f4 feat(chain-state): add persisted block tracking (#20876)
Co-authored-by: cakevm <cakevm@proton.me>
2026-01-09 09:56:20 +00:00
joshieDo
c26cfa3dcb fix: pre-calculate transitions on append_blocks_with_state (#20850) 2026-01-09 09:26:46 +00:00
joshieDo
13e59651f1 fix: initialize transaction-senders sf during genesis (#20846) 2026-01-09 09:26:26 +00:00
Hwangjae Lee
0f4995d1ea chore(trie): fix typo in comment (#20870)
Signed-off-by: Hwangjae Lee <meetrick@gmail.com>
2026-01-09 09:19:14 +00:00
Matthias Seitz
cff7e8be53 perf(engine): avoid unnecessary B256 copy in get_proof_targets (#20845)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 04:57:23 +00:00
YK
5433d7a4ac feat(storage): add RocksDB history lookup methods and owned batch type [2/3] (#20543) 2026-01-09 04:52:15 +00:00
fig
1866db4d50 chore(engine): remove unnecessary debug-level clone() (#20862) 2026-01-08 22:21:29 +00:00
Danno Ferrin
c9b92550b6 feat(network): add customizable announcement filtering policy to APIs (#20861)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-01-08 22:08:41 +00:00
Sebastian Stammler
8e81ebfc1f feat(optimism): Also require non-zero elasticity in payload attributes (#20858) 2026-01-08 21:32:46 +00:00
joshieDo
1363205b5d feat: allow TransactionHashNumbers to be written to rocksdb during live sync (#20853) 2026-01-08 20:02:49 +00:00
DaniPopes
ed201cae0e chore(rbc): improve compilation log message (#20855) 2026-01-08 19:30:04 +00:00
Matthias Seitz
a5b10f11ce perf(engine): handle EmptyProof inline during prefetch batching (#20848)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 19:12:14 +00:00
Brian Picciano
a06644944f feat(trie): Keep cached storage roots on proof workers (#20838) 2026-01-08 17:04:42 +00:00
Matthias Seitz
8eecad3d1d chore(engine): remove state update batching in multiproof (#20842)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 16:04:12 +00:00
Emilia Hane
412f39e223 chore(consensus): Remove associated type Consensus::Error (#20843)
Co-authored-by: Josh_dfG <126518346+JoshdfG@users.noreply.github.com>
2026-01-08 15:54:31 +00:00
Matthias Seitz
13106233e4 perf(engine): use crossbeam select for persistence events (#20813) 2026-01-08 15:47:50 +00:00
joshieDo
e63fef0e79 ci: rocksdb job to unit.yml (#20839) 2026-01-08 13:20:43 +00:00
Dan Cline
eed34254f5 feat: add StaticFileSegment::AccountChangeSets (#18882)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-01-08 12:05:05 +00:00
Emilia Hane
b38d37a1e1 feat(sdk): Add custom TrieType (#20804)
Co-authored-by: jagroot <4516586+itschaindev@users.noreply.github.com>
2026-01-08 11:53:27 +00:00
Maxim Evtush
7efaf4ca97 docs: mention optional EraStage in DefaultStages documentation (#20836) 2026-01-08 11:51:10 +00:00
Emilia Hane
ef708792a9 chore(storage): Add trait object error variant to DatabaseError (#20096) 2026-01-08 11:40:09 +00:00
Alexey Shekhirin
bcd74d021b feat(metrics): configurable jeprof pprof dumps directory (#20834) 2026-01-08 11:21:42 +00:00
bigbear
0f0a181fe2 fix(trie): account for all flag in PrefixSet::is_empty() (#20801) 2026-01-08 11:20:55 +00:00
Matthias Seitz
9678d6c76d chore: tighten iat timeout (#20835) 2026-01-08 11:09:03 +00:00
Brian Picciano
7ceca70353 feat(trie): Add flag to enable proof v2 for storage proof workers (#20617)
Co-authored-by: YK <chiayongkang@hotmail.com>
2026-01-08 10:53:24 +00:00
Matthias Seitz
4412a501eb perf(chain-state): avoid clones in deferred trie computation (#20816) 2026-01-08 09:25:32 +00:00
YK
3ca5cf49b6 refactor(storage): extract shared find_changeset_block_from_index algorithm [1/3] (#20542) 2026-01-08 02:56:38 +00:00
Matthias Seitz
1d4603769f perf(trie): use sorted_unstable for proof target chunking (#20827) 2026-01-08 01:05:14 +00:00
Lorsmirq Benton
9bba8c7a98 docs(net): complete incomplete doc comment (#20793) 2026-01-07 21:16:00 +00:00
Alexey Shekhirin
6f0ef914b9 feat(metrics): jemalloc heap dump endpoint (#20811) 2026-01-07 19:36:08 +00:00
Alexey Shekhirin
d756e8310a chore(engine): more logs when cache is not available (#20817)
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-01-07 19:35:27 +00:00
DaniPopes
74a7ba581c feat(rbc): don't wait in between FCUs when warming up (#20818) 2026-01-07 19:20:33 +00:00
Matthias Seitz
a8980bf7c1 chore: ignore RUSTSEC-2026-0002 (#20819) 2026-01-07 18:47:09 +00:00
Matthias Seitz
050d9f440f chore: ignore RUSTSEC-2025-0141 bincode advisory (#20815) 2026-01-07 19:10:30 +01:00
Brian Picciano
df33a8200f feat(reth-bench-compare): Do unwind first (#20808) 2026-01-07 16:49:07 +00:00
Matthias Seitz
d3dab613fc revert: "perf(engine): parellelize multiproof_targets_from_state (#206… (#20807) 2026-01-07 15:49:10 +00:00
Matthias Seitz
1b31739adf revert: "perf(engine): paralellize evm_state_to_hashed_post_state() (#… (#20806) 2026-01-07 15:47:15 +00:00
DaniPopes
6280abedd0 chore(reth-bench-compare): skip last unwind (#20805) 2026-01-07 15:44:36 +00:00
Mohan Somnath
4c064a4d20 docs: fix article and grammar errors in comments (#20794) 2026-01-07 15:00:13 +00:00
phrwlk
8d19a36492 docs: clarify pending pending_block build_block docs (#20800) 2026-01-07 14:09:54 +00:00
cui
78f2685ee9 perf: remove unnecessary code (#20719)
Co-authored-by: weixie.cui <weixie.cui@okg.com>
2026-01-07 12:12:17 +00:00
YK
fee7e997ff refactor(trie): replace TrieMasks with Option<BranchNodeMasks> (#20707) 2026-01-07 11:27:23 +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
444 changed files with 15104 additions and 6207 deletions

View File

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

View File

@@ -28,10 +28,14 @@ jobs:
build:
- name: 'Build and push the nightly reth image'
command: 'make PROFILE=maxperf docker-build-push-nightly'
- name: 'Build and push the nightly edge profiling reth image'
command: 'make PROFILE=profiling docker-build-push-nightly-edge-profiling'
- name: 'Build and push the nightly profiling reth image'
command: 'make PROFILE=profiling docker-build-push-nightly-profiling'
- name: 'Build and push the nightly op-reth image'
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=maxperf op-docker-build-push-nightly'
- name: 'Build and push the nightly edge profiling op-reth image'
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=profiling op-docker-build-push-nightly-edge-profiling'
- name: 'Build and push the nightly profiling op-reth image'
command: 'make IMAGE_NAME=$OP_IMAGE_NAME DOCKER_IMAGE_NAME=$OP_DOCKER_IMAGE_NAME PROFILE=profiling op-docker-build-push-nightly-profiling'
steps:

View File

@@ -15,16 +15,27 @@ concurrency:
cancel-in-progress: true
jobs:
prepare-reth:
prepare-reth-stable:
uses: ./.github/workflows/prepare-reth.yml
with:
image_tag: ghcr.io/paradigmxyz/reth:latest
binary_name: reth
cargo_features: "asm-keccak"
artifact_name: "reth-stable"
prepare-reth-edge:
uses: ./.github/workflows/prepare-reth.yml
with:
image_tag: ghcr.io/paradigmxyz/reth:latest
binary_name: reth
cargo_features: "asm-keccak edge"
artifact_name: "reth-edge"
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
@@ -76,6 +87,7 @@ jobs:
strategy:
fail-fast: false
matrix:
storage: [stable, edge]
# ethereum/rpc to be deprecated:
# https://github.com/ethereum/hive/pull/1117
scenario:
@@ -85,7 +97,7 @@ jobs:
- sim: devp2p
limit: discv4
# started failing after https://github.com/ethereum/go-ethereum/pull/31843, no
# action on our side, remove from here when we get unxpected passes on these tests
# action on our side, remove from here when we get unexpected passes on these tests
# - sim: devp2p
# limit: eth
# include:
@@ -175,10 +187,12 @@ jobs:
- sim: ethereum/eels/consume-rlp
limit: .*tests/paris.*
needs:
- prepare-reth
- prepare-reth-stable
- prepare-reth-edge
- prepare-hive
name: run ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
runs-on: depot-ubuntu-latest-16
name: ${{ matrix.storage }} / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
runs-on:
group: Reth
permissions:
issues: write
steps:
@@ -195,7 +209,7 @@ jobs:
- name: Download reth image
uses: actions/download-artifact@v7
with:
name: artifacts
name: reth-${{ matrix.storage }}
path: /tmp
- name: Load Docker images

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

View File

@@ -21,6 +21,11 @@ on:
required: false
type: string
description: "Optional cargo package path"
artifact_name:
required: false
type: string
default: "artifacts"
description: "Name for the uploaded artifact"
jobs:
prepare-reth:
@@ -52,5 +57,5 @@ jobs:
id: upload
uses: actions/upload-artifact@v6
with:
name: artifacts
name: ${{ inputs.artifact_name }}
path: ./artifacts

View File

@@ -19,29 +19,22 @@ concurrency:
jobs:
test:
name: test / ${{ matrix.type }} (${{ matrix.partition }}/${{ matrix.total_partitions }})
name: test / ${{ matrix.type }} / ${{ matrix.storage }}
runs-on: depot-ubuntu-latest-4
env:
RUST_BACKTRACE: 1
EDGE_FEATURES: ${{ matrix.storage == 'edge' && 'edge' || '' }}
strategy:
matrix:
type: [ethereum, optimism]
storage: [stable, edge]
include:
- type: ethereum
args: --features "asm-keccak ethereum" --locked
partition: 1
total_partitions: 2
- type: ethereum
args: --features "asm-keccak ethereum" --locked
partition: 2
total_partitions: 2
features: asm-keccak ethereum
exclude_args: ""
- type: optimism
args: --features "asm-keccak" --locked --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
partition: 1
total_partitions: 2
- type: optimism
args: --features "asm-keccak" --locked --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
partition: 2
total_partitions: 2
features: asm-keccak
exclude_args: --exclude reth --exclude reth-bench --exclude "example-*" --exclude "reth-ethereum-*" --exclude "*-ethereum"
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
@@ -59,9 +52,9 @@ jobs:
- name: Run tests
run: |
cargo nextest run \
${{ matrix.args }} --workspace \
--features "${{ matrix.features }} $EDGE_FEATURES" --locked \
${{ matrix.exclude_args }} --workspace \
--exclude ef-tests --no-tests=warn \
--partition hash:${{ matrix.partition }}/2 \
-E "!kind(test) and not binary(e2e_testsuite)"
state:

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

View File

@@ -18,7 +18,7 @@ Reth is a high-performance Ethereum execution client written in Rust, focusing o
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
9 **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
9. **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
### Key Design Principles

View File

@@ -51,9 +51,7 @@ elsewhere.
<!-- - **Asking in the support Telegram:** The [Foundry Support Telegram][support-tg] is a fast and easy way to ask questions. -->
<!-- - **Opening a discussion:** This repository comes with a discussions board where you can also ask for help. Click the "Discussions" tab at the top. -->
If you have reviewed existing documentation and still have questions, or you are having problems, you can get help by *
*opening a discussion**. This repository comes with a discussions board where you can also ask for help. Click the "
Discussions" tab at the top.
If you have reviewed existing documentation and still have questions, or you are having problems, you can get help by **opening a discussion**. This repository comes with a discussions board where you can also ask for help. Click the "Discussions" tab at the top.
As Reth is still in heavy development, the documentation can be a bit scattered. The [Reth Docs][reth-docs] is our
current best-effort attempt at keeping up-to-date information.

1052
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -487,7 +487,7 @@ revm-inspectors = "0.33.2"
alloy-chains = { version = "0.2.5", default-features = false }
alloy-dyn-abi = "1.4.1"
alloy-eip2124 = { version = "0.2.0", default-features = false }
alloy-eip7928 = { version = "0.1.0" }
alloy-eip7928 = { version = "0.1.0", default-features = false }
alloy-evm = { version = "0.25.1", default-features = false }
alloy-primitives = { version = "1.5.0", default-features = false, features = ["map-foldhash"] }
alloy-rlp = { version = "0.3.10", default-features = false, features = ["core-net"] }
@@ -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"
@@ -586,7 +587,7 @@ tracing-appender = "0.2"
url = { version = "2.3", default-features = false }
zstd = "0.13"
byteorder = "1"
mini-moka = "0.10"
fixed-cache = { version = "0.1.5", features = ["stats"] }
moka = "0.12"
tar-no-std = { version = "0.3.2", default-features = false }
miniz_oxide = { version = "0.8.4", default-features = false }
@@ -595,9 +596,9 @@ chrono = "0.4.41"
# metrics
metrics = "0.24.0"
metrics-derive = "0.1"
metrics-exporter-prometheus = { version = "0.16.0", default-features = false }
metrics-exporter-prometheus = { version = "0.18.0", default-features = false }
metrics-process = "2.1.0"
metrics-util = { default-features = false, version = "0.19.0" }
metrics-util = { default-features = false, version = "0.20.0" }
# proc-macros
proc-macro2 = "1.0"
@@ -683,6 +684,7 @@ ethereum_ssz = "0.9.0"
ethereum_ssz_derive = "0.9.0"
# allocators
jemalloc_pprof = { version = "0.8", default-features = false }
tikv-jemalloc-ctl = "0.6"
tikv-jemallocator = "0.6"
tracy-client = "0.18.0"
@@ -693,7 +695,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,7 +732,9 @@ 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 }
tracing-tracy = "0.11"
triehash = "0.8"
typenum = "1.15.0"
vergen = "9.0.4"

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

@@ -276,13 +276,18 @@ docker-build-push-latest: ## Build and push a cross-arch Docker image tagged wit
docker-build-push-nightly: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
$(call docker_build_push,nightly,nightly)
.PHONY: docker-build-push-nightly-edge-profiling
docker-build-push-nightly-edge-profiling: FEATURES := $(FEATURES) edge
docker-build-push-nightly-edge-profiling: ## Build and push cross-arch Docker image with edge features tagged with `nightly-edge-profiling`.
$(call docker_build_push,nightly-edge-profiling,nightly-edge-profiling)
# Create a cross-arch Docker image with the given tags and push it
define docker_build_push
$(MAKE) build-x86_64-unknown-linux-gnu
$(MAKE) FEATURES="$(FEATURES)" build-x86_64-unknown-linux-gnu
mkdir -p $(BIN_DIR)/amd64
cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/reth $(BIN_DIR)/amd64/reth
$(MAKE) build-aarch64-unknown-linux-gnu
$(MAKE) FEATURES="$(FEATURES)" build-aarch64-unknown-linux-gnu
mkdir -p $(BIN_DIR)/arm64
cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/reth $(BIN_DIR)/arm64/reth
@@ -328,6 +333,11 @@ op-docker-build-push-latest: ## Build and push a cross-arch Docker image tagged
op-docker-build-push-nightly: ## Build and push cross-arch Docker image tagged with the latest git tag with a `-nightly` suffix, and `latest-nightly`.
$(call op_docker_build_push,nightly,nightly)
.PHONY: op-docker-build-push-nightly-edge-profiling
op-docker-build-push-nightly-edge-profiling: FEATURES := $(FEATURES) edge
op-docker-build-push-nightly-edge-profiling: ## Build and push cross-arch Docker image with edge features tagged with `nightly-edge-profiling`.
$(call op_docker_build_push,nightly-edge-profiling,nightly-edge-profiling)
# Note: This requires a buildx builder with emulation support. For example:
#
# `docker run --privileged --rm tonistiigi/binfmt --install amd64,arm64`
@@ -347,11 +357,11 @@ op-docker-build-push-nightly-profiling: ## Build and push cross-arch Docker imag
# Create a cross-arch Docker image with the given tags and push it
define op_docker_build_push
$(MAKE) op-build-x86_64-unknown-linux-gnu
$(MAKE) FEATURES="$(FEATURES)" op-build-x86_64-unknown-linux-gnu
mkdir -p $(BIN_DIR)/amd64
cp $(CARGO_TARGET_DIR)/x86_64-unknown-linux-gnu/$(PROFILE)/op-reth $(BIN_DIR)/amd64/op-reth
$(MAKE) op-build-aarch64-unknown-linux-gnu
$(MAKE) FEATURES="$(FEATURES)" op-build-aarch64-unknown-linux-gnu
mkdir -p $(BIN_DIR)/arm64
cp $(CARGO_TARGET_DIR)/aarch64-unknown-linux-gnu/$(PROFILE)/op-reth $(BIN_DIR)/arm64/op-reth

View File

@@ -44,7 +44,7 @@ More historical context below:
- We released 1.0 "production-ready" stable Reth in June 2024.
- Reth completed an audit with [Sigma Prime](https://sigmaprime.io/), the developers of [Lighthouse](https://github.com/sigp/lighthouse), the Rust Consensus Layer implementation. Find it [here](./audit/sigma_prime_audit_v2.pdf).
- Revm (the EVM used in Reth) underwent an audit with [Guido Vranken](https://x.com/guidovranken) (#1 [Ethereum Bug Bounty](https://ethereum.org/en/bug-bounty)). We will publish the results soon.
- We released multiple iterative beta versions, up to [beta.9](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.9) on Monday June 3, 2024,the last beta release.
- We released multiple iterative beta versions, up to [beta.9](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.9) on Monday June 3, 2024, the last beta release.
- We released [beta](https://github.com/paradigmxyz/reth/releases/tag/v0.2.0-beta.1) on Monday March 4, 2024, our first breaking change to the database model, providing faster query speed, smaller database footprint, and allowing "history" to be mounted on separate drives.
- We shipped iterative improvements until the last alpha release on February 28, 2024, [0.1.0-alpha.21](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.21).
- We [initially announced](https://www.paradigm.xyz/2023/06/reth-alpha) [0.1.0-alpha.1](https://github.com/paradigmxyz/reth/releases/tag/v0.1.0-alpha.1) on June 20, 2023.

View File

@@ -25,7 +25,9 @@ reth-chainspec.workspace = true
# alloy
alloy-provider = { workspace = true, features = ["reqwest-rustls-tls"], default-features = false }
alloy-rpc-client = { workspace = true, features = ["pubsub"] }
alloy-rpc-types-eth.workspace = true
alloy-transport-ws.workspace = true
alloy-primitives.workspace = true
# CLI and argument parsing
@@ -69,7 +71,11 @@ jemalloc = [
"reth-node-core/jemalloc",
]
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
tracy-allocator = ["reth-cli-util/tracy-allocator"]
tracy-allocator = ["reth-cli-util/tracy-allocator", "tracy"]
tracy = [
"reth-node-core/tracy",
"reth-tracing/tracy",
]
min-error-logs = [
"tracing/release_max_level_error",

View File

@@ -0,0 +1,50 @@
# reth-bench-compare
Compare reth performance between two git references.
## Usage
```bash
reth-bench-compare \
--baseline-ref main \
--feature-ref my-feature \
--blocks 100 \
--wait-for-persistence
```
## Arguments
| Argument | Description | Default | Required |
|----------|-------------|---------|----------|
| `--baseline-ref <REF>` | Git reference for baseline | - | Yes |
| `--feature-ref <REF>` | Git reference to compare | - | Yes |
| `--blocks <N>` | Number of blocks to benchmark | `100` | No |
| `--chain <CHAIN>` | Chain to benchmark | `mainnet` | No |
| `--datadir <PATH>` | Data directory path | OS-specific | No |
| `--rpc-url <URL>` | RPC endpoint for block data | Chain default | No |
| `--output-dir <PATH>` | Output directory | `./reth-bench-compare` | No |
| `--wait-for-persistence` | Wait for block persistence | `false` | No |
| `--persistence-threshold <N>` | Wait after every N+1 blocks | `2` | No |
| `--wait-time <DURATION>` | Fixed delay (legacy) | - | No |
| `--warmup-blocks <N>` | Cache warmup blocks | Same as `--blocks` | No |
| `--draw` | Generate charts (needs Python/uv) | `false` | No |
| `--profile` | Enable CPU profiling (needs samply) | `false` | No |
| `-vvvv` | Debug logging | Info | No |
| `--features <FEATURES>` | Rust features for both builds | `jemalloc,asm-keccak` | No |
| `--rustflags <FLAGS>` | RUSTFLAGS for both builds | `-C target-cpu=native` | No |
| `--baseline-features <FEATURES>` | Features for baseline only | Inherits `--features` | No |
| `--feature-features <FEATURES>` | Features for feature only | Inherits `--features` | No |
| `--baseline-rustflags <FLAGS>` | RUSTFLAGS for baseline only | Inherits `--rustflags` | No |
| `--feature-rustflags <FLAGS>` | RUSTFLAGS for feature only | Inherits `--rustflags` | No |
| `--baseline-args <ARGS>` | Extra args for baseline node | - | No |
| `--feature-args <ARGS>` | Extra args for feature node | - | No |
| `--metrics-port <PORT>` | Metrics endpoint port | `5005` | No |
| `--sudo` | Run with elevated privileges | `false` | No |
## Output
Results in `./reth-bench-compare/results/<timestamp>/`:
- `comparison_report.json` - Metrics comparison
- `per_block_comparison.csv` - Per-block statistics
- `baseline/` and `feature/` - Individual run results
- `latency_comparison.png` - Chart (if `--draw` used)

View File

@@ -18,6 +18,8 @@ pub(crate) struct BenchmarkRunner {
rpc_url: String,
jwt_secret: String,
wait_time: Option<String>,
wait_for_persistence: bool,
persistence_threshold: Option<u64>,
warmup_blocks: u64,
}
@@ -28,6 +30,8 @@ impl BenchmarkRunner {
rpc_url: args.get_rpc_url(),
jwt_secret: args.jwt_secret_path().to_string_lossy().to_string(),
wait_time: args.wait_time.clone(),
wait_for_persistence: args.wait_for_persistence,
persistence_threshold: args.persistence_threshold,
warmup_blocks: args.get_warmup_blocks(),
}
}
@@ -96,13 +100,9 @@ impl BenchmarkRunner {
&from_block.to_string(),
"--to",
&to_block.to_string(),
"--wait-time=0ms", // Warmup should avoid persistence waits.
]);
// Add wait-time argument if provided
if let Some(ref wait_time) = self.wait_time {
cmd.args(["--wait-time", wait_time]);
}
cmd.env("RUST_LOG_STYLE", "never")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
@@ -186,9 +186,16 @@ impl BenchmarkRunner {
&output_dir.to_string_lossy(),
]);
// Add wait-time argument if provided
// Configure wait mode: wait-time takes precedence over persistence-based flow
if let Some(ref wait_time) = self.wait_time {
cmd.args(["--wait-time", wait_time]);
} else if self.wait_for_persistence {
cmd.arg("--wait-for-persistence");
// Add persistence threshold if specified
if let Some(threshold) = self.persistence_threshold {
cmd.args(["--persistence-threshold", &threshold.to_string()]);
}
}
cmd.env("RUST_LOG_STYLE", "never")

View File

@@ -114,10 +114,29 @@ pub(crate) struct Args {
#[arg(long)]
pub profile: bool,
/// Wait time between engine API calls (passed to reth-bench)
#[arg(long, value_name = "DURATION")]
/// Optional fixed delay between engine API calls (passed to reth-bench).
///
/// When set, reth-bench uses wait-time mode and disables persistence-based flow.
/// This flag remains for compatibility with older scripts.
#[arg(long, value_name = "DURATION", hide = true)]
pub wait_time: Option<String>,
/// Wait for blocks to be persisted before sending the next batch (passed to reth-bench).
///
/// When enabled, waits for every Nth block to be persisted using the
/// `reth_subscribePersistedBlock` subscription. This ensures the benchmark
/// doesn't outpace persistence.
#[arg(long)]
pub wait_for_persistence: bool,
/// Engine persistence threshold (passed to reth-bench).
///
/// The benchmark waits after every `(threshold + 1)` blocks. By default this
/// matches the engine's default persistence threshold (2), so waits occur
/// at blocks 3, 6, 9, etc.
#[arg(long, value_name = "PERSISTENCE_THRESHOLD")]
pub persistence_threshold: Option<u64>,
/// Number of blocks to run for cache warmup after clearing caches.
/// If not specified, defaults to the same as --blocks
#[arg(long, value_name = "N")]
@@ -164,12 +183,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 +377,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 +496,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 +519,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);
}
@@ -471,6 +531,7 @@ async fn run_compilation_phase(
Ok((baseline_commit, feature_commit))
}
#[allow(clippy::too_many_arguments)]
/// Run warmup phase to warm up caches before benchmarking
async fn run_warmup_phase(
git_manager: &GitManager,
@@ -480,9 +541,15 @@ async fn run_warmup_phase(
args: &Args,
is_optimism: bool,
baseline_commit: &str,
starting_tip: u64,
) -> Result<()> {
info!("=== Running warmup phase ===");
// Unwind to starting block minus warmup blocks, so we end up back at starting_tip
let warmup_blocks = args.get_warmup_blocks();
let unwind_target = starting_tip.saturating_sub(warmup_blocks);
node_manager.unwind_to_block(unwind_target).await?;
// Use baseline for warmup
let warmup_ref = &args.baseline_ref;
@@ -511,12 +578,9 @@ async fn run_warmup_phase(
node_manager.start_node(&binary_path, warmup_ref, "warmup", &additional_args).await?;
// Wait for node to be ready and get its current tip
let current_tip = node_manager.wait_for_node_ready_and_get_tip().await?;
let current_tip = node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?;
info!("Warmup node is ready at tip: {}", current_tip);
// Store the tip we'll unwind back to
let original_tip = current_tip;
// Clear filesystem caches before warmup run only (unless disabled)
if args.no_clear_cache {
info!("Skipping filesystem cache clearing (--no-clear-cache flag set)");
@@ -527,12 +591,9 @@ async fn run_warmup_phase(
// Run warmup to warm up caches
benchmark_runner.run_warmup(current_tip).await?;
// Stop node before unwinding (node must be stopped to release database lock)
// Stop node after warmup
node_manager.stop_node(&mut node_process).await?;
// Unwind back to starting block after warmup
node_manager.unwind_to_block(original_tip).await?;
info!("Warmup phase completed");
Ok(())
}
@@ -554,6 +615,27 @@ async fn run_benchmark_workflow(
let (baseline_commit, feature_commit) =
run_compilation_phase(git_manager, compilation_manager, args, is_optimism).await?;
// Switch to baseline reference and get the starting tip
git_manager.switch_ref(&args.baseline_ref)?;
let binary_path =
compilation_manager.get_cached_binary_path_for_commit(&baseline_commit, is_optimism);
if !binary_path.exists() {
return Err(eyre!(
"Cached baseline binary not found at {:?}. Compilation phase should have created it.",
binary_path
));
}
// Start node briefly to get the current tip, then stop it
info!("=== Determining initial block height ===");
let additional_args = args.build_additional_args("baseline", args.baseline_args.as_ref());
let (mut node_process, _) = node_manager
.start_node(&binary_path, &args.baseline_ref, "baseline", &additional_args)
.await?;
let starting_tip = node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?;
info!("Node starting tip: {}", starting_tip);
node_manager.stop_node(&mut node_process).await?;
// Run warmup phase before benchmarking (skip if warmup_blocks is 0)
if args.get_warmup_blocks() > 0 {
run_warmup_phase(
@@ -564,6 +646,7 @@ async fn run_benchmark_workflow(
args,
is_optimism,
&baseline_commit,
starting_tip,
)
.await?;
} else {
@@ -579,6 +662,10 @@ async fn run_benchmark_workflow(
let commit = commits[i];
info!("=== Processing {} reference: {} ===", ref_type, git_ref);
// Unwind to starting block minus benchmark blocks, so we end up back at starting_tip
let unwind_target = starting_tip.saturating_sub(args.blocks);
node_manager.unwind_to_block(unwind_target).await?;
// Switch to target reference
git_manager.switch_ref(git_ref)?;
@@ -612,17 +699,14 @@ async fn run_benchmark_workflow(
node_manager.start_node(&binary_path, git_ref, ref_type, &additional_args).await?;
// Wait for node to be ready and get its current tip (wherever it is)
let current_tip = node_manager.wait_for_node_ready_and_get_tip().await?;
let current_tip = node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?;
info!("Node is ready at tip: {}", current_tip);
// Store the tip we'll unwind back to
let original_tip = current_tip;
// Calculate benchmark range
// Note: reth-bench has an off-by-one error where it consumes the first block
// of the range, so we add 1 to compensate and get exactly args.blocks blocks
let from_block = original_tip;
let to_block = original_tip + args.blocks;
let from_block = current_tip;
let to_block = current_tip + args.blocks;
// Run benchmark
let output_dir = comparison_generator.get_ref_output_dir(ref_type);
@@ -639,9 +723,6 @@ async fn run_benchmark_workflow(
// Stop node
node_manager.stop_node(&mut node_process).await?;
// Unwind back to original tip
node_manager.unwind_to_block(original_tip).await?;
// Store results for comparison
comparison_generator.add_ref_results(ref_type, &output_dir)?;

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,
}
@@ -97,6 +99,7 @@ pub(crate) struct RefInfo {
/// Summary of the comparison between references.
///
/// Percent deltas are `(feature - baseline) / baseline * 100`:
/// - `new_payload_latency_mean_change_percent`: percent changes of the per-block means.
/// - `new_payload_latency_p50_change_percent` / p90 / p99: percent changes of the respective
/// per-block percentiles.
/// - `per_block_latency_change_mean_percent` / `per_block_latency_change_median_percent` are the
@@ -114,6 +117,7 @@ pub(crate) struct ComparisonSummary {
pub per_block_latency_change_median_percent: f64,
pub per_block_latency_change_std_dev_percent: f64,
pub new_payload_total_latency_change_percent: f64,
pub new_payload_latency_mean_change_percent: f64,
pub new_payload_latency_p50_change_percent: f64,
pub new_payload_latency_p90_change_percent: f64,
pub new_payload_latency_p99_change_percent: f64,
@@ -125,7 +129,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,
@@ -442,6 +447,10 @@ impl ComparisonGenerator {
per_block_latency_change_median_percent,
per_block_latency_change_std_dev_percent,
new_payload_total_latency_change_percent,
new_payload_latency_mean_change_percent: calc_percent_change(
baseline.mean_new_payload_latency_ms,
feature.mean_new_payload_latency_ms,
),
new_payload_latency_p50_change_percent: calc_percent_change(
baseline.median_new_payload_latency_ms,
feature.median_new_payload_latency_ms,
@@ -572,6 +581,10 @@ impl ComparisonGenerator {
" Total newPayload time change: {:+.2}%",
summary.new_payload_total_latency_change_percent
);
println!(
" NewPayload Latency mean: {:+.2}%",
summary.new_payload_latency_mean_change_percent
);
println!(
" NewPayload Latency p50: {:+.2}%",
summary.new_payload_latency_p50_change_percent

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,11 +117,11 @@ 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);
info!("Compiling {binary_name} with {cmd:?}");
let output = cmd.output().wrap_err("Failed to execute cargo build command")?;
@@ -227,8 +230,7 @@ impl CompilationManager {
let mut cmd = Command::new("cargo");
cmd.args(["install", "--locked", "samply"]);
// Debug log the command
debug!("Executing cargo command: {:?}", cmd);
info!("Installing samply with {cmd:?}");
let output = cmd.output().wrap_err("Failed to execute cargo install samply command")?;
@@ -303,8 +305,7 @@ impl CompilationManager {
let mut cmd = Command::new("make");
cmd.arg("install-reth-bench").current_dir(&self.repo_root);
// Debug log the command
debug!("Executing make command: {:?}", cmd);
info!("Compiling reth-bench with {cmd:?}");
let output = cmd.output().wrap_err("Failed to execute make install-reth-bench command")?;

View File

@@ -2,7 +2,9 @@
use crate::cli::Args;
use alloy_provider::{Provider, ProviderBuilder};
use alloy_rpc_client::RpcClient;
use alloy_rpc_types_eth::SyncStatus;
use alloy_transport_ws::WsConnect;
use eyre::{eyre, OptionExt, Result, WrapErr};
#[cfg(unix)]
use nix::sys::signal::{killpg, Signal};
@@ -18,6 +20,9 @@ use tokio::{
};
use tracing::{debug, info, warn};
/// Default websocket RPC port used by reth
const DEFAULT_WS_RPC_PORT: u16 = 8546;
/// Manages reth node lifecycle and operations
pub(crate) struct NodeManager {
datadir: Option<String>,
@@ -152,7 +157,10 @@ impl NodeManager {
metrics_arg,
"--http".to_string(),
"--http.api".to_string(),
"eth".to_string(),
"eth,reth".to_string(),
"--ws".to_string(),
"--ws.api".to_string(),
"eth,reth".to_string(),
"--disable-discovery".to_string(),
"--trusted-only".to_string(),
]);
@@ -211,6 +219,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");
@@ -354,8 +367,13 @@ impl NodeManager {
Ok((child, reth_command))
}
/// Wait for the node to be ready and return its current tip
pub(crate) async fn wait_for_node_ready_and_get_tip(&self) -> Result<u64> {
/// Wait for the node to be ready and return its current tip.
///
/// Fails early if the node process exits before becoming ready.
pub(crate) async fn wait_for_node_ready_and_get_tip(
&self,
child: &mut tokio::process::Child,
) -> Result<u64> {
info!("Waiting for node to be ready and synced...");
let max_wait = Duration::from_secs(120); // 2 minutes to allow for sync
@@ -366,8 +384,23 @@ impl NodeManager {
let url = rpc_url.parse().map_err(|e| eyre!("Invalid RPC URL '{}': {}", rpc_url, e))?;
let provider = ProviderBuilder::new().connect_http(url);
let start_time = tokio::time::Instant::now();
let mut iteration = 0;
timeout(max_wait, async {
loop {
iteration += 1;
debug!(
"Readiness check iteration {} (elapsed: {:?})",
iteration,
start_time.elapsed()
);
// Check if the node process has exited.
if let Some(status) = child.try_wait()? {
return Err(eyre!("Node process exited unexpectedly with {status}"));
}
// First check if RPC is up and node is not syncing
match provider.syncing().await {
Ok(sync_result) => {
@@ -376,24 +409,48 @@ impl NodeManager {
debug!("Node is still syncing {sync_info:?}, waiting...");
}
_ => {
debug!("HTTP RPC is up and node is not syncing, checking block number...");
// Node is not syncing, now get the tip
match provider.get_block_number().await {
Ok(tip) => {
info!("Node is ready and not syncing at block: {}", tip);
return Ok(tip);
debug!("HTTP RPC ready at block: {}, checking WebSocket...", tip);
// Verify WebSocket RPC is ready (public endpoint, no JWT required)
let ws_url = format!("ws://localhost:{}", DEFAULT_WS_RPC_PORT);
debug!("Attempting WebSocket connection to {} (public endpoint)", ws_url);
let ws_connect = WsConnect::new(&ws_url);
match RpcClient::connect_pubsub(ws_connect).await
{
Ok(_) => {
info!(
"Node is ready (HTTP and WebSocket) at block: {} (took {:?}, {} iterations)",
tip, start_time.elapsed(), iteration
);
return Ok(tip);
}
Err(e) => {
debug!(
"HTTP RPC ready but WebSocket not ready yet (iteration {}): {:?}",
iteration, e
);
debug!("WebSocket error details: {}", e);
}
}
}
Err(e) => {
debug!("Failed to get block number: {}", e);
debug!("Failed to get block number (iteration {}): {:?}", iteration, e);
}
}
}
}
}
Err(e) => {
debug!("Node RPC not ready yet or failed to check sync status: {}", e);
debug!("Node RPC not ready yet or failed to check sync status (iteration {}): {:?}", iteration, e);
}
}
debug!("Sleeping for {:?} before next check", check_interval);
sleep(check_interval).await;
}
})
@@ -403,7 +460,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 +609,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

@@ -16,6 +16,7 @@ workspace = true
# reth
reth-cli-runner.workspace = true
reth-cli-util.workspace = true
reth-engine-primitives.workspace = true
reth-fs-util.workspace = true
reth-node-api.workspace = true
reth-node-core.workspace = true
@@ -25,10 +26,11 @@ reth-tracing.workspace = true
# alloy
alloy-eips.workspace = true
alloy-json-rpc.workspace = true
alloy-network.workspace = true
alloy-primitives.workspace = true
alloy-provider = { workspace = true, features = ["engine-api", "reqwest-rustls-tls"], default-features = false }
alloy-provider = { workspace = true, features = ["engine-api", "pubsub", "reqwest-rustls-tls"], default-features = false }
alloy-pubsub.workspace = true
alloy-rpc-client.workspace = true
alloy-rpc-client = { workspace = true, features = ["pubsub"] }
alloy-rpc-types-engine.workspace = true
alloy-transport-http.workspace = true
alloy-transport-ipc.workspace = true
@@ -50,6 +52,9 @@ tracing.workspace = true
serde.workspace = true
serde_json.workspace = true
# url parsing
url.workspace = true
# async
async-trait.workspace = true
futures.workspace = true
@@ -58,6 +63,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
@@ -79,7 +85,11 @@ jemalloc = [
"reth-node-core/jemalloc",
]
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
tracy-allocator = ["reth-cli-util/tracy-allocator"]
tracy-allocator = ["reth-cli-util/tracy-allocator", "tracy"]
tracy = [
"reth-node-core/tracy",
"reth-tracing/tracy",
]
min-error-logs = [
"tracing/release_max_level_error",

View File

@@ -31,6 +31,14 @@ Otherwise, running `make maxperf` at the root of the repo should be sufficient f
`reth-bench` contains different commands to benchmark different patterns of engine API calls.
The `reth-bench new-payload-fcu` command is the most representative of ethereum mainnet live sync, alternating between sending `engine_newPayload` calls and `engine_forkchoiceUpdated` calls.
The `new-payload-fcu` command supports two optional waiting modes that can be used together or independently:
- `--wait-time <duration>`: Fixed sleep interval between blocks (e.g., `--wait-time 100ms`)
- `--wait-for-persistence`: Waits for blocks to be persisted using the `reth_subscribePersistedBlock` subscription
When using `--wait-for-persistence`, the benchmark waits after every `(threshold + 1)` blocks, where the threshold defaults to the engine's persistence threshold (2). This can be customized with `--persistence-threshold <N>`.
By default, the WebSocket URL for persistence subscriptions is derived from `--engine-rpc-url` (converting to ws:// on port 8546). Use `--ws-rpc-url` to override this.
Below is an overview of how to run a benchmark:
### Setup

View File

@@ -163,7 +163,7 @@ impl AuthenticatedTransport {
// shift the iat forward by one second so there is some buffer time
let mut shifted_claims = inner_and_claims.1;
shifted_claims.iat -= 1;
shifted_claims.iat -= 30;
// if the claims are out of date, reset the inner transport
if !shifted_claims.is_within_time_window() {

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

@@ -1,5 +1,13 @@
//! Runs the `reth bench` command, calling first newPayload for each block, then calling
//! forkchoiceUpdated.
//!
//! Supports configurable waiting behavior:
//! - **`--wait-time`**: Fixed sleep interval between blocks.
//! - **`--wait-for-persistence`**: Waits for every Nth block to be persisted using the
//! `reth_subscribePersistedBlock` subscription, where N matches the engine's persistence
//! threshold. This ensures the benchmark doesn't outpace persistence.
//!
//! Both options can be used together or independently.
use crate::{
bench::{
@@ -11,16 +19,26 @@ use crate::{
},
valid_payload::{block_to_new_payload, call_forkchoice_updated, call_new_payload},
};
use alloy_provider::Provider;
use alloy_eips::BlockNumHash;
use alloy_network::Ethereum;
use alloy_provider::{Provider, RootProvider};
use alloy_pubsub::SubscriptionStream;
use alloy_rpc_client::RpcClient;
use alloy_rpc_types_engine::ForkchoiceState;
use alloy_transport_ws::WsConnect;
use clap::Parser;
use csv::Writer;
use eyre::{Context, OptionExt};
use futures::StreamExt;
use humantime::parse_duration;
use reth_cli_runner::CliContext;
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
use reth_node_core::args::BenchmarkArgs;
use std::time::{Duration, Instant};
use tracing::{debug, info};
use url::Url;
const PERSISTENCE_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(60);
/// `reth benchmark new-payload-fcu` command
#[derive(Debug, Parser)]
@@ -30,8 +48,31 @@ pub struct Command {
rpc_url: String,
/// How long to wait after a forkchoice update before sending the next payload.
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, default_value = "250ms", verbatim_doc_comment)]
wait_time: Duration,
#[arg(long, value_name = "WAIT_TIME", value_parser = parse_duration, verbatim_doc_comment)]
wait_time: Option<Duration>,
/// Wait for blocks to be persisted before sending the next batch.
///
/// When enabled, waits for every Nth block to be persisted using the
/// `reth_subscribePersistedBlock` subscription. This ensures the benchmark
/// doesn't outpace persistence.
///
/// The subscription uses the regular RPC websocket endpoint (no JWT required).
#[arg(long, default_value = "false", verbatim_doc_comment)]
wait_for_persistence: bool,
/// Engine persistence threshold used for deciding when to wait for persistence.
///
/// The benchmark waits after every `(threshold + 1)` blocks. By default this
/// matches the engine's `DEFAULT_PERSISTENCE_THRESHOLD` (2), so waits occur
/// at blocks 3, 6, 9, etc.
#[arg(
long = "persistence-threshold",
value_name = "PERSISTENCE_THRESHOLD",
default_value_t = DEFAULT_PERSISTENCE_THRESHOLD,
verbatim_doc_comment
)]
persistence_threshold: u64,
/// The size of the block buffer (channel capacity) for prefetching blocks from the RPC
/// endpoint.
@@ -50,6 +91,32 @@ pub struct Command {
impl Command {
/// Execute `benchmark new-payload-fcu` command
pub async fn execute(self, _ctx: CliContext) -> eyre::Result<()> {
// Log mode configuration
if let Some(duration) = self.wait_time {
info!("Using wait-time mode with {}ms delay between blocks", duration.as_millis());
}
if self.wait_for_persistence {
info!(
"Persistence waiting enabled (waits after every {} blocks to match engine gap > {} behavior)",
self.persistence_threshold + 1,
self.persistence_threshold
);
}
// Set up waiter based on configured options (duration takes precedence)
let mut waiter = match (self.wait_time, self.wait_for_persistence) {
(Some(duration), _) => Some(PersistenceWaiter::with_duration(duration)),
(None, true) => {
let sub = self.setup_persistence_subscription().await?;
Some(PersistenceWaiter::with_subscription(
sub,
self.persistence_threshold,
PERSISTENCE_CHECKPOINT_TIMEOUT,
))
}
(None, false) => None,
};
let BenchContext {
benchmark_mode,
block_provider,
@@ -110,7 +177,6 @@ impl Command {
}
});
// put results in a summary vec so they can be printed at the end
let mut results = Vec::new();
let total_benchmark_duration = Instant::now();
let mut total_wait_time = Duration::ZERO;
@@ -121,14 +187,12 @@ impl Command {
total_wait_time += wait_start.elapsed();
result
} {
// just put gas used here
let gas_used = block.header.gas_used;
let block_number = block.header.number;
let transaction_count = block.transactions.len() as u64;
debug!(target: "reth-bench", ?block_number, "Sending payload",);
debug!(target: "reth-bench", ?block_number, "Sending payload");
// construct fcu to call
let forkchoice_state = ForkchoiceState {
head_block_hash: head,
safe_block_hash: safe,
@@ -143,7 +207,6 @@ impl Command {
call_forkchoice_updated(&auth_provider, version, forkchoice_state, None).await?;
// calculate the total duration and the fcu latency, record
let total_latency = start.elapsed();
let fcu_latency = total_latency - new_payload_result.latency;
let combined_result = CombinedResult {
@@ -154,17 +217,15 @@ impl Command {
total_latency,
};
// current duration since the start of the benchmark minus the time
// waiting for blocks
// Exclude time spent waiting on the block prefetch channel from the benchmark duration.
// We want to measure engine throughput, not RPC fetch latency.
let current_duration = total_benchmark_duration.elapsed() - total_wait_time;
// convert gas used to gigagas, then compute gigagas per second
info!(%combined_result);
// wait before sending the next payload
tokio::time::sleep(self.wait_time).await;
if let Some(w) = &mut waiter {
w.on_block(block_number).await?;
}
// record the current result
let gas_row =
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
results.push((gas_row, combined_result));
@@ -175,24 +236,26 @@ impl Command {
return Err(error);
}
// Drop waiter - we don't need to wait for final blocks to persist
// since the benchmark goal is measuring Ggas/s of newPayload/FCU, not persistence.
drop(waiter);
let (gas_output_results, combined_results): (_, Vec<CombinedResult>) =
results.into_iter().unzip();
// write the csv output to files
if let Some(path) = self.benchmark.output {
// first write the combined results to a file
// Write CSV output files
if let Some(ref path) = self.benchmark.output {
let output_path = path.join(COMBINED_OUTPUT_SUFFIX);
info!("Writing engine api call latency output to file: {:?}", output_path);
let mut writer = Writer::from_path(output_path)?;
let mut writer = Writer::from_path(&output_path)?;
for result in combined_results {
writer.serialize(result)?;
}
writer.flush()?;
// now write the gas output to a file
let output_path = path.join(GAS_OUTPUT_SUFFIX);
info!("Writing total gas output to file: {:?}", output_path);
let mut writer = Writer::from_path(output_path)?;
let mut writer = Writer::from_path(&output_path)?;
for row in &gas_output_results {
writer.serialize(row)?;
}
@@ -201,8 +264,8 @@ impl Command {
info!("Finished writing benchmark output files to {:?}.", path);
}
// accumulate the results and calculate the overall Ggas/s
let gas_output = TotalGasOutput::new(gas_output_results)?;
info!(
total_duration=?gas_output.total_duration,
total_gas_used=?gas_output.total_gas_used,
@@ -213,4 +276,278 @@ impl Command {
Ok(())
}
/// Returns the websocket RPC URL used for the persistence subscription.
///
/// Preference:
/// - If `--ws-rpc-url` is provided, use it directly.
/// - Otherwise, derive a WS RPC URL from `--engine-rpc-url`.
///
/// The persistence subscription endpoint (`reth_subscribePersistedBlock`) is exposed on
/// the regular RPC server (WS port, usually 8546), not on the engine API port (usually 8551).
/// Since `BenchmarkArgs` only has the engine URL by default, we convert the scheme
/// (http→ws, https→wss) and force the port to 8546.
fn derive_ws_rpc_url(&self) -> eyre::Result<Url> {
if let Some(ref ws_url) = self.benchmark.ws_rpc_url {
let parsed: Url = ws_url
.parse()
.wrap_err_with(|| format!("Failed to parse WebSocket RPC URL: {ws_url}"))?;
info!(target: "reth-bench", ws_url = %parsed, "Using provided WebSocket RPC URL");
Ok(parsed)
} else {
let derived = engine_url_to_ws_url(&self.benchmark.engine_rpc_url)?;
debug!(
target: "reth-bench",
engine_url = %self.benchmark.engine_rpc_url,
%derived,
"Derived WebSocket RPC URL from engine RPC URL"
);
Ok(derived)
}
}
/// Establishes a websocket connection and subscribes to `reth_subscribePersistedBlock`.
async fn setup_persistence_subscription(&self) -> eyre::Result<PersistenceSubscription> {
let ws_url = self.derive_ws_rpc_url()?;
info!("Connecting to WebSocket at {} for persistence subscription", ws_url);
let ws_connect = WsConnect::new(ws_url.to_string());
let client = RpcClient::connect_pubsub(ws_connect)
.await
.wrap_err("Failed to connect to WebSocket RPC endpoint")?;
let provider: RootProvider<Ethereum> = RootProvider::new(client);
let subscription = provider
.subscribe_to::<BlockNumHash>("reth_subscribePersistedBlock")
.await
.wrap_err("Failed to subscribe to persistence notifications")?;
info!("Subscribed to persistence notifications");
Ok(PersistenceSubscription::new(provider, subscription.into_stream()))
}
}
/// Converts an engine API URL to the default RPC websocket URL.
///
/// Transformations:
/// - `http` → `ws`
/// - `https` → `wss`
/// - `ws` / `wss` keep their scheme
/// - Port is always set to `8546`, reth's default RPC websocket port.
///
/// This is used when we only know the engine API URL (typically `:8551`) but
/// need to connect to the node's WS RPC endpoint for persistence events.
fn engine_url_to_ws_url(engine_url: &str) -> eyre::Result<Url> {
let url: Url = engine_url
.parse()
.wrap_err_with(|| format!("Failed to parse engine RPC URL: {engine_url}"))?;
let mut ws_url = url.clone();
match ws_url.scheme() {
"http" => ws_url
.set_scheme("ws")
.map_err(|_| eyre::eyre!("Failed to set WS scheme for URL: {url}"))?,
"https" => ws_url
.set_scheme("wss")
.map_err(|_| eyre::eyre!("Failed to set WSS scheme for URL: {url}"))?,
"ws" | "wss" => {}
scheme => {
return Err(eyre::eyre!(
"Unsupported URL scheme '{scheme}' for URL: {url}. Expected http, https, ws, or wss."
))
}
}
ws_url.set_port(Some(8546)).map_err(|_| eyre::eyre!("Failed to set port for URL: {url}"))?;
Ok(ws_url)
}
/// Waits until the persistence subscription reports that `target` has been persisted.
///
/// Consumes subscription events until `last_persisted >= target`, or returns an error if:
/// - the subscription stream ends unexpectedly, or
/// - `timeout` elapses before `target` is observed.
async fn wait_for_persistence(
stream: &mut SubscriptionStream<BlockNumHash>,
target: u64,
last_persisted: &mut u64,
timeout: Duration,
) -> eyre::Result<()> {
tokio::time::timeout(timeout, async {
while *last_persisted < target {
match stream.next().await {
Some(persisted) => {
*last_persisted = persisted.number;
debug!(
target: "reth-bench",
persisted_block = ?last_persisted,
"Received persistence notification"
);
}
None => {
return Err(eyre::eyre!("Persistence subscription closed unexpectedly"));
}
}
}
Ok(())
})
.await
.map_err(|_| {
eyre::eyre!(
"Persistence timeout: target block {} not persisted within {:?}. Last persisted: {}",
target,
timeout,
last_persisted
)
})?
}
/// Wrapper that keeps both the subscription stream and the underlying provider alive.
/// The provider must be kept alive for the subscription to continue receiving events.
struct PersistenceSubscription {
_provider: RootProvider<Ethereum>,
stream: SubscriptionStream<BlockNumHash>,
}
impl PersistenceSubscription {
const fn new(
provider: RootProvider<Ethereum>,
stream: SubscriptionStream<BlockNumHash>,
) -> Self {
Self { _provider: provider, stream }
}
const fn stream_mut(&mut self) -> &mut SubscriptionStream<BlockNumHash> {
&mut self.stream
}
}
/// Encapsulates the block waiting logic.
///
/// Provides a simple `on_block()` interface that handles both:
/// - Fixed duration waits (when `wait_time` is set)
/// - Persistence-based waits (when `subscription` is set)
///
/// For persistence mode, waits after every `(threshold + 1)` blocks.
struct PersistenceWaiter {
wait_time: Option<Duration>,
subscription: Option<PersistenceSubscription>,
blocks_sent: u64,
last_persisted: u64,
threshold: u64,
timeout: Duration,
}
impl PersistenceWaiter {
const fn with_duration(wait_time: Duration) -> Self {
Self {
wait_time: Some(wait_time),
subscription: None,
blocks_sent: 0,
last_persisted: 0,
threshold: 0,
timeout: Duration::ZERO,
}
}
const fn with_subscription(
subscription: PersistenceSubscription,
threshold: u64,
timeout: Duration,
) -> Self {
Self {
wait_time: None,
subscription: Some(subscription),
blocks_sent: 0,
last_persisted: 0,
threshold,
timeout,
}
}
/// Called once per block. Waits based on the configured mode.
#[allow(clippy::manual_is_multiple_of)]
async fn on_block(&mut self, block_number: u64) -> eyre::Result<()> {
if let Some(wait_time) = self.wait_time {
tokio::time::sleep(wait_time).await;
return Ok(());
}
let Some(ref mut subscription) = self.subscription else {
return Ok(());
};
self.blocks_sent += 1;
if self.blocks_sent % (self.threshold + 1) == 0 {
debug!(
target: "reth-bench",
target_block = ?block_number,
last_persisted = self.last_persisted,
blocks_sent = self.blocks_sent,
"Waiting for persistence"
);
wait_for_persistence(
subscription.stream_mut(),
block_number,
&mut self.last_persisted,
self.timeout,
)
.await?;
debug!(
target: "reth-bench",
persisted = self.last_persisted,
"Persistence caught up"
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_engine_url_to_ws_url() {
// http -> ws, always uses port 8546
let result = engine_url_to_ws_url("http://localhost:8551").unwrap();
assert_eq!(result.as_str(), "ws://localhost:8546/");
// https -> wss
let result = engine_url_to_ws_url("https://localhost:8551").unwrap();
assert_eq!(result.as_str(), "wss://localhost:8546/");
// Custom engine port still maps to 8546
let result = engine_url_to_ws_url("http://localhost:9551").unwrap();
assert_eq!(result.port(), Some(8546));
// Already ws passthrough
let result = engine_url_to_ws_url("ws://localhost:8546").unwrap();
assert_eq!(result.scheme(), "ws");
// Invalid inputs
assert!(engine_url_to_ws_url("ftp://localhost:8551").is_err());
assert!(engine_url_to_ws_url("not a valid url").is_err());
}
#[tokio::test]
async fn test_waiter_with_duration() {
let mut waiter = PersistenceWaiter::with_duration(Duration::from_millis(1));
let start = Instant::now();
waiter.on_block(1).await.unwrap();
waiter.on_block(2).await.unwrap();
waiter.on_block(3).await.unwrap();
// Should have waited ~3ms total
assert!(start.elapsed() >= Duration::from_millis(3));
}
}

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 = [
@@ -115,6 +116,11 @@ jemalloc-prof = [
"reth-cli-util/jemalloc",
"reth-cli-util/jemalloc-prof",
"reth-ethereum-cli/jemalloc-prof",
"reth-node-metrics/jemalloc-prof",
]
jemalloc-symbols = [
"jemalloc-prof",
"reth-ethereum-cli/jemalloc-symbols",
]
jemalloc-unprefixed = [
"reth-cli-util/jemalloc-unprefixed",
@@ -125,6 +131,11 @@ jemalloc-unprefixed = [
tracy-allocator = [
"reth-cli-util/tracy-allocator",
"reth-ethereum-cli/tracy-allocator",
"tracy",
]
tracy = [
"reth-ethereum-cli/tracy",
"reth-node-core/tracy",
]
# Because jemalloc is default and preferred over snmalloc when both features are
@@ -165,6 +176,8 @@ min-trace-logs = [
"reth-node-core/min-trace-logs",
]
edge = ["reth-ethereum-cli/edge"]
[[bin]]
name = "reth"
path = "src/main.rs"

View File

@@ -2,22 +2,46 @@
//!
//! ## Feature Flags
//!
//! ### Default Features
//!
//! - `jemalloc`: Uses [jemallocator](https://github.com/tikv/jemallocator) as the global allocator.
//! This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc)
//! for more info.
//! - `otlp`: Enables [OpenTelemetry](https://opentelemetry.io/) metrics export to a configured OTLP
//! collector endpoint.
//! - `js-tracer`: Enables the `JavaScript` tracer for the `debug_trace` endpoints, allowing custom
//! `JavaScript`-based transaction tracing.
//! - `keccak-cache-global`: Enables global caching for Keccak256 hashes to improve performance.
//! - `asm-keccak`: Replaces the default, pure-Rust implementation of Keccak256 with one implemented
//! in assembly; see [the `keccak-asm` crate](https://github.com/DaniPopes/keccak-asm) for more
//! details and supported targets.
//!
//! ### Allocator Features
//!
//! - `jemalloc-prof`: Enables [jemallocator's](https://github.com/tikv/jemallocator) heap profiling
//! and leak detection functionality. See [jemalloc's opt.prof](https://jemalloc.net/jemalloc.3.html#opt.prof)
//! documentation for usage details. This is **not recommended on Windows**. See [here](https://rust-lang.github.io/rfcs/1974-global-allocators.html#jemalloc)
//! for more info.
//! - `asm-keccak`: replaces the default, pure-Rust implementation of Keccak256 with one implemented
//! in assembly; see [the `keccak-asm` crate](https://github.com/DaniPopes/keccak-asm) for more
//! details and supported targets
//! documentation for usage details. This is **not recommended on Windows**.
//! - `jemalloc-symbols`: Enables jemalloc symbols for profiling. Includes `jemalloc-prof`.
//! - `jemalloc-unprefixed`: Uses unprefixed jemalloc symbols.
//! - `tracy-allocator`: Enables [Tracy](https://github.com/wolfpld/tracy) profiler allocator
//! integration for memory profiling.
//! - `snmalloc`: Uses [snmalloc](https://github.com/snmalloc/snmalloc) as the global allocator. Use
//! `--no-default-features` when enabling this, as jemalloc takes precedence.
//! - `snmalloc-native`: Uses snmalloc with native CPU optimizations. Use `--no-default-features`
//! when enabling this.
//!
//! ### Log Level Features
//!
//! - `min-error-logs`: Disables all logs below `error` level.
//! - `min-warn-logs`: Disables all logs below `warn` level.
//! - `min-info-logs`: Disables all logs below `info` level. This can speed up the node, since fewer
//! calls to the logging component are made.
//! - `min-debug-logs`: Disables all logs below `debug` level.
//! - `min-trace-logs`: Disables all logs below `trace` level.
//!
//! ### Development Features
//!
//! - `dev`: Enables development mode features, including test vector generation commands.
#![doc(
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
@@ -170,7 +194,7 @@ pub mod rpc {
pub use reth_rpc::eth::*;
}
/// Re-exported from `reth_rpc::rpc`.
/// Re-exported from `reth_rpc_server_types::result`.
pub mod result {
pub use reth_rpc_server_types::result::*;
}

View File

@@ -3,6 +3,10 @@
#[global_allocator]
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
#[cfg(all(feature = "jemalloc-prof", unix))]
#[unsafe(export_name = "_rjem_malloc_conf")]
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
use clap::Parser;
use reth::{args::RessArgs, cli::Cli, ress::install_ress_subprotocol};
use reth_ethereum_cli::chainspec::EthereumChainSpecParser;

View File

@@ -33,6 +33,7 @@ where
) -> Self {
let (finalized_block, _) = watch::channel(finalized);
let (safe_block, _) = watch::channel(safe);
let (persisted_block, _) = watch::channel(None);
Self {
inner: Arc::new(ChainInfoInner {
@@ -42,6 +43,7 @@ where
canonical_head: RwLock::new(head),
safe_block,
finalized_block,
persisted_block,
}),
}
}
@@ -97,6 +99,11 @@ where
self.inner.finalized_block.borrow().as_ref().map(SealedHeader::num_hash)
}
/// Returns the `BlockNumHash` of the persisted block.
pub fn get_persisted_num_hash(&self) -> Option<BlockNumHash> {
*self.inner.persisted_block.borrow()
}
/// Sets the canonical head of the chain.
pub fn set_canonical_head(&self, header: SealedHeader<N::BlockHeader>) {
let number = header.number();
@@ -130,6 +137,18 @@ where
});
}
/// Sets the persisted block of the chain.
pub fn set_persisted(&self, num_hash: BlockNumHash) {
self.inner.persisted_block.send_if_modified(|current| {
if current.map(|b| b.hash) != Some(num_hash.hash) {
let _ = current.replace(num_hash);
return true
}
false
});
}
/// Subscribe to the finalized block.
pub fn subscribe_finalized_block(
&self,
@@ -141,6 +160,11 @@ where
pub fn subscribe_safe_block(&self) -> watch::Receiver<Option<SealedHeader<N::BlockHeader>>> {
self.inner.safe_block.subscribe()
}
/// Subscribe to the persisted block.
pub fn subscribe_persisted_block(&self) -> watch::Receiver<Option<BlockNumHash>> {
self.inner.persisted_block.subscribe()
}
}
/// Container type for all chain info fields
@@ -159,11 +183,14 @@ struct ChainInfoInner<N: NodePrimitives = reth_ethereum_primitives::EthPrimitive
safe_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
/// The block that the beacon node considers finalized.
finalized_block: watch::Sender<Option<SealedHeader<N::BlockHeader>>>,
/// The last block that was persisted to disk.
persisted_block: watch::Sender<Option<BlockNumHash>>,
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::B256;
use reth_ethereum_primitives::EthPrimitives;
use reth_testing_utils::{generators, generators::random_header};
@@ -338,4 +365,28 @@ mod tests {
// Assert that the BlockNumHash returned matches the safe header
assert_eq!(tracker.get_safe_num_hash(), Some(safe_header.num_hash()));
}
#[test]
fn test_set_persisted() {
let mut rng = generators::rng();
let header = random_header(&mut rng, 10, None);
let tracker: ChainInfoTracker<EthPrimitives> = ChainInfoTracker::new(header, None, None);
// Initial state: persisted block should be None
assert!(tracker.get_persisted_num_hash().is_none());
// Set a persisted block
let num_hash1 = BlockNumHash::new(10, B256::random());
tracker.set_persisted(num_hash1);
assert_eq!(tracker.get_persisted_num_hash(), Some(num_hash1));
// Setting the same block again should not change anything
tracker.set_persisted(num_hash1);
assert_eq!(tracker.get_persisted_num_hash(), Some(num_hash1));
// Set a different block
let num_hash2 = BlockNumHash::new(20, B256::random());
tracker.set_persisted(num_hash2);
assert_eq!(tracker.get_persisted_num_hash(), Some(num_hash2));
}
}

View File

@@ -37,12 +37,19 @@ 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,
/// not just this block's changes. Child blocks reuse the parent's overlay in O(1) by
/// cloning the Arc-wrapped data.
///
/// The `anchor_hash` is metadata indicating which persisted base state this overlay
/// sits on top of. It is CRITICAL for overlay reuse decisions: an overlay built on top
/// of Anchor A cannot be reused for a block anchored to Anchor B, as it would result
/// in an incorrect 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.
pub trie_input: Arc<TrieInputSorted>,
}
@@ -62,7 +69,8 @@ static DEFERRED_TRIE_METRICS: LazyLock<DeferredTrieMetrics> =
/// Internal state for deferred trie data.
enum DeferredState {
/// Data is not yet available; raw inputs stored for fallback computation.
Pending(PendingInputs),
/// Wrapped in `Option` to allow taking ownership during computation.
Pending(Option<PendingInputs>),
/// Data has been computed and is ready.
Ready(ComputedTrieData),
}
@@ -112,12 +120,12 @@ impl DeferredTrieData {
ancestors: Vec<Self>,
) -> Self {
Self {
state: Arc::new(Mutex::new(DeferredState::Pending(PendingInputs {
state: Arc::new(Mutex::new(DeferredState::Pending(Some(PendingInputs {
hashed_state,
trie_updates,
anchor_hash,
ancestors,
}))),
})))),
}
}
@@ -138,8 +146,9 @@ 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. Reuse parent's cached overlay if available (O(1) - the common case)
/// 3. Otherwise, rebuild overlay from ancestors (rare fallback)
/// 4. Extend the overlay with this block's sorted data
///
/// Used by both the async background task and the synchronous fallback path.
///
@@ -147,49 +156,103 @@ 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,
hashed_state: Arc<HashedPostState>,
trie_updates: Arc<TrieUpdates>,
anchor_hash: B256,
ancestors: &[Self],
) -> 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_hashed_state = match Arc::try_unwrap(hashed_state) {
Ok(state) => state.into_sorted(),
Err(arc) => arc.clone_into_sorted(),
};
let sorted_trie_updates = match Arc::try_unwrap(trie_updates) {
Ok(updates) => updates.into_sorted(),
Err(arc) => arc.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());
}
}
// Reuse parent's overlay if available and anchors match.
// We can only reuse the parent's overlay if it was built on top of the same
// persisted anchor. If the anchor has changed (e.g., due to persistence),
// the parent's overlay is relative to an old state and cannot be used.
let overlay = if let Some(parent) = ancestors.last() {
let parent_data = parent.wait_cloned();
// Extend overlay with current block's sorted data
{
let state_mut = Arc::make_mut(&mut overlay.state);
state_mut.extend_ref(sorted_hashed_state.as_ref());
}
{
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
nodes_mut.extend_ref(sorted_trie_updates.as_ref());
}
match &parent_data.anchored_trie_input {
// Case 1: Parent has cached overlay AND anchors match.
Some(AnchoredTrieInput { anchor_hash: parent_anchor, trie_input })
if *parent_anchor == anchor_hash =>
{
// O(1): Reuse parent's overlay, extend with current block's data.
let mut overlay = TrieInputSorted::new(
Arc::clone(&trie_input.nodes),
Arc::clone(&trie_input.state),
Default::default(), // prefix_sets are per-block, not cumulative
);
// Only trigger COW clone if there's actually data to add.
if !sorted_hashed_state.is_empty() {
Arc::make_mut(&mut overlay.state).extend_ref(&sorted_hashed_state);
}
if !sorted_trie_updates.is_empty() {
Arc::make_mut(&mut overlay.nodes).extend_ref(&sorted_trie_updates);
}
overlay
}
// Case 2: Parent exists but anchor mismatch or no cached overlay.
// We must rebuild from the ancestors list (which only contains unpersisted blocks).
_ => Self::merge_ancestors_into_overlay(
ancestors,
&sorted_hashed_state,
&sorted_trie_updates,
),
}
} else {
// Case 3: No in-memory ancestors (first block after persisted anchor).
// Build overlay with just this block's data.
Self::merge_ancestors_into_overlay(&[], &sorted_hashed_state, &sorted_trie_updates)
};
ComputedTrieData::with_trie_input(
sorted_hashed_state,
sorted_trie_updates,
Arc::new(sorted_hashed_state),
Arc::new(sorted_trie_updates),
anchor_hash,
Arc::new(overlay),
)
}
/// Merge all ancestors and current block's data into a single overlay.
///
/// This is a rare fallback path, only used when no ancestor has a cached
/// `anchored_trie_input` (e.g., blocks created via alternative constructors).
/// In normal operation, the parent always has a cached overlay and this
/// function is never called.
///
/// Iterates ancestors oldest -> newest, then extends with current block's data,
/// so later state takes precedence.
fn merge_ancestors_into_overlay(
ancestors: &[Self],
sorted_hashed_state: &HashedPostStateSorted,
sorted_trie_updates: &TrieUpdatesSorted,
) -> TrieInputSorted {
let mut overlay = TrieInputSorted::default();
let state_mut = Arc::make_mut(&mut overlay.state);
let nodes_mut = Arc::make_mut(&mut overlay.nodes);
for ancestor in ancestors {
let ancestor_data = ancestor.wait_cloned();
state_mut.extend_ref(ancestor_data.hashed_state.as_ref());
nodes_mut.extend_ref(ancestor_data.trie_updates.as_ref());
}
// Extend with current block's sorted data last (takes precedence)
state_mut.extend_ref(sorted_hashed_state);
nodes_mut.extend_ref(sorted_trie_updates);
overlay
}
/// Returns trie data, computing synchronously if the async task hasn't completed.
///
/// - If the async task has completed (`Ready`), returns the cached result.
@@ -204,7 +267,7 @@ impl DeferredTrieData {
#[instrument(level = "debug", target = "engine::tree::deferred_trie", skip_all)]
pub fn wait_cloned(&self) -> ComputedTrieData {
let mut state = self.state.lock();
match &*state {
match &mut *state {
// If the deferred trie data is ready, return the cached result.
DeferredState::Ready(bundle) => {
DEFERRED_TRIE_METRICS.deferred_trie_async_ready.increment(1);
@@ -212,11 +275,14 @@ impl DeferredTrieData {
}
// If the deferred trie data is pending, compute the trie data synchronously and return
// the result. This is the fallback path if the async task hasn't completed.
DeferredState::Pending(inputs) => {
DeferredState::Pending(maybe_inputs) => {
DEFERRED_TRIE_METRICS.deferred_trie_sync_fallback.increment(1);
let inputs = maybe_inputs.take().expect("inputs must be present in Pending state");
let computed = Self::sort_and_build_trie_input(
&inputs.hashed_state,
&inputs.trie_updates,
inputs.hashed_state,
inputs.trie_updates,
inputs.anchor_hash,
&inputs.ancestors,
);
@@ -441,4 +507,365 @@ 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 parent's overlay is reused regardless of anchor.
#[test]
fn reuses_parent_overlay() {
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 - should reuse parent's overlay
let child = DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
anchor,
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 parent's overlay is NOT reused when anchor changes (after persist).
/// The overlay data is dependent on the anchor, so it must be rebuilt from the
/// remaining ancestors.
#[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)
// Should NOT reuse parent's overlay because anchor changed
let child = DeferredTrieData::pending(
Arc::new(HashedPostState::default()),
Arc::new(TrieUpdates::default()),
new_anchor,
vec![parent],
);
let result = child.wait_cloned();
// Verify result uses new anchor
let overlay = result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.anchor_hash, new_anchor);
// Crucially, since we provided `parent` in ancestors but it has a different anchor,
// the code falls back to `merge_ancestors_into_overlay`.
// `merge_ancestors_into_overlay` reads `parent.hashed_state` (which has the account).
// So the account IS present, but it was obtained via REBUILD, not REUSE.
// We can check `DEFERRED_TRIE_METRICS` if we want to be sure, but functionally:
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);
}
/// Verifies that a multi-ancestor overlay is rebuilt when anchor changes.
/// This simulates the "persist prefix then keep building" scenario where:
/// 1. A chain of blocks is built with anchor A
/// 2. Some blocks are persisted, changing anchor to B
/// 3. New blocks must rebuild the overlay from the remaining ancestors
#[test]
fn multi_ancestor_overlay_rebuilt_after_anchor_change() {
let old_anchor = B256::with_last_byte(1);
let new_anchor = B256::with_last_byte(2);
let key1 = B256::with_last_byte(1);
let key2 = B256::with_last_byte(2);
let key3 = B256::with_last_byte(3);
let key4 = B256::with_last_byte(4);
// Build a chain of 3 blocks with old_anchor
let block1 = ready_block_with_state(
old_anchor,
vec![(key1, Some(Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None }))],
);
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()),
old_anchor,
vec![block1.clone()],
);
let block2_ready = DeferredTrieData::ready(block2.wait_cloned());
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()),
old_anchor,
vec![block1.clone(), block2_ready.clone()],
);
let block3_ready = DeferredTrieData::ready(block3.wait_cloned());
// Verify block3's overlay has all 3 accounts with old_anchor
let block3_overlay = block3_ready.wait_cloned().anchored_trie_input.unwrap();
assert_eq!(block3_overlay.anchor_hash, old_anchor);
assert_eq!(block3_overlay.trie_input.state.accounts.len(), 3);
// Now simulate persist: create block4 with NEW anchor but same ancestors.
// To verify correct rebuilding, we must provide ALL unpersisted ancestors.
// If we only provided block3, the rebuild would only see block3's state.
// We pass block1, block2, block3 to simulate that they are all still in memory
// but the anchor check forces a rebuild (e.g. artificial anchor change).
let block4_hashed = HashedPostState::default().with_accounts([(
key4,
Some(Account { nonce: 4, balance: U256::ZERO, bytecode_hash: None }),
)]);
let block4 = DeferredTrieData::pending(
Arc::new(block4_hashed),
Arc::new(TrieUpdates::default()),
new_anchor, // Different anchor - simulates post-persist
vec![block1, block2_ready, block3_ready],
);
let result = block4.wait_cloned();
// Verify:
// 1. New anchor is used in result
assert_eq!(result.anchor_hash(), Some(new_anchor));
// 2. All 4 accounts are in the overlay (rebuilt from ancestors + extended)
let overlay = result.anchored_trie_input.as_ref().unwrap();
assert_eq!(overlay.trie_input.state.accounts.len(), 4);
// 3. All accounts have correct values
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));
assert!(accounts.iter().any(|(k, a)| *k == key4 && a.unwrap().nonce == 4));
}
}

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.
@@ -311,6 +317,7 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
/// This will update the links between blocks and remove all blocks that are [..
/// `persisted_height`].
pub fn remove_persisted_blocks(&self, persisted_num_hash: BlockNumHash) {
self.set_persisted(persisted_num_hash);
// if the persisted hash is not in the canonical in memory state, do nothing, because it
// means canonical blocks were not actually persisted.
//
@@ -438,6 +445,11 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
self.inner.chain_info_tracker.set_finalized(header);
}
/// Persisted block setter.
pub fn set_persisted(&self, num_hash: BlockNumHash) {
self.inner.chain_info_tracker.set_persisted(num_hash);
}
/// Canonical head getter.
pub fn get_canonical_head(&self) -> SealedHeader<N::BlockHeader> {
self.inner.chain_info_tracker.get_canonical_head()
@@ -453,6 +465,11 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
self.inner.chain_info_tracker.get_safe_header()
}
/// Persisted block `BlockNumHash` getter.
pub fn get_persisted_num_hash(&self) -> Option<BlockNumHash> {
self.inner.chain_info_tracker.get_persisted_num_hash()
}
/// Returns the `SealedHeader` corresponding to the pending state.
pub fn pending_sealed_header(&self) -> Option<SealedHeader<N::BlockHeader>> {
self.pending_state().map(|h| h.block_ref().recovered_block().clone_sealed_header())
@@ -505,6 +522,11 @@ impl<N: NodePrimitives> CanonicalInMemoryState<N> {
self.inner.chain_info_tracker.subscribe_finalized_block()
}
/// Subscribe to new persisted block events.
pub fn subscribe_persisted_block(&self) -> watch::Receiver<Option<BlockNumHash>> {
self.inner.chain_info_tracker.subscribe_persisted_block()
}
/// Attempts to send a new [`CanonStateNotification`] to all active Receiver handles.
pub fn notify_canon_state(&self, event: CanonStateNotification<N>) {
self.inner.canon_state_notification_sender.send(event).ok();
@@ -664,22 +686,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 +704,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());
}
@@ -927,6 +946,8 @@ impl<N: NodePrimitives<SignedTx: SignedTransaction>> NewCanonicalChain<N> {
chain.append_block(
exec.recovered_block().clone(),
exec.execution_outcome().clone(),
exec.trie_updates(),
exec.hashed_state(),
);
chain
}));
@@ -937,6 +958,8 @@ impl<N: NodePrimitives<SignedTx: SignedTransaction>> NewCanonicalChain<N> {
chain.append_block(
exec.recovered_block().clone(),
exec.execution_outcome().clone(),
exec.trie_updates(),
exec.hashed_state(),
);
chain
}));
@@ -944,6 +967,8 @@ impl<N: NodePrimitives<SignedTx: SignedTransaction>> NewCanonicalChain<N> {
chain.append_block(
exec.recovered_block().clone(),
exec.execution_outcome().clone(),
exec.trie_updates(),
exec.hashed_state(),
);
chain
}));
@@ -1453,19 +1478,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 +1500,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);
@@ -1529,13 +1552,24 @@ mod tests {
// Test commit notification
let chain_commit = NewCanonicalChain::Commit { new: vec![block0.clone(), block1.clone()] };
// Build expected trie updates map
let mut expected_trie_updates = BTreeMap::new();
expected_trie_updates.insert(0, block0.trie_updates());
expected_trie_updates.insert(1, block1.trie_updates());
// Build expected hashed state map
let mut expected_hashed_state = BTreeMap::new();
expected_hashed_state.insert(0, block0.hashed_state());
expected_hashed_state.insert(1, block1.hashed_state());
assert_eq!(
chain_commit.to_chain_notification(),
CanonStateNotification::Commit {
new: Arc::new(Chain::new(
vec![block0.recovered_block().clone(), block1.recovered_block().clone()],
sample_execution_outcome.clone(),
None
expected_trie_updates,
expected_hashed_state
))
}
);
@@ -1546,18 +1580,40 @@ mod tests {
old: vec![block1.clone(), block2.clone()],
};
// Build expected trie updates for old chain
let mut old_trie_updates = BTreeMap::new();
old_trie_updates.insert(1, block1.trie_updates());
old_trie_updates.insert(2, block2.trie_updates());
// Build expected trie updates for new chain
let mut new_trie_updates = BTreeMap::new();
new_trie_updates.insert(1, block1a.trie_updates());
new_trie_updates.insert(2, block2a.trie_updates());
// Build expected hashed state for old chain
let mut old_hashed_state = BTreeMap::new();
old_hashed_state.insert(1, block1.hashed_state());
old_hashed_state.insert(2, block2.hashed_state());
// Build expected hashed state for new chain
let mut new_hashed_state = BTreeMap::new();
new_hashed_state.insert(1, block1a.hashed_state());
new_hashed_state.insert(2, block2a.hashed_state());
assert_eq!(
chain_reorg.to_chain_notification(),
CanonStateNotification::Reorg {
old: Arc::new(Chain::new(
vec![block1.recovered_block().clone(), block2.recovered_block().clone()],
sample_execution_outcome.clone(),
None
old_trie_updates,
old_hashed_state
)),
new: Arc::new(Chain::new(
vec![block1a.recovered_block().clone(), block2a.recovered_block().clone()],
sample_execution_outcome,
None
new_trie_updates,
new_hashed_state
))
}
);

View File

@@ -23,7 +23,8 @@ mod notifications;
pub use notifications::{
CanonStateNotification, CanonStateNotificationSender, CanonStateNotificationStream,
CanonStateNotifications, CanonStateSubscriptions, ForkChoiceNotifications, ForkChoiceStream,
ForkChoiceSubscriptions,
ForkChoiceSubscriptions, PersistedBlockNotifications, PersistedBlockSubscriptions,
WatchValueStream,
};
mod memory_overlay;

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

@@ -2,7 +2,7 @@
use crate::{
CanonStateNotifications, CanonStateSubscriptions, ForkChoiceNotifications,
ForkChoiceSubscriptions,
ForkChoiceSubscriptions, PersistedBlockNotifications, PersistedBlockSubscriptions,
};
use reth_primitives_traits::NodePrimitives;
use reth_storage_api::noop::NoopProvider;
@@ -27,3 +27,10 @@ impl<C: Send + Sync, N: NodePrimitives> ForkChoiceSubscriptions for NoopProvider
ForkChoiceNotifications(rx)
}
}
impl<C: Send + Sync, N: NodePrimitives> PersistedBlockSubscriptions for NoopProvider<C, N> {
fn subscribe_persisted_block(&self) -> PersistedBlockNotifications {
let (_, rx) = watch::channel(None);
PersistedBlockNotifications(rx)
}
}

View File

@@ -1,6 +1,6 @@
//! Canonical chain state notification trait and types.
use alloy_eips::eip2718::Encodable2718;
use alloy_eips::{eip2718::Encodable2718, BlockNumHash};
use derive_more::{Deref, DerefMut};
use reth_execution_types::{BlockReceipts, Chain};
use reth_primitives_traits::{NodePrimitives, RecoveredBlock, SealedHeader};
@@ -205,22 +205,22 @@ pub trait ForkChoiceSubscriptions: Send + Sync {
}
}
/// A stream for fork choice watch channels (pending, safe or finalized watchers)
/// A stream that yields values from a `watch::Receiver<Option<T>>`, filtering out `None` values.
#[derive(Debug)]
#[pin_project::pin_project]
pub struct ForkChoiceStream<T> {
pub struct WatchValueStream<T> {
#[pin]
st: WatchStream<Option<T>>,
}
impl<T: Clone + Sync + Send + 'static> ForkChoiceStream<T> {
/// Creates a new `ForkChoiceStream`
impl<T: Clone + Sync + Send + 'static> WatchValueStream<T> {
/// Creates a new [`WatchValueStream`]
pub fn new(rx: watch::Receiver<Option<T>>) -> Self {
Self { st: WatchStream::from_changes(rx) }
}
}
impl<T: Clone + Sync + Send + 'static> Stream for ForkChoiceStream<T> {
impl<T: Clone + Sync + Send + 'static> Stream for WatchValueStream<T> {
type Item = T;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
@@ -234,6 +234,24 @@ impl<T: Clone + Sync + Send + 'static> Stream for ForkChoiceStream<T> {
}
}
/// Alias for [`WatchValueStream`] for fork choice watch channels.
pub type ForkChoiceStream<T> = WatchValueStream<T>;
/// Wrapper around a watch receiver that receives persisted block notifications.
#[derive(Debug, Deref, DerefMut)]
pub struct PersistedBlockNotifications(pub watch::Receiver<Option<BlockNumHash>>);
/// A trait that allows subscribing to persisted block events.
pub trait PersistedBlockSubscriptions: Send + Sync {
/// Get notified when a new block is persisted to disk.
fn subscribe_persisted_block(&self) -> PersistedBlockNotifications;
/// Convenience method to get a stream of the persisted blocks.
fn persisted_block_stream(&self) -> WatchValueStream<BlockNumHash> {
WatchValueStream::new(self.subscribe_persisted_block().0)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -242,6 +260,7 @@ mod tests {
use reth_ethereum_primitives::{Receipt, TransactionSigned, TxType};
use reth_execution_types::ExecutionOutcome;
use reth_primitives_traits::SealedBlock;
use std::collections::BTreeMap;
#[test]
fn test_commit_notification() {
@@ -260,7 +279,8 @@ mod tests {
let chain: Arc<Chain> = Arc::new(Chain::new(
vec![block1.clone(), block2.clone()],
ExecutionOutcome::default(),
None,
BTreeMap::new(),
BTreeMap::new(),
));
// Create a commit notification
@@ -295,12 +315,17 @@ mod tests {
block3.set_block_number(3);
block3.set_hash(block3_hash);
let old_chain: Arc<Chain> =
Arc::new(Chain::new(vec![block1.clone()], ExecutionOutcome::default(), None));
let old_chain: Arc<Chain> = Arc::new(Chain::new(
vec![block1.clone()],
ExecutionOutcome::default(),
BTreeMap::new(),
BTreeMap::new(),
));
let new_chain = Arc::new(Chain::new(
vec![block2.clone(), block3.clone()],
ExecutionOutcome::default(),
None,
BTreeMap::new(),
BTreeMap::new(),
));
// Create a reorg notification
@@ -362,8 +387,12 @@ mod tests {
let execution_outcome = ExecutionOutcome { receipts, ..Default::default() };
// Create a new chain segment with `block1` and `block2` and the execution outcome.
let new_chain: Arc<Chain> =
Arc::new(Chain::new(vec![block1.clone(), block2.clone()], execution_outcome, None));
let new_chain: Arc<Chain> = Arc::new(Chain::new(
vec![block1.clone(), block2.clone()],
execution_outcome,
BTreeMap::new(),
BTreeMap::new(),
));
// Create a commit notification containing the new chain segment.
let notification = CanonStateNotification::Commit { new: new_chain };
@@ -420,8 +449,12 @@ mod tests {
ExecutionOutcome { receipts: old_receipts, ..Default::default() };
// Create an old chain segment to be reverted, containing `old_block1`.
let old_chain: Arc<Chain> =
Arc::new(Chain::new(vec![old_block1.clone()], old_execution_outcome, None));
let old_chain: Arc<Chain> = Arc::new(Chain::new(
vec![old_block1.clone()],
old_execution_outcome,
BTreeMap::new(),
BTreeMap::new(),
));
// Define block2 for the new chain segment, which will be committed.
let mut body = BlockBody::<TransactionSigned>::default();
@@ -449,7 +482,12 @@ mod tests {
ExecutionOutcome { receipts: new_receipts, ..Default::default() };
// Create a new chain segment to be committed, containing `new_block1`.
let new_chain = Arc::new(Chain::new(vec![new_block1.clone()], new_execution_outcome, None));
let new_chain = Arc::new(Chain::new(
vec![new_block1.clone()],
new_execution_outcome,
BTreeMap::new(),
BTreeMap::new(),
));
// Create a reorg notification with both reverted (old) and committed (new) chain segments.
let notification = CanonStateNotification::Reorg { old: old_chain, new: new_chain };

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

@@ -129,3 +129,5 @@ arbitrary = [
"reth-primitives-traits/arbitrary",
"reth-ethereum-primitives/arbitrary",
]
edge = ["reth-db-common/edge", "reth-stages/rocksdb"]

View File

@@ -107,13 +107,13 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
let (db, sfp) = match access {
AccessRights::RW => (
Arc::new(init_db(db_path, self.db.database_args())?),
StaticFileProviderBuilder::read_write(sf_path)?
StaticFileProviderBuilder::read_write(sf_path)
.with_genesis_block_number(genesis_block_number)
.build()?,
),
AccessRights::RO | AccessRights::RoInconsistent => {
(Arc::new(open_db_read_only(&db_path, self.db.database_args())?), {
let provider = StaticFileProviderBuilder::read_only(sf_path)?
let provider = StaticFileProviderBuilder::read_only(sf_path)
.with_genesis_block_number(genesis_block_number)
.build()?;
provider.watch_directory();

View File

@@ -2,8 +2,8 @@ use alloy_primitives::{hex, BlockHash};
use clap::Parser;
use reth_db::{
static_file::{
ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask, ReceiptMask, TransactionMask,
TransactionSenderMask,
AccountChangesetMask, ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask,
ReceiptMask, TransactionMask, TransactionSenderMask,
},
RawDupSort,
};
@@ -19,7 +19,7 @@ use reth_db_common::DbTool;
use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
use reth_node_builder::NodeTypesWithDB;
use reth_primitives_traits::ValueWithSubKey;
use reth_provider::{providers::ProviderNodeTypes, StaticFileProviderFactory};
use reth_provider::{providers::ProviderNodeTypes, ChangeSetReader, StaticFileProviderFactory};
use reth_static_file_types::StaticFileSegment;
use tracing::error;
@@ -64,6 +64,10 @@ enum Subcommand {
#[arg(value_parser = maybe_json_value_parser)]
key: String,
/// The subkey to get content for, for example address in changeset
#[arg(value_parser = maybe_json_value_parser)]
subkey: Option<String>,
/// Output bytes instead of human-readable decoded value
#[arg(long)]
raw: bool,
@@ -77,33 +81,77 @@ impl Command {
Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
}
Subcommand::StaticFile { segment, key, raw } => {
let (key, mask): (u64, _) = match segment {
Subcommand::StaticFile { segment, key, subkey, raw } => {
let (key, subkey, mask): (u64, _, _) = match segment {
StaticFileSegment::Headers => (
table_key::<tables::Headers>(&key)?,
None,
<HeaderWithHashMask<HeaderTy<N>>>::MASK,
),
StaticFileSegment::Transactions => {
(table_key::<tables::Transactions>(&key)?, <TransactionMask<TxTy<N>>>::MASK)
}
StaticFileSegment::Receipts => {
(table_key::<tables::Receipts>(&key)?, <ReceiptMask<ReceiptTy<N>>>::MASK)
}
StaticFileSegment::Transactions => (
table_key::<tables::Transactions>(&key)?,
None,
<TransactionMask<TxTy<N>>>::MASK,
),
StaticFileSegment::Receipts => (
table_key::<tables::Receipts>(&key)?,
None,
<ReceiptMask<ReceiptTy<N>>>::MASK,
),
StaticFileSegment::TransactionSenders => (
table_key::<tables::TransactionSenders>(&key)?,
<TransactionSenderMask>::MASK,
None,
TransactionSenderMask::MASK,
),
StaticFileSegment::AccountChangeSets => {
let subkey =
table_subkey::<tables::AccountChangeSets>(subkey.as_deref()).ok();
(
table_key::<tables::AccountChangeSets>(&key)?,
subkey,
AccountChangesetMask::MASK,
)
}
};
let content = tool
.provider_factory
.static_file_provider()
.get_segment_provider(segment, key)?
.cursor()?
.get(key.into(), mask)
.map(|result| {
result.map(|vec| vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>())
})?;
// handle account changesets differently if a subkey is provided.
if let StaticFileSegment::AccountChangeSets = segment {
let Some(subkey) = subkey else {
// get all changesets for the block
let changesets = tool
.provider_factory
.static_file_provider()
.account_block_changeset(key)?;
println!("{}", serde_json::to_string_pretty(&changesets)?);
return Ok(())
};
let account = tool
.provider_factory
.static_file_provider()
.get_account_before_block(key, subkey)?;
if let Some(account) = account {
println!("{}", serde_json::to_string_pretty(&account)?);
} else {
error!(target: "reth::cli", "No content for the given table key.");
}
return Ok(())
}
let content = tool.provider_factory.static_file_provider().find_static_file(
segment,
|provider| {
let mut cursor = provider.cursor()?;
cursor.get(key.into(), mask).map(|result| {
result.map(|vec| {
vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>()
})
})
},
)?;
match content {
Some(content) => {
@@ -139,6 +187,9 @@ impl Command {
)?;
println!("{}", serde_json::to_string_pretty(&sender)?);
}
StaticFileSegment::AccountChangeSets => {
unreachable!("account changeset static files are special cased before this match")
}
}
}
}

View File

@@ -162,7 +162,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
let access_rights =
if command.dry_run { AccessRights::RO } else { AccessRights::RW };
db_exec!(self.env, tool, N, access_rights, {
command.execute(&tool, ctx.task_executor.clone())?;
command.execute(&tool, ctx.task_executor.clone(), &data_dir)?;
});
}
Subcommands::StaticFileHeader(command) => {

View File

@@ -9,7 +9,10 @@ use reth_db_api::{
transaction::{DbTx, DbTxMut},
};
use reth_db_common::DbTool;
use reth_node_core::version::version_metadata;
use reth_node_core::{
dirs::{ChainPath, DataDirPath},
version::version_metadata,
};
use reth_node_metrics::{
chain::ChainSpecInfo,
hooks::Hooks,
@@ -53,11 +56,13 @@ impl Command {
self,
tool: &DbTool<N>,
task_executor: TaskExecutor,
data_dir: &ChainPath<DataDirPath>,
) -> eyre::Result<()> {
// Set up metrics server if requested
let _metrics_handle = if let Some(listen_addr) = self.metrics {
let chain_name = tool.provider_factory.chain_spec().chain().to_string();
let executor = task_executor.clone();
let pprof_dump_dir = data_dir.pprof_dumps();
let handle = task_executor.spawn_critical("metrics server", async move {
let config = MetricServerConfig::new(
@@ -73,6 +78,7 @@ impl Command {
ChainSpecInfo { name: chain_name },
executor,
Hooks::builder().build(),
pprof_dump_dir,
);
// Spawn the metrics server
@@ -301,8 +307,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

@@ -40,12 +40,17 @@ enum Subcommands {
#[clap(rename_all = "snake_case")]
pub enum SetCommand {
/// Store receipts in static files instead of the database
ReceiptsInStaticFiles {
Receipts {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store transaction senders in static files instead of the database
TransactionSendersInStaticFiles {
TransactionSenders {
#[clap(action(ArgAction::Set))]
value: bool,
},
/// Store account changesets in static files instead of the database
AccountChangesets {
#[clap(action(ArgAction::Set))]
value: bool,
},
@@ -94,11 +99,12 @@ impl Command {
storages_history_in_rocksdb: _,
transaction_hash_numbers_in_rocksdb: _,
account_history_in_rocksdb: _,
account_changesets_in_static_files: _,
} = settings.unwrap_or_else(StorageSettings::legacy);
// Update the setting based on the key
match cmd {
SetCommand::ReceiptsInStaticFiles { value } => {
SetCommand::Receipts { value } => {
if settings.receipts_in_static_files == value {
println!("receipts_in_static_files is already set to {}", value);
return Ok(());
@@ -106,7 +112,7 @@ impl Command {
settings.receipts_in_static_files = value;
println!("Set receipts_in_static_files = {}", value);
}
SetCommand::TransactionSendersInStaticFiles { value } => {
SetCommand::TransactionSenders { value } => {
if settings.transaction_senders_in_static_files == value {
println!("transaction_senders_in_static_files is already set to {}", value);
return Ok(());
@@ -114,6 +120,14 @@ impl Command {
settings.transaction_senders_in_static_files = value;
println!("Set transaction_senders_in_static_files = {}", value);
}
SetCommand::AccountChangesets { value } => {
if settings.account_changesets_in_static_files == value {
println!("account_changesets_in_static_files is already set to {}", value);
return Ok(());
}
settings.account_changesets_in_static_files = value;
println!("Set account_changesets_in_static_files = {}", value);
}
}
// Write updated settings

View File

@@ -69,9 +69,7 @@ pub async fn import_blocks_from_file<N>(
provider_factory: ProviderFactory<N>,
config: &Config,
executor: impl ConfigureEvm<Primitives = N::Primitives> + 'static,
consensus: Arc<
impl FullConsensus<N::Primitives, Error = reth_consensus::ConsensusError> + 'static,
>,
consensus: Arc<impl FullConsensus<N::Primitives> + 'static>,
) -> eyre::Result<ImportResult>
where
N: ProviderNodeTypes,
@@ -198,7 +196,7 @@ pub fn build_import_pipeline_impl<N, C, E>(
) -> eyre::Result<(Pipeline<N>, impl futures::Stream<Item = NodeEvent<N::Primitives>> + use<N, C, E>)>
where
N: ProviderNodeTypes,
C: FullConsensus<N::Primitives, Error = reth_consensus::ConsensusError> + 'static,
C: FullConsensus<N::Primitives> + 'static,
E: ConfigureEvm<Primitives = N::Primitives> + 'static,
{
if !file_client.has_canonical_blocks() {

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(),
)
@@ -99,6 +99,7 @@ where
/// * Headers: It will push an empty block.
/// * Transactions: It will not push any tx, only increments the end block range.
/// * Receipts: It will not push any receipt, only increments the end block range.
/// * TransactionSenders: If the segment exists, increments the end block range.
fn append_dummy_chain<N, F>(
sf_provider: &StaticFileProvider<N>,
target_height: BlockNumber,
@@ -110,8 +111,15 @@ where
{
let (tx, rx) = std::sync::mpsc::channel();
// Spawn jobs for incrementing the block end range of transactions and receipts
for segment in [StaticFileSegment::Transactions, StaticFileSegment::Receipts] {
// Spawn jobs for incrementing the block end range of transactions, receipts, and senders.
for segment in [
StaticFileSegment::Transactions,
StaticFileSegment::Receipts,
StaticFileSegment::TransactionSenders,
] {
if sf_provider.get_highest_static_file_block(segment).is_none() {
continue
}
let tx_clone = tx.clone();
let provider = sf_provider.clone();
std::thread::spawn(move || {
@@ -151,9 +159,15 @@ where
// If, for any reason, rayon crashes this verifies if all segments are at the same
// target_height.
for segment in
[StaticFileSegment::Headers, StaticFileSegment::Receipts, StaticFileSegment::Transactions]
{
for segment in [
StaticFileSegment::Headers,
StaticFileSegment::Receipts,
StaticFileSegment::Transactions,
StaticFileSegment::TransactionSenders,
] {
if sf_provider.get_highest_static_file_block(segment).is_none() {
continue
}
assert_eq!(
sf_provider.latest_writer(segment)?.user_header().block_end(),
Some(target_height),

View File

@@ -87,6 +87,9 @@ impl<C: ChainSpecParser> Command<C> {
.unwrap_or_default();
writer.prune_transaction_senders(to_delete, 0)?;
}
StaticFileSegment::AccountChangeSets => {
writer.prune_account_changesets(highest_block)?;
}
}
}
}

View File

@@ -1,5 +1,5 @@
use super::setup;
use reth_consensus::{noop::NoopConsensus, ConsensusError, FullConsensus};
use reth_consensus::{noop::NoopConsensus, FullConsensus};
use reth_db::DatabaseEnv;
use reth_db_api::{
cursor::DbCursorRO, database::Database, table::TableImporter, tables, transaction::DbTx,
@@ -28,7 +28,7 @@ pub(crate) async fn dump_execution_stage<N, E, C>(
where
N: ProviderNodeTypes<DB = Arc<DatabaseEnv>>,
E: ConfigureEvm<Primitives = N::Primitives> + 'static,
C: FullConsensus<E::Primitives, Error = ConsensusError> + 'static,
C: FullConsensus<E::Primitives> + 'static,
{
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
@@ -169,7 +169,7 @@ fn dry_run<N, E, C>(
where
N: ProviderNodeTypes,
E: ConfigureEvm<Primitives = N::Primitives> + 'static,
C: FullConsensus<E::Primitives, Error = ConsensusError> + 'static,
C: FullConsensus<E::Primitives> + 'static,
{
info!(target: "reth::cli", "Executing stage. [dry-run]");

View File

@@ -4,7 +4,7 @@ use super::setup;
use alloy_primitives::{Address, BlockNumber};
use eyre::Result;
use reth_config::config::EtlConfig;
use reth_consensus::{ConsensusError, FullConsensus};
use reth_consensus::FullConsensus;
use reth_db::DatabaseEnv;
use reth_db_api::{database::Database, models::BlockNumberAddress, table::TableImporter, tables};
use reth_db_common::DbTool;
@@ -31,7 +31,7 @@ pub(crate) async fn dump_merkle_stage<N>(
output_datadir: ChainPath<DataDirPath>,
should_run: bool,
evm_config: impl ConfigureEvm<Primitives = N::Primitives>,
consensus: impl FullConsensus<N::Primitives, Error = ConsensusError> + 'static,
consensus: impl FullConsensus<N::Primitives> + 'static,
) -> Result<()>
where
N: ProviderNodeTypes<DB = Arc<DatabaseEnv>>,
@@ -79,7 +79,7 @@ fn unwind_and_copy<N: ProviderNodeTypes>(
tip_block_number: u64,
output_db: &DatabaseEnv,
evm_config: impl ConfigureEvm<Primitives = N::Primitives>,
consensus: impl FullConsensus<N::Primitives, Error = ConsensusError> + 'static,
consensus: impl FullConsensus<N::Primitives> + 'static,
) -> eyre::Result<()> {
let (from, to) = range;
let provider = db_tool.provider_factory.database_provider_rw()?;

View File

@@ -11,7 +11,6 @@ use reth_cli::chainspec::ChainSpecParser;
use reth_cli_runner::CliContext;
use reth_cli_util::get_secret_key;
use reth_config::config::{HashingConfig, SenderRecoveryConfig, TransactionLookupConfig};
use reth_db_api::database_metrics::DatabaseMetrics;
use reth_downloaders::{
bodies::bodies::BodiesDownloaderBuilder,
headers::reverse_headers::ReverseHeadersDownloaderBuilder,
@@ -19,19 +18,19 @@ use reth_downloaders::{
use reth_exex::ExExManagerHandle;
use reth_network::BlockDownloaderProvider;
use reth_network_p2p::HeadersClient;
use reth_node_builder::common::metrics_hooks;
use reth_node_core::{
args::{NetworkArgs, StageEnum},
version::version_metadata,
};
use reth_node_metrics::{
chain::ChainSpecInfo,
hooks::Hooks,
server::{MetricServer, MetricServerConfig},
version::VersionInfo,
};
use reth_provider::{
ChainSpecProvider, DBProvider, DatabaseProviderFactory, StageCheckpointReader,
StageCheckpointWriter, StaticFileProviderFactory,
StageCheckpointWriter,
};
use reth_stages::{
stages::{
@@ -139,20 +138,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
},
ChainSpecInfo { name: provider_factory.chain_spec().chain().to_string() },
ctx.task_executor,
Hooks::builder()
.with_hook({
let db = provider_factory.db_ref().clone();
move || db.report_metrics()
})
.with_hook({
let sfp = provider_factory.static_file_provider();
move || {
if let Err(error) = sfp.report_metrics() {
error!(%error, "Failed to report metrics from static file provider");
}
}
})
.build(),
metrics_hooks(&provider_factory),
data_dir.pprof_dumps(),
);
MetricServer::new(config).serve().await?;

View File

@@ -18,8 +18,8 @@ use tracing::{debug, error, trace};
///
/// Provides utilities for running a cli command to completion.
#[derive(Debug)]
#[non_exhaustive]
pub struct CliRunner {
config: CliRunnerConfig,
tokio_runtime: tokio::runtime::Runtime,
}
@@ -29,12 +29,18 @@ impl CliRunner {
///
/// The default tokio runtime is multi-threaded, with both I/O and time drivers enabled.
pub fn try_default_runtime() -> Result<Self, std::io::Error> {
Ok(Self { tokio_runtime: tokio_runtime()? })
Ok(Self { config: CliRunnerConfig::default(), tokio_runtime: tokio_runtime()? })
}
/// Create a new [`CliRunner`] from a provided tokio [`Runtime`](tokio::runtime::Runtime).
pub const fn from_runtime(tokio_runtime: tokio::runtime::Runtime) -> Self {
Self { tokio_runtime }
Self { config: CliRunnerConfig::new(), tokio_runtime }
}
/// Sets the [`CliRunnerConfig`] for this runner.
pub const fn with_config(mut self, config: CliRunnerConfig) -> Self {
self.config = config;
self
}
/// Executes an async block on the runtime and blocks until completion.
@@ -74,7 +80,7 @@ impl CliRunner {
// after the command has finished or exit signal was received we shutdown the task
// manager which fires the shutdown signal to all tasks spawned via the task
// executor and awaiting on tasks spawned with graceful shutdown
task_manager.graceful_shutdown_with_timeout(Duration::from_secs(5));
task_manager.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
}
// `drop(tokio_runtime)` would block the current thread until its pools
@@ -128,7 +134,7 @@ impl CliRunner {
error!(target: "reth::cli", "shutting down due to error");
} else {
debug!(target: "reth::cli", "shutting down gracefully");
task_manager.graceful_shutdown_with_timeout(Duration::from_secs(5));
task_manager.graceful_shutdown_with_timeout(self.config.graceful_shutdown_timeout);
}
// Shutdown the runtime on a separate thread
@@ -211,6 +217,38 @@ pub struct CliContext {
pub task_executor: TaskExecutor,
}
/// Default timeout for graceful shutdown of tasks.
const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
/// Configuration for [`CliRunner`].
#[derive(Debug, Clone)]
pub struct CliRunnerConfig {
/// Timeout for graceful shutdown of tasks.
///
/// After the command completes, this is the maximum time to wait for spawned tasks
/// to finish before forcefully terminating them.
pub graceful_shutdown_timeout: Duration,
}
impl Default for CliRunnerConfig {
fn default() -> Self {
Self::new()
}
}
impl CliRunnerConfig {
/// Creates a new config with default values.
pub const fn new() -> Self {
Self { graceful_shutdown_timeout: DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT }
}
/// Sets the graceful shutdown timeout.
pub const fn with_graceful_shutdown_timeout(mut self, timeout: Duration) -> Self {
self.graceful_shutdown_timeout = timeout;
self
}
}
/// Creates a new default tokio multi-thread [Runtime](tokio::runtime::Runtime) with all features
/// enabled
pub fn tokio_runtime() -> Result<tokio::runtime::Runtime, std::io::Error> {

View File

@@ -26,7 +26,8 @@ rand_08.workspace = true
thiserror.workspace = true
serde.workspace = true
tracy-client = { workspace = true, optional = true, features = ["demangle"] }
tracy-client = { workspace = true, optional = true }
reth-tracing = { workspace = true, optional = true }
[dev-dependencies]
rand.workspace = true
@@ -46,7 +47,7 @@ jemalloc-prof = ["jemalloc", "tikv-jemallocator?/profiling"]
jemalloc-unprefixed = ["jemalloc", "tikv-jemallocator?/unprefixed_malloc_on_supported_platforms"]
# Wraps the selected allocator in the tracy profiling allocator
tracy-allocator = ["dep:tracy-client"]
tracy-allocator = ["dep:tracy-client", "dep:reth-tracing"]
snmalloc = ["dep:snmalloc-rs"]

View File

@@ -25,7 +25,6 @@ cfg_if::cfg_if! {
cfg_if::cfg_if! {
if #[cfg(feature = "tracy-allocator")] {
type AllocatorWrapper = tracy_client::ProfiledAllocator<AllocatorInner>;
tracy_client::register_demangler!();
const fn new_allocator_wrapper() -> AllocatorWrapper {
AllocatorWrapper::new(AllocatorInner {}, 100)
}

View File

@@ -8,6 +8,9 @@
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#[cfg(feature = "tracy-allocator")]
use reth_tracing as _;
pub mod allocator;
pub mod cancellation;

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))]
@@ -438,6 +437,8 @@ pub struct BlocksPerFileConfig {
pub receipts: Option<u64>,
/// Number of blocks per file for the transaction senders segment.
pub transaction_senders: Option<u64>,
/// Number of blocks per file for the account changesets segment.
pub account_change_sets: Option<u64>,
}
impl StaticFilesConfig {
@@ -445,8 +446,13 @@ impl StaticFilesConfig {
///
/// Returns an error if any blocks per file value is zero.
pub fn validate(&self) -> eyre::Result<()> {
let BlocksPerFileConfig { headers, transactions, receipts, transaction_senders } =
self.blocks_per_file;
let BlocksPerFileConfig {
headers,
transactions,
receipts,
transaction_senders,
account_change_sets,
} = self.blocks_per_file;
eyre::ensure!(headers != Some(0), "Headers segment blocks per file must be greater than 0");
eyre::ensure!(
transactions != Some(0),
@@ -460,13 +466,22 @@ impl StaticFilesConfig {
transaction_senders != Some(0),
"Transaction senders segment blocks per file must be greater than 0"
);
eyre::ensure!(
account_change_sets != Some(0),
"Account changesets segment blocks per file must be greater than 0"
);
Ok(())
}
/// Converts the blocks per file configuration into a [`HashMap`] per segment.
pub fn as_blocks_per_file_map(&self) -> HashMap<StaticFileSegment, u64> {
let BlocksPerFileConfig { headers, transactions, receipts, transaction_senders } =
self.blocks_per_file;
let BlocksPerFileConfig {
headers,
transactions,
receipts,
transaction_senders,
account_change_sets,
} = self.blocks_per_file;
let mut map = HashMap::new();
// Iterating over all possible segments allows us to do an exhaustive match here,
@@ -477,6 +492,7 @@ impl StaticFilesConfig {
StaticFileSegment::Transactions => transactions,
StaticFileSegment::Receipts => receipts,
StaticFileSegment::TransactionSenders => transaction_senders,
StaticFileSegment::AccountChangeSets => account_change_sets,
};
if let Some(blocks_per_file) = blocks_per_file {
@@ -528,7 +544,7 @@ impl PruneConfig {
/// Returns whether there is any kind of receipt pruning configuration.
pub fn has_receipts_pruning(&self) -> bool {
self.segments.receipts.is_some() || !self.segments.receipts_log_filter.is_empty()
self.segments.has_receipts_pruning()
}
/// Merges values from `other` into `self`.
@@ -1063,18 +1079,6 @@ transaction_lookup = 'full'
receipts = { distance = 16384 }
#";
let _conf: Config = toml::from_str(s).unwrap();
let s = r"#
[prune]
block_interval = 5
[prune.segments]
sender_recovery = { distance = 16384 }
transaction_lookup = 'full'
receipts = 'full'
#";
let err = toml::from_str::<Config>(s).unwrap_err().to_string();
assert!(err.contains("invalid value: string \"full\""), "{}", err);
}
#[test]

View File

@@ -500,13 +500,11 @@ mod tests {
let expected_blob_gas_used = 10 * DATA_GAS_PER_BLOB;
// validate blob, it should fail blob gas used validation
assert_eq!(
validate_block_pre_execution(&block, &chain_spec),
Err(ConsensusError::BlobGasUsedDiff(GotExpected {
got: 1,
expected: expected_blob_gas_used
}))
);
assert!(matches!(
validate_block_pre_execution(&block, &chain_spec).unwrap_err(),
ConsensusError::BlobGasUsedDiff(diff)
if diff.got == 1 && diff.expected == expected_blob_gas_used
));
}
#[test]
@@ -517,10 +515,10 @@ mod tests {
// Test exceeding default - should fail
let header_33 = Header { extra_data: Bytes::from(vec![0; 33]), ..Default::default() };
assert_eq!(
validate_header_extra_data(&header_33, 32),
Err(ConsensusError::ExtraDataExceedsMax { len: 33 })
);
assert!(matches!(
validate_header_extra_data(&header_33, 32).unwrap_err(),
ConsensusError::ExtraDataExceedsMax { len } if len == 33
));
// Test with custom larger limit - should pass
assert!(validate_header_extra_data(&header_33, 64).is_ok());

View File

@@ -11,9 +11,10 @@
extern crate alloc;
use alloc::{boxed::Box, fmt::Debug, string::String, vec::Vec};
use alloc::{boxed::Box, fmt::Debug, string::String, sync::Arc, vec::Vec};
use alloy_consensus::Header;
use alloy_primitives::{BlockHash, BlockNumber, Bloom, B256};
use core::error::Error;
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{
constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MINIMUM_GAS_LIMIT},
@@ -49,15 +50,12 @@ pub trait FullConsensus<N: NodePrimitives>: Consensus<N::Block> {
/// Consensus is a protocol that chooses canonical chain.
#[auto_impl::auto_impl(&, Arc)]
pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
/// The error type related to consensus.
type Error;
/// Ensures that body field values match the header.
fn validate_body_against_header(
&self,
body: &B::Body,
header: &SealedHeader<B::Header>,
) -> Result<(), Self::Error>;
) -> Result<(), ConsensusError>;
/// Validate a block disregarding world state, i.e. things that can be checked before sender
/// recovery and execution.
@@ -69,7 +67,7 @@ pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
/// **This should not be called for the genesis block**.
///
/// Note: validating blocks does not include other validations of the Consensus
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), Self::Error>;
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError>;
}
/// `HeaderValidator` is a protocol that validates headers and their relationships.
@@ -125,7 +123,7 @@ pub trait HeaderValidator<H = Header>: Debug + Send + Sync {
}
/// Consensus Errors
#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
#[derive(Debug, Clone, thiserror::Error)]
pub enum ConsensusError {
/// Error when the gas used in the header exceeds the gas limit.
#[error("block used gas ({gas_used}) is greater than gas limit ({gas_limit})")]
@@ -135,7 +133,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})"
)]
@@ -410,6 +408,9 @@ pub enum ConsensusError {
/// Other, likely an injected L2 error.
#[error("{0}")]
Other(String),
/// Other unspecified error.
#[error(transparent)]
Custom(#[from] Arc<dyn Error + Send + Sync>),
}
impl ConsensusError {
@@ -447,3 +448,34 @@ pub struct TxGasLimitTooHighErr {
/// The maximum allowed gas limit
pub max_allowed: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(thiserror::Error, Debug)]
#[error("Custom L2 consensus error")]
struct CustomL2Error;
#[test]
fn test_custom_error_conversion() {
// Test conversion from custom error to ConsensusError
let custom_err = CustomL2Error;
let arc_err: Arc<dyn Error + Send + Sync> = Arc::new(custom_err);
let consensus_err: ConsensusError = arc_err.into();
// Verify it's the Custom variant
assert!(matches!(consensus_err, ConsensusError::Custom(_)));
}
#[test]
fn test_custom_error_display() {
let custom_err = CustomL2Error;
let arc_err: Arc<dyn Error + Send + Sync> = Arc::new(custom_err);
let consensus_err: ConsensusError = arc_err.into();
// Verify the error message is preserved through transparent attribute
let error_message = format!("{}", consensus_err);
assert_eq!(error_message, "Custom L2 consensus error");
}
}

View File

@@ -55,19 +55,17 @@ impl<H> HeaderValidator<H> for NoopConsensus {
}
impl<B: Block> Consensus<B> for NoopConsensus {
type Error = ConsensusError;
/// Validates body against header (no-op implementation).
fn validate_body_against_header(
&self,
_body: &B::Body,
_header: &SealedHeader<B::Header>,
) -> Result<(), Self::Error> {
) -> Result<(), ConsensusError> {
Ok(())
}
/// Validates block before execution (no-op implementation).
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), Self::Error> {
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), ConsensusError> {
Ok(())
}
}

View File

@@ -61,13 +61,11 @@ impl<N: NodePrimitives> FullConsensus<N> for TestConsensus {
}
impl<B: Block> Consensus<B> for TestConsensus {
type Error = ConsensusError;
fn validate_body_against_header(
&self,
_body: &B::Body,
_header: &SealedHeader<B::Header>,
) -> Result<(), Self::Error> {
) -> Result<(), ConsensusError> {
if self.fail_body_against_header() {
Err(ConsensusError::BaseFeeMissing)
} else {
@@ -75,7 +73,7 @@ impl<B: Block> Consensus<B> for TestConsensus {
}
}
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), Self::Error> {
fn validate_block_pre_execution(&self, _block: &SealedBlock<B>) -> Result<(), ConsensusError> {
if self.fail_validation() {
Err(ConsensusError::BaseFeeMissing)
} else {

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

@@ -113,7 +113,6 @@ pub async fn setup_engine_with_chain_import(
let rocksdb_dir_path = datadir.join("rocksdb");
// Initialize the database using init_db (same as CLI import command)
// Use the same database arguments as the node will use
let db_args = reth_node_core::args::DatabaseArgs::default().database_args();
let db_env = reth_db::init_db(&db_path, db_args)?;
let db = Arc::new(db_env);
@@ -317,7 +316,8 @@ mod tests {
// Import the chain
{
let db_env = reth_db::init_db(&db_path, DatabaseArguments::default()).unwrap();
let db_args = reth_node_core::args::DatabaseArgs::default().database_args();
let db_env = reth_db::init_db(&db_path, db_args).unwrap();
let db = Arc::new(db_env);
let provider_factory: ProviderFactory<
@@ -475,7 +475,8 @@ mod tests {
let datadir = temp_dir.path().join("datadir");
std::fs::create_dir_all(&datadir).unwrap();
let db_path = datadir.join("db");
let db_env = reth_db::init_db(&db_path, DatabaseArguments::default()).unwrap();
let db_args = reth_node_core::args::DatabaseArgs::default().database_args();
let db_env = reth_db::init_db(&db_path, db_args).unwrap();
let db = Arc::new(reth_db::test_utils::TempDatabase::new(db_env, db_path));
// Create static files path

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

@@ -45,7 +45,7 @@ pub const DEFAULT_PREWARM_MAX_CONCURRENCY: usize = 16;
const DEFAULT_BLOCK_BUFFER_LIMIT: u32 = EPOCH_SLOTS as u32 * 2;
const DEFAULT_MAX_INVALID_HEADER_CACHE_LENGTH: u32 = 256;
const DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE: usize = 4;
const DEFAULT_CROSS_BLOCK_CACHE_SIZE: u64 = 4 * 1024 * 1024 * 1024;
const DEFAULT_CROSS_BLOCK_CACHE_SIZE: usize = 4 * 1024 * 1024 * 1024;
/// Determines if the host has enough parallelism to run the payload processor.
///
@@ -100,7 +100,7 @@ pub struct TreeConfig {
/// Whether to enable state provider metrics.
state_provider_metrics: bool,
/// Cross-block cache size in bytes.
cross_block_cache_size: u64,
cross_block_cache_size: usize,
/// Whether the host has enough parallelism to run state root task.
has_enough_parallelism: bool,
/// Whether multiproof task should chunk proof targets.
@@ -135,6 +135,8 @@ pub struct TreeConfig {
storage_worker_count: usize,
/// Number of account proof worker threads.
account_worker_count: usize,
/// Whether to enable V2 storage proofs.
enable_proof_v2: bool,
}
impl Default for TreeConfig {
@@ -163,6 +165,7 @@ impl Default for TreeConfig {
allow_unwind_canonical_header: false,
storage_worker_count: default_storage_worker_count(),
account_worker_count: default_account_worker_count(),
enable_proof_v2: false,
}
}
}
@@ -182,7 +185,7 @@ impl TreeConfig {
disable_prewarming: bool,
disable_parallel_sparse_trie: bool,
state_provider_metrics: bool,
cross_block_cache_size: u64,
cross_block_cache_size: usize,
has_enough_parallelism: bool,
multiproof_chunking_enabled: bool,
multiproof_chunk_size: usize,
@@ -194,6 +197,7 @@ impl TreeConfig {
allow_unwind_canonical_header: bool,
storage_worker_count: usize,
account_worker_count: usize,
enable_proof_v2: bool,
) -> Self {
Self {
persistence_threshold,
@@ -219,6 +223,7 @@ impl TreeConfig {
allow_unwind_canonical_header,
storage_worker_count,
account_worker_count,
enable_proof_v2,
}
}
@@ -295,7 +300,7 @@ impl TreeConfig {
}
/// Returns the cross-block cache size.
pub const fn cross_block_cache_size(&self) -> u64 {
pub const fn cross_block_cache_size(&self) -> usize {
self.cross_block_cache_size
}
@@ -398,7 +403,7 @@ impl TreeConfig {
}
/// Setter for cross block cache size.
pub const fn with_cross_block_cache_size(mut self, cross_block_cache_size: u64) -> Self {
pub const fn with_cross_block_cache_size(mut self, cross_block_cache_size: usize) -> Self {
self.cross_block_cache_size = cross_block_cache_size;
self
}
@@ -500,4 +505,15 @@ impl TreeConfig {
self.account_worker_count = account_worker_count.max(MIN_WORKER_COUNT);
self
}
/// Return whether V2 storage proofs are enabled.
pub const fn enable_proof_v2(&self) -> bool {
self.enable_proof_v2
}
/// Setter for whether to enable V2 storage proofs.
pub const fn with_enable_proof_v2(mut self, enable_proof_v2: bool) -> Self {
self.enable_proof_v2 = enable_proof_v2;
self
}
}

View File

@@ -1,7 +1,7 @@
use futures::{Stream, StreamExt};
use pin_project::pin_project;
use reth_chainspec::EthChainSpec;
use reth_consensus::{ConsensusError, FullConsensus};
use reth_consensus::FullConsensus;
use reth_engine_primitives::{BeaconEngineMessage, ConsensusEngineEvent};
use reth_engine_tree::{
backfill::PipelineSync,
@@ -70,7 +70,7 @@ where
/// Constructor for `EngineService`.
#[expect(clippy::too_many_arguments)]
pub fn new<V, C>(
consensus: Arc<dyn FullConsensus<N::Primitives, Error = ConsensusError>>,
consensus: Arc<dyn FullConsensus<N::Primitives>>,
chain_spec: Arc<N::ChainSpec>,
client: Client,
incoming_requests: EngineMessageStream<N::Payload>,

View File

@@ -29,7 +29,6 @@ reth-provider.workspace = true
reth-prune.workspace = true
reth-revm.workspace = true
reth-stages-api.workspace = true
reth-storage-errors.workspace = true
reth-tasks.workspace = true
reth-trie-parallel.workspace = true
reth-trie-sparse = { workspace = true, features = ["std", "metrics"] }
@@ -52,7 +51,7 @@ revm-primitives.workspace = true
futures.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "sync", "macros"] }
mini-moka = { workspace = true, features = ["sync"] }
fixed-cache.workspace = true
moka = { workspace = true, features = ["sync"] }
smallvec.workspace = true

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

@@ -4,7 +4,7 @@ use crate::{engine::DownloadRequest, metrics::BlockDownloaderMetrics};
use alloy_consensus::BlockHeader;
use alloy_primitives::B256;
use futures::FutureExt;
use reth_consensus::{Consensus, ConsensusError};
use reth_consensus::Consensus;
use reth_network_p2p::{
full_block::{FetchFullBlockFuture, FetchFullBlockRangeFuture, FullBlockClient},
BlockClient,
@@ -81,7 +81,7 @@ where
B: Block,
{
/// Create a new instance
pub fn new(client: Client, consensus: Arc<dyn Consensus<B, Error = ConsensusError>>) -> Self {
pub fn new(client: Client, consensus: Arc<dyn Consensus<B>>) -> Self {
Self {
full_block_client: FullBlockClient::new(client, consensus),
inflight_full_block_requests: Vec::new(),

View File

@@ -6,6 +6,7 @@ use crate::{
download::{BlockDownloader, DownloadAction, DownloadOutcome},
};
use alloy_primitives::B256;
use crossbeam_channel::Sender;
use futures::{Stream, StreamExt};
use reth_chain_state::ExecutedBlock;
use reth_engine_primitives::{BeaconEngineMessage, ConsensusEngineEvent};
@@ -15,7 +16,6 @@ use reth_primitives_traits::{Block, NodePrimitives, SealedBlock};
use std::{
collections::HashSet,
fmt::Display,
sync::mpsc::Sender,
task::{ready, Context, Poll},
};
use tokio::sync::mpsc::UnboundedReceiver;

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,6 +1,6 @@
use crate::metrics::PersistenceMetrics;
use alloy_consensus::BlockHeader;
use alloy_eips::BlockNumHash;
use crossbeam_channel::Sender as CrossbeamSender;
use reth_chain_state::ExecutedBlock;
use reth_errors::ProviderError;
use reth_ethereum_primitives::EthPrimitives;
@@ -16,7 +16,6 @@ use std::{
time::Instant,
};
use thiserror::Error;
use tokio::sync::oneshot;
use tracing::{debug, error};
/// Writes parts of reth's in memory tree state to the database and static files.
@@ -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)
}
}
@@ -186,13 +183,13 @@ pub enum PersistenceAction<N: NodePrimitives = EthPrimitives> {
///
/// First, header, transaction, and receipt-related data should be written to static files.
/// Then the execution history-related data will be written to the database.
SaveBlocks(Vec<ExecutedBlock<N>>, oneshot::Sender<Option<BlockNumHash>>),
SaveBlocks(Vec<ExecutedBlock<N>>, CrossbeamSender<Option<BlockNumHash>>),
/// Removes block data above the given block number from the database.
///
/// This will first update checkpoints from the database, then remove actual block data from
/// static files.
RemoveBlocksAbove(u64, oneshot::Sender<Option<BlockNumHash>>),
RemoveBlocksAbove(u64, CrossbeamSender<Option<BlockNumHash>>),
/// Update the persisted finalized block on disk
SaveFinalizedBlock(u64),
@@ -264,7 +261,7 @@ impl<T: NodePrimitives> PersistenceHandle<T> {
pub fn save_blocks(
&self,
blocks: Vec<ExecutedBlock<T>>,
tx: oneshot::Sender<Option<BlockNumHash>>,
tx: CrossbeamSender<Option<BlockNumHash>>,
) -> Result<(), SendError<PersistenceAction<T>>> {
self.send_action(PersistenceAction::SaveBlocks(blocks, tx))
}
@@ -293,7 +290,7 @@ impl<T: NodePrimitives> PersistenceHandle<T> {
pub fn remove_blocks_above(
&self,
block_num: u64,
tx: oneshot::Sender<Option<BlockNumHash>>,
tx: CrossbeamSender<Option<BlockNumHash>>,
) -> Result<(), SendError<PersistenceAction<T>>> {
self.send_action(PersistenceAction::RemoveBlocksAbove(block_num, tx))
}
@@ -322,22 +319,22 @@ mod tests {
PersistenceHandle::<EthPrimitives>::spawn_service(provider, pruner, sync_metrics_tx)
}
#[tokio::test]
async fn test_save_blocks_empty() {
#[test]
fn test_save_blocks_empty() {
reth_tracing::init_test_tracing();
let persistence_handle = default_persistence_handle();
let blocks = vec![];
let (tx, rx) = oneshot::channel();
let (tx, rx) = crossbeam_channel::bounded(1);
persistence_handle.save_blocks(blocks, tx).unwrap();
let hash = rx.await.unwrap();
let hash = rx.recv().unwrap();
assert_eq!(hash, None);
}
#[tokio::test]
async fn test_save_blocks_single_block() {
#[test]
fn test_save_blocks_single_block() {
reth_tracing::init_test_tracing();
let persistence_handle = default_persistence_handle();
let block_number = 0;
@@ -347,37 +344,35 @@ mod tests {
let block_hash = executed.recovered_block().hash();
let blocks = vec![executed];
let (tx, rx) = oneshot::channel();
let (tx, rx) = crossbeam_channel::bounded(1);
persistence_handle.save_blocks(blocks, tx).unwrap();
let BlockNumHash { hash: actual_hash, number: _ } =
tokio::time::timeout(std::time::Duration::from_secs(10), rx)
.await
.expect("test timed out")
.expect("channel closed unexpectedly")
.expect("no hash returned");
let BlockNumHash { hash: actual_hash, number: _ } = rx
.recv_timeout(std::time::Duration::from_secs(10))
.expect("test timed out")
.expect("no hash returned");
assert_eq!(block_hash, actual_hash);
}
#[tokio::test]
async fn test_save_blocks_multiple_blocks() {
#[test]
fn test_save_blocks_multiple_blocks() {
reth_tracing::init_test_tracing();
let persistence_handle = default_persistence_handle();
let mut test_block_builder = TestBlockBuilder::eth();
let blocks = test_block_builder.get_executed_blocks(0..5).collect::<Vec<_>>();
let last_hash = blocks.last().unwrap().recovered_block().hash();
let (tx, rx) = oneshot::channel();
let (tx, rx) = crossbeam_channel::bounded(1);
persistence_handle.save_blocks(blocks, tx).unwrap();
let BlockNumHash { hash: actual_hash, number: _ } = rx.await.unwrap().unwrap();
let BlockNumHash { hash: actual_hash, number: _ } = rx.recv().unwrap().unwrap();
assert_eq!(last_hash, actual_hash);
}
#[tokio::test]
async fn test_save_blocks_multiple_calls() {
#[test]
fn test_save_blocks_multiple_calls() {
reth_tracing::init_test_tracing();
let persistence_handle = default_persistence_handle();
@@ -386,11 +381,11 @@ mod tests {
for range in ranges {
let blocks = test_block_builder.get_executed_blocks(range).collect::<Vec<_>>();
let last_hash = blocks.last().unwrap().recovered_block().hash();
let (tx, rx) = oneshot::channel();
let (tx, rx) = crossbeam_channel::bounded(1);
persistence_handle.save_blocks(blocks, tx).unwrap();
let BlockNumHash { hash: actual_hash, number: _ } = rx.await.unwrap().unwrap();
let BlockNumHash { hash: actual_hash, number: _ } = rx.recv().unwrap().unwrap();
assert_eq!(last_hash, actual_hash);
}
}

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 {

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,13 @@ use reth_errors::{BlockExecutionError, BlockValidationError, ProviderError};
use reth_evm::execute::InternalBlockExecutionError;
use reth_payload_primitives::NewPayloadError;
use reth_primitives_traits::{Block, BlockBody, SealedBlock};
use tokio::sync::oneshot::error::TryRecvError;
/// This is an error that can come from advancing persistence. Either this can be a
/// [`TryRecvError`], or this can be a [`ProviderError`]
/// This is an error that can come from advancing persistence.
#[derive(Debug, thiserror::Error)]
pub enum AdvancePersistenceError {
/// An error that can be from failing to receive a value from persistence
#[error(transparent)]
RecvError(#[from] TryRecvError),
/// The persistence channel was closed unexpectedly
#[error("persistence channel closed")]
ChannelClosed,
/// A provider error
#[error(transparent)]
Provider(#[from] ProviderError),

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 = || {
@@ -320,7 +321,7 @@ impl NewPayloadStatusMetrics {
}
/// Metrics for non-execution related block validation.
#[derive(Metrics)]
#[derive(Metrics, Clone)]
#[metrics(scope = "sync.block_validation")]
pub(crate) struct BlockValidationMetrics {
/// Total number of storage tries updated in the state root calculation
@@ -347,6 +348,14 @@ pub(crate) struct BlockValidationMetrics {
pub(crate) post_execution_validation_duration: Histogram,
/// Total duration of the new payload call
pub(crate) total_duration: Histogram,
/// Size of `HashedPostStateSorted` (`total_len`)
pub(crate) hashed_post_state_size: Histogram,
/// Size of `TrieUpdatesSorted` (`total_len`)
pub(crate) trie_updates_sorted_size: Histogram,
/// Size of `AnchoredTrieInput` overlay `TrieUpdatesSorted` (`total_len`)
pub(crate) anchored_overlay_trie_updates_size: Histogram,
/// Size of `AnchoredTrieInput` overlay `HashedPostStateSorted` (`total_len`)
pub(crate) anchored_overlay_hashed_state_size: Histogram,
}
impl BlockValidationMetrics {
@@ -529,6 +538,7 @@ mod tests {
let _result = metrics.execute_metered::<_, EmptyDB>(
executor,
input.clone_transactions_recovered().map(Ok::<_, BlockExecutionError>),
input.transaction_count(),
state_hook,
);
@@ -585,6 +595,7 @@ mod tests {
let _result = metrics.execute_metered::<_, EmptyDB>(
executor,
input.clone_transactions_recovered().map(Ok::<_, BlockExecutionError>),
input.transaction_count(),
state_hook,
);

View File

@@ -37,18 +37,12 @@ use reth_revm::database::StateProviderDatabase;
use reth_stages_api::ControlFlow;
use revm::state::EvmState;
use state::TreeState;
use std::{
fmt::Debug,
ops,
sync::{
mpsc::{Receiver, RecvError, RecvTimeoutError, Sender},
Arc,
},
time::Instant,
};
use std::{fmt::Debug, ops, sync::Arc, time::Instant};
use crossbeam_channel::{Receiver, Sender};
use tokio::sync::{
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
oneshot::{self, error::TryRecvError},
oneshot,
};
use tracing::*;
@@ -64,7 +58,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;
@@ -241,7 +234,7 @@ where
C: ConfigureEvm<Primitives = N> + 'static,
{
provider: P,
consensus: Arc<dyn FullConsensus<N, Error = ConsensusError>>,
consensus: Arc<dyn FullConsensus<N>>,
payload_validator: V,
/// Keeps track of internals such as executed and buffered blocks.
state: EngineApiTreeState<N>,
@@ -327,7 +320,7 @@ where
#[expect(clippy::too_many_arguments)]
pub fn new(
provider: P,
consensus: Arc<dyn FullConsensus<N, Error = ConsensusError>>,
consensus: Arc<dyn FullConsensus<N>>,
payload_validator: V,
outgoing: UnboundedSender<EngineApiEvent<N>>,
state: EngineApiTreeState<N>,
@@ -339,7 +332,7 @@ where
engine_kind: EngineApiKind,
evm_config: C,
) -> Self {
let (incoming_tx, incoming) = std::sync::mpsc::channel();
let (incoming_tx, incoming) = crossbeam_channel::unbounded();
Self {
provider,
@@ -369,7 +362,7 @@ where
#[expect(clippy::complexity)]
pub fn spawn_new(
provider: P,
consensus: Arc<dyn FullConsensus<N, Error = ConsensusError>>,
consensus: Arc<dyn FullConsensus<N>>,
payload_validator: V,
persistence: PersistenceHandle<N>,
payload_builder: PayloadBuilderHandle<T>,
@@ -424,8 +417,8 @@ where
/// This will block the current thread and process incoming messages.
pub fn run(mut self) {
loop {
match self.try_recv_engine_message() {
Ok(Some(msg)) => {
match self.wait_for_event() {
LoopEvent::EngineMessage(msg) => {
debug!(target: "engine::tree", %msg, "received new engine message");
match self.on_engine_message(msg) {
Ok(ops::ControlFlow::Break(())) => return,
@@ -436,15 +429,22 @@ where
}
}
}
Ok(None) => {
debug!(target: "engine::tree", "received no engine message for some time, while waiting for persistence task to complete");
LoopEvent::PersistenceComplete { result, start_time } => {
if let Err(err) = self.on_persistence_complete(result, start_time) {
error!(target: "engine::tree", %err, "Persistence complete handling failed");
return
}
}
Err(_err) => {
error!(target: "engine::tree", "Engine channel disconnected");
LoopEvent::Disconnected => {
error!(target: "engine::tree", "Channel disconnected");
return
}
}
// Always check if we need to trigger new persistence after any event:
// - After engine messages: new blocks may have been inserted that exceed the
// persistence threshold
// - After persistence completion: we can now persist more blocks if needed
if let Err(err) = self.advance_persistence() {
error!(target: "engine::tree", %err, "Advancing persistence failed");
return
@@ -452,6 +452,47 @@ where
}
}
/// Blocks until the next event is ready: either an incoming engine message or a persistence
/// completion (if one is in progress).
///
/// Uses biased selection to prioritize persistence completion to update in-memory state and
/// unblock further writes.
fn wait_for_event(&mut self) -> LoopEvent<T, N> {
// Take ownership of persistence rx if present
let maybe_persistence = self.persistence_state.rx.take();
if let Some((persistence_rx, start_time, action)) = maybe_persistence {
// Biased select prioritizes persistence completion to update in memory state and
// unblock further writes
crossbeam_channel::select_biased! {
recv(persistence_rx) -> result => {
// Don't put it back - consumed (oneshot-like behavior)
match result {
Ok(value) => LoopEvent::PersistenceComplete {
result: value,
start_time,
},
Err(_) => LoopEvent::Disconnected,
}
},
recv(self.incoming) -> msg => {
// Put the persistence rx back - we didn't consume it
self.persistence_state.rx = Some((persistence_rx, start_time, action));
match msg {
Ok(m) => LoopEvent::EngineMessage(m),
Err(_) => LoopEvent::Disconnected,
}
},
}
} else {
// No persistence in progress - just wait on incoming
match self.incoming.recv() {
Ok(m) => LoopEvent::EngineMessage(m),
Err(_) => LoopEvent::Disconnected,
}
}
}
/// Invoked when previously requested blocks were downloaded.
///
/// If the block count exceeds the configured batch size we're allowed to execute at once, this
@@ -826,7 +867,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 +973,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.
@@ -1233,39 +1233,13 @@ where
.with_event(TreeEvent::Download(DownloadRequest::single_block(target))))
}
/// Attempts to receive the next engine request.
///
/// If there's currently no persistence action in progress, this will block until a new request
/// is received. If there's a persistence action in progress, this will try to receive the
/// next request with a timeout to not block indefinitely and return `Ok(None)` if no request is
/// received in time.
///
/// Returns an error if the engine channel is disconnected.
#[expect(clippy::type_complexity)]
fn try_recv_engine_message(
&self,
) -> Result<Option<FromEngine<EngineApiRequest<T, N>, N::Block>>, RecvError> {
if self.persistence_state.in_progress() {
// try to receive the next request with a timeout to not block indefinitely
match self.incoming.recv_timeout(std::time::Duration::from_millis(500)) {
Ok(msg) => Ok(Some(msg)),
Err(err) => match err {
RecvTimeoutError::Timeout => Ok(None),
RecvTimeoutError::Disconnected => Err(RecvError),
},
}
} else {
self.incoming.recv().map(Some)
}
}
/// Helper method to remove blocks and set the persistence state. This ensures we keep track of
/// the current persistence action while we're removing blocks.
fn remove_blocks(&mut self, new_tip_num: u64) {
debug!(target: "engine::tree", ?new_tip_num, last_persisted_block_number=?self.persistence_state.last_persisted_block.number, "Removing blocks using persistence task");
if new_tip_num < self.persistence_state.last_persisted_block.number {
debug!(target: "engine::tree", ?new_tip_num, "Starting remove blocks job");
let (tx, rx) = oneshot::channel();
let (tx, rx) = crossbeam_channel::bounded(1);
let _ = self.persistence.remove_blocks_above(new_tip_num, tx);
self.persistence_state.start_remove(new_tip_num, rx);
}
@@ -1287,35 +1261,17 @@ where
.expect("Checked non-empty persisting blocks");
debug!(target: "engine::tree", count=blocks_to_persist.len(), blocks = ?blocks_to_persist.iter().map(|block| block.recovered_block().num_hash()).collect::<Vec<_>>(), "Persisting blocks");
let (tx, rx) = oneshot::channel();
let (tx, rx) = crossbeam_channel::bounded(1);
let _ = self.persistence.save_blocks(blocks_to_persist, tx);
self.persistence_state.start_save(highest_num_hash, rx);
}
/// Attempts to advance the persistence state.
/// Triggers new persistence actions if no persistence task is currently in progress.
///
/// If we're currently awaiting a response this will try to receive the response (non-blocking)
/// or send a new persistence action if necessary.
/// This checks if we need to remove blocks (disk reorg) or save new blocks to disk.
/// Persistence completion is handled separately via the `wait_for_event` method.
fn advance_persistence(&mut self) -> Result<(), AdvancePersistenceError> {
if self.persistence_state.in_progress() {
let (mut rx, start_time, current_action) = self
.persistence_state
.rx
.take()
.expect("if a persistence task is in progress Receiver must be Some");
// Check if persistence has complete
match rx.try_recv() {
Ok(last_persisted_hash_num) => {
self.on_persistence_complete(last_persisted_hash_num, start_time)?;
}
Err(TryRecvError::Closed) => return Err(TryRecvError::Closed.into()),
Err(TryRecvError::Empty) => {
self.persistence_state.rx = Some((rx, start_time, current_action))
}
}
}
if !self.persistence_state.in_progress() {
if let Some(new_tip_num) = self.find_disk_reorg()? {
self.remove_blocks(new_tip_num)
@@ -1348,7 +1304,7 @@ where
loop {
// Wait for any in-progress persistence to complete (blocking)
if let Some((rx, start_time, _action)) = self.persistence_state.rx.take() {
let result = rx.blocking_recv().map_err(|_| TryRecvError::Closed)?;
let result = rx.recv().map_err(|_| AdvancePersistenceError::ChannelClosed)?;
self.on_persistence_complete(result, start_time)?;
}
@@ -1364,6 +1320,31 @@ where
}
}
/// Tries to poll for a completed persistence task (non-blocking).
///
/// Returns `true` if a persistence task was completed, `false` otherwise.
#[cfg(test)]
pub fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
let Some((rx, start_time, action)) = self.persistence_state.rx.take() else {
return Ok(false);
};
match rx.try_recv() {
Ok(result) => {
self.on_persistence_complete(result, start_time)?;
Ok(true)
}
Err(crossbeam_channel::TryRecvError::Empty) => {
// Not ready yet, put it back
self.persistence_state.rx = Some((rx, start_time, action));
Ok(false)
}
Err(crossbeam_channel::TryRecvError::Disconnected) => {
Err(AdvancePersistenceError::ChannelClosed)
}
}
}
/// Handles a completed persistence task.
fn on_persistence_complete(
&mut self,
@@ -2569,14 +2550,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);
@@ -2893,6 +2871,26 @@ where
}
}
/// Events received in the main engine loop.
#[derive(Debug)]
enum LoopEvent<T, N>
where
N: NodePrimitives,
T: PayloadTypes,
{
/// An engine API message was received.
EngineMessage(FromEngine<EngineApiRequest<T, N>, N::Block>),
/// A persistence task completed.
PersistenceComplete {
/// The result of the persistence operation.
result: Option<BlockNumHash>,
/// When the persistence operation started.
start_time: Instant,
},
/// A channel was disconnected.
Disconnected,
}
/// Block inclusion can be valid, accepted, or invalid. Invalid blocks are returned as an error
/// variant.
///

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

@@ -1,7 +1,7 @@
//! Configured sparse trie enum for switching between serial and parallel implementations.
use alloy_primitives::B256;
use reth_trie::{Nibbles, ProofTrieNode, TrieMasks, TrieNode};
use reth_trie::{BranchNodeMasks, Nibbles, ProofTrieNode, TrieNode};
use reth_trie_sparse::{
errors::SparseTrieResult, provider::TrieNodeProvider, LeafLookup, LeafLookupError,
SerialSparseTrie, SparseTrieInterface, SparseTrieUpdates,
@@ -44,7 +44,7 @@ impl SparseTrieInterface for ConfiguredSparseTrie {
fn with_root(
self,
root: TrieNode,
masks: TrieMasks,
masks: Option<BranchNodeMasks>,
retain_updates: bool,
) -> SparseTrieResult<Self> {
match self {
@@ -75,7 +75,7 @@ impl SparseTrieInterface for ConfiguredSparseTrie {
&mut self,
path: Nibbles,
node: TrieNode,
masks: TrieMasks,
masks: Option<BranchNodeMasks>,
) -> SparseTrieResult<()> {
match self {
Self::Serial(trie) => trie.reveal_node(path, node, masks),

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,
FixedCacheMetrics, 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;
@@ -113,7 +116,7 @@ where
/// Metrics for trie operations
trie_metrics: MultiProofTaskMetrics,
/// Cross-block cache size in bytes.
cross_block_cache_size: u64,
cross_block_cache_size: usize,
/// Whether transactions should not be executed on prewarming task.
disable_transaction_prewarming: bool,
/// Whether state cache should be disable
@@ -236,70 +239,72 @@ where
let span = Span::current();
let (to_sparse_trie, sparse_trie_rx) = channel();
// 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.
// Create and spawn the storage proof task
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
let storage_worker_count = config.storage_worker_count();
let account_worker_count = config.account_worker_count();
let proof_handle = ProofWorkerHandle::new(
self.executor.handle().clone(),
task_ctx,
storage_worker_count,
account_worker_count,
);
let multi_proof_task = MultiProofTask::new(
proof_handle.clone(),
to_sparse_trie,
config.multiproof_chunking_enabled().then_some(config.multiproof_chunk_size()),
);
// wire the multiproof task to the prewarm task
let to_multi_proof = Some(multi_proof_task.state_root_message_sender());
let (to_multi_proof, from_multi_proof) = crossbeam_channel::unbounded();
// 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");
// 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
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");
}
let _ = to_multi_proof.send(MultiProofMessage::BlockAccessList(Arc::clone(&bal)));
// Spawn minimal cache-only task without prewarming
// 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 full prewarming
// Normal path: spawn with transaction prewarming
self.spawn_caching_with(
env,
prewarm_rx,
transaction_count_hint,
provider_builder.clone(),
to_multi_proof.clone(),
Some(to_multi_proof.clone()),
None,
)
};
// Create and spawn the storage proof task
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
let storage_worker_count = config.storage_worker_count();
let account_worker_count = config.account_worker_count();
let v2_proofs_enabled = config.enable_proof_v2();
let proof_handle = ProofWorkerHandle::new(
self.executor.handle().clone(),
task_ctx,
storage_worker_count,
account_worker_count,
v2_proofs_enabled,
);
let multi_proof_task = MultiProofTask::new(
proof_handle.clone(),
to_sparse_trie,
config.multiproof_chunking_enabled().then_some(config.multiproof_chunk_size()),
to_multi_proof.clone(),
from_multi_proof,
);
// 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);
});
@@ -310,7 +315,7 @@ where
self.spawn_sparse_trie_task(sparse_trie_rx, proof_handle, state_root_tx);
PayloadHandle {
to_multi_proof,
to_multi_proof: Some(to_multi_proof),
prewarm_handle,
state_root: Some(state_root_rx),
transactions: execution_rx,
@@ -327,13 +332,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 +413,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 +424,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 +451,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.
@@ -469,8 +474,13 @@ where
cache
} else {
debug!("creating new execution cache on cache miss");
let cache = ExecutionCacheBuilder::default().build_caches(self.cross_block_cache_size);
SavedCache::new(parent_hash, cache, CachedStateMetrics::zeroed())
let cache = crate::tree::cached_state::ExecutionCache::new(self.cross_block_cache_size);
SavedCache::new(
parent_hash,
cache,
CachedStateMetrics::zeroed(),
FixedCacheMetrics::zeroed(),
)
}
}
@@ -486,38 +496,40 @@ where
BPF::AccountNodeProvider: TrieNodeProvider + Send + Sync,
BPF::StorageNodeProvider: TrieNodeProvider + Send + Sync,
{
// Reuse a stored SparseStateTrie, or create a new one using the desired configuration if
// there's none to reuse.
let cleared_sparse_trie = Arc::clone(&self.sparse_state_trie);
let sparse_state_trie = cleared_sparse_trie.lock().take().unwrap_or_else(|| {
let default_trie = SparseTrie::blind_from(if self.disable_parallel_sparse_trie {
ConfiguredSparseTrie::Serial(Default::default())
} else {
ConfiguredSparseTrie::Parallel(Box::new(
ParallelSparseTrie::default()
.with_parallelism_thresholds(PARALLEL_SPARSE_TRIE_PARALLELISM_THRESHOLDS),
))
});
ClearedSparseStateTrie::from_state_trie(
SparseStateTrie::new()
.with_accounts_trie(default_trie.clone())
.with_default_storage_trie(default_trie)
.with_updates(true),
)
});
let task =
SparseTrieTask::<_, ConfiguredSparseTrie, ConfiguredSparseTrie>::new_with_cleared_trie(
sparse_trie_rx,
proof_worker_handle,
self.trie_metrics.clone(),
sparse_state_trie,
);
let disable_parallel_sparse_trie = self.disable_parallel_sparse_trie;
let trie_metrics = self.trie_metrics.clone();
let span = Span::current();
self.executor.spawn_blocking(move || {
let _enter = span.entered();
// Reuse a stored SparseStateTrie, or create a new one using the desired configuration
// if there's none to reuse.
let sparse_state_trie = cleared_sparse_trie.lock().take().unwrap_or_else(|| {
let default_trie = SparseTrie::blind_from(if disable_parallel_sparse_trie {
ConfiguredSparseTrie::Serial(Default::default())
} else {
ConfiguredSparseTrie::Parallel(Box::new(
ParallelSparseTrie::default()
.with_parallelism_thresholds(PARALLEL_SPARSE_TRIE_PARALLELISM_THRESHOLDS),
))
});
ClearedSparseStateTrie::from_state_trie(
SparseStateTrie::new()
.with_accounts_trie(default_trie.clone())
.with_default_storage_trie(default_trie)
.with_updates(true),
)
});
let task = SparseTrieTask::<_, ConfiguredSparseTrie, ConfiguredSparseTrie>::new_with_cleared_trie(
sparse_trie_rx,
proof_worker_handle,
trie_metrics,
sparse_state_trie,
);
let (result, trie) = task.run();
// Send state root computation result
let _ = state_root_tx.send(result);
@@ -561,18 +573,22 @@ where
}
// Take existing cache (if any) or create fresh caches
let (caches, cache_metrics) = match cached.take() {
Some(existing) => {
existing.split()
}
let (caches, cache_metrics, fixed_cache_metrics) = match cached.take() {
Some(existing) => existing.split(),
None => (
ExecutionCacheBuilder::default().build_caches(self.cross_block_cache_size),
crate::tree::cached_state::ExecutionCache::new(self.cross_block_cache_size),
CachedStateMetrics::zeroed(),
FixedCacheMetrics::zeroed(),
),
};
// Insert the block's bundle state into cache
let new_cache = SavedCache::new(block_with_parent.block.hash, caches, cache_metrics);
let new_cache = SavedCache::new(
block_with_parent.block.hash,
caches,
cache_metrics,
fixed_cache_metrics,
);
if new_cache.cache().insert_state(bundle_state).is_err() {
*cached = None;
debug!(target: "engine::caching", "cleared execution cache on update error");
@@ -641,12 +657,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 +699,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>>>,
}
@@ -771,12 +785,37 @@ impl ExecutionCache {
warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
}
cache
.as_ref()
if let Some(c) = cache.as_ref() {
let cached_hash = c.executed_block_hash();
// Check that the cache hash matches the parent hash of the current block. It won't
// match in case it's a fork block.
let hash_matches = cached_hash == parent_hash;
// Check `is_available()` to ensure no other tasks (e.g., prewarming) currently hold
// a reference to this cache. We can only reuse it when we have exclusive access.
.filter(|c| c.executed_block_hash() == parent_hash && c.is_available())
.cloned()
let available = c.is_available();
let usage_count = c.usage_count();
debug!(
target: "engine::caching",
%cached_hash,
%parent_hash,
hash_matches,
available,
usage_count,
"Existing cache found"
);
if available {
if !hash_matches {
c.clear();
}
return Some(c.clone());
}
} else {
debug!(target: "engine::caching", %parent_hash, "No cache found");
}
None
}
/// Clears the tracked cache
@@ -835,7 +874,7 @@ where
mod tests {
use super::ExecutionCache;
use crate::tree::{
cached_state::{CachedStateMetrics, ExecutionCacheBuilder, SavedCache},
cached_state::{CachedStateMetrics, FixedCacheMetrics, SavedCache},
payload_processor::{
evm_state_to_hashed_post_state, executor::WorkloadExecutor, PayloadProcessor,
},
@@ -864,8 +903,13 @@ mod tests {
use std::sync::Arc;
fn make_saved_cache(hash: B256) -> SavedCache {
let execution_cache = ExecutionCacheBuilder::default().build_caches(1_000);
SavedCache::new(hash, execution_cache, CachedStateMetrics::zeroed())
let execution_cache = crate::tree::cached_state::ExecutionCache::new(1_000);
SavedCache::new(
hash,
execution_cache,
CachedStateMetrics::zeroed(),
FixedCacheMetrics::zeroed(),
)
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@@ -14,13 +14,16 @@
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};
@@ -30,10 +33,11 @@ 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 +47,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 +176,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;
@@ -265,8 +272,8 @@ where
execution_cache.update_with_guard(|cached| {
// consumes the `SavedCache` held by the prewarming task, which releases its usage
// guard
let (caches, cache_metrics) = saved_cache.split();
let new_cache = SavedCache::new(hash, caches, cache_metrics);
let (caches, cache_metrics, fixed_cache_metrics) = saved_cache.split();
let new_cache = SavedCache::new(hash, caches, cache_metrics, fixed_cache_metrics);
// Insert state into cache while holding the lock
// Access the BundleState through the shared ExecutionOutcome
@@ -291,6 +298,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 +389,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;
@@ -536,27 +632,134 @@ 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());
}
}
@@ -639,4 +842,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

@@ -121,8 +121,9 @@ where
ParallelStateRootError::Other(format!("could not calculate state root: {e:?}"))
})?;
self.metrics.sparse_trie_final_update_duration_histogram.record(start.elapsed());
self.metrics.sparse_trie_total_duration_histogram.record(now.elapsed());
let end = Instant::now();
self.metrics.sparse_trie_final_update_duration_histogram.record(end.duration_since(start));
self.metrics.sparse_trie_total_duration_histogram.record(end.duration_since(now));
Ok(StateRootComputeOutcome { state_root, trie_updates })
}
@@ -166,15 +167,14 @@ 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)))
.par_bridge()
.map(|(address, storage, storage_trie)| {
let _enter =
debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: span.clone(), "storage trie", ?address)
debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage trie", ?address)
.entered();
trace!(target: "engine::tree::payload_processor::sparse_trie", "Updating storage");
@@ -217,13 +217,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 +229,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

@@ -34,13 +34,12 @@ use reth_primitives_traits::{
SealedHeader, SignerRecoverable,
};
use reth_provider::{
providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockReader,
DatabaseProviderFactory, DatabaseProviderROFactory, ExecutionOutcome, HashedPostStateProvider,
ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider,
StateProviderFactory, StateReader, TrieReader,
providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockNumReader, BlockReader,
ChangeSetReader, DatabaseProviderFactory, DatabaseProviderROFactory, ExecutionOutcome,
HashedPostStateProvider, ProviderError, PruneCheckpointReader, StageCheckpointReader,
StateProvider, StateProviderFactory, StateReader, TrieReader,
};
use reth_revm::db::State;
use reth_storage_errors::db::DatabaseError;
use reth_trie::{updates::TrieUpdates, HashedPostState, StateRoot, TrieInputSorted};
use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError};
use revm_primitives::Address;
@@ -112,7 +111,7 @@ where
/// Provider for database access.
provider: P,
/// Consensus implementation for validation.
consensus: Arc<dyn FullConsensus<Evm::Primitives, Error = ConsensusError>>,
consensus: Arc<dyn FullConsensus<Evm::Primitives>>,
/// EVM configuration.
evm_config: Evm,
/// Configuration for the tree.
@@ -136,8 +135,15 @@ impl<N, P, Evm, V> BasicEngineValidator<P, Evm, V>
where
N: NodePrimitives,
P: DatabaseProviderFactory<
Provider: BlockReader + TrieReader + StageCheckpointReader + PruneCheckpointReader,
Provider: BlockReader
+ TrieReader
+ StageCheckpointReader
+ PruneCheckpointReader
+ ChangeSetReader
+ BlockNumReader,
> + BlockReader<Header = N::BlockHeader>
+ ChangeSetReader
+ BlockNumReader
+ StateProviderFactory
+ StateReader
+ HashedPostStateProvider
@@ -149,7 +155,7 @@ where
#[allow(clippy::too_many_arguments)]
pub fn new(
provider: P,
consensus: Arc<dyn FullConsensus<N, Error = ConsensusError>>,
consensus: Arc<dyn FullConsensus<N>>,
evm_config: Evm,
validator: V,
config: TreeConfig,
@@ -435,8 +441,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 +608,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>>,
@@ -617,7 +622,8 @@ where
.without_state_clear()
.build();
let evm = self.evm_config.evm_with_env(&mut db, env.evm_env.clone());
let spec_id = *env.evm_env.spec_id();
let evm = self.evm_config.evm_with_env(&mut db, env.evm_env);
let ctx =
self.execution_ctx_for(input).map_err(|e| InsertBlockErrorKind::Other(Box::new(e)))?;
let mut executor = self.evm_config.create_executor(evm, ctx);
@@ -633,7 +639,7 @@ where
CachedPrecompile::wrap(
precompile,
self.precompile_cache_map.cache_for_address(*address),
*env.evm_env.spec_id(),
spec_id,
Some(metrics),
)
});
@@ -644,6 +650,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();
@@ -713,8 +720,7 @@ where
Ok(StateRoot::new(&provider, &provider)
.with_prefix_sets(prefix_sets.freeze())
.root_with_updates()
.map_err(Into::<DatabaseError>::into)?)
.root_with_updates()?)
}
/// Validates the block after execution.
@@ -862,8 +868,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
@@ -1076,16 +1086,33 @@ where
ancestors,
);
let deferred_handle_task = deferred_trie_data.clone();
let deferred_compute_duration =
self.metrics.block_validation.deferred_trie_compute_duration.clone();
let block_validation_metrics = self.metrics.block_validation.clone();
// Spawn background task to compute trie data. Calling `wait_cloned` will compute from
// the stored inputs and cache the result, so subsequent calls return immediately.
let compute_trie_input_task = move || {
let result = panic::catch_unwind(AssertUnwindSafe(|| {
let compute_start = Instant::now();
let _ = deferred_handle_task.wait_cloned();
deferred_compute_duration.record(compute_start.elapsed().as_secs_f64());
let computed = deferred_handle_task.wait_cloned();
block_validation_metrics
.deferred_trie_compute_duration
.record(compute_start.elapsed().as_secs_f64());
// Record sizes of the computed trie data
block_validation_metrics
.hashed_post_state_size
.record(computed.hashed_state.total_len() as f64);
block_validation_metrics
.trie_updates_sorted_size
.record(computed.trie_updates.total_len() as f64);
if let Some(anchored) = &computed.anchored_trie_input {
block_validation_metrics
.anchored_overlay_trie_updates_size
.record(anchored.trie_input.nodes.total_len() as f64);
block_validation_metrics
.anchored_overlay_hashed_state_size
.record(anchored.trie_input.state.total_len() as f64);
}
}));
if result.is_err() {
@@ -1181,10 +1208,17 @@ pub trait EngineValidator<
impl<N, Types, P, Evm, V> EngineValidator<Types> for BasicEngineValidator<P, Evm, V>
where
P: DatabaseProviderFactory<
Provider: BlockReader + TrieReader + StageCheckpointReader + PruneCheckpointReader,
Provider: BlockReader
+ TrieReader
+ StageCheckpointReader
+ PruneCheckpointReader
+ ChangeSetReader
+ BlockNumReader,
> + BlockReader<Header = N::BlockHeader>
+ StateProviderFactory
+ StateReader
+ ChangeSetReader
+ BlockNumReader
+ HashedPostStateProvider
+ Clone
+ 'static,
@@ -1288,4 +1322,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

@@ -22,12 +22,12 @@
use alloy_eips::BlockNumHash;
use alloy_primitives::B256;
use crossbeam_channel::Receiver as CrossbeamReceiver;
use std::time::Instant;
use tokio::sync::oneshot;
use tracing::trace;
/// The state of the persistence task.
#[derive(Default, Debug)]
#[derive(Debug)]
pub struct PersistenceState {
/// Hash and number of the last block persisted.
///
@@ -36,7 +36,7 @@ pub struct PersistenceState {
/// Receiver end of channel where the result of the persistence task will be
/// sent when done. A None value means there's no persistence task in progress.
pub(crate) rx:
Option<(oneshot::Receiver<Option<BlockNumHash>>, Instant, CurrentPersistenceAction)>,
Option<(CrossbeamReceiver<Option<BlockNumHash>>, Instant, CurrentPersistenceAction)>,
}
impl PersistenceState {
@@ -50,7 +50,7 @@ impl PersistenceState {
pub(crate) fn start_remove(
&mut self,
new_tip_num: u64,
rx: oneshot::Receiver<Option<BlockNumHash>>,
rx: CrossbeamReceiver<Option<BlockNumHash>>,
) {
self.rx =
Some((rx, Instant::now(), CurrentPersistenceAction::RemovingBlocks { new_tip_num }));
@@ -60,7 +60,7 @@ impl PersistenceState {
pub(crate) fn start_save(
&mut self,
highest: BlockNumHash,
rx: oneshot::Receiver<Option<BlockNumHash>>,
rx: CrossbeamReceiver<Option<BlockNumHash>>,
) {
self.rx = Some((rx, Instant::now(), CurrentPersistenceAction::SavingBlocks { highest }));
}

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

@@ -31,7 +31,7 @@ use std::{
collections::BTreeMap,
str::FromStr,
sync::{
mpsc::{channel, Receiver, Sender},
mpsc::{Receiver, Sender},
Arc,
},
};
@@ -97,6 +97,7 @@ struct TestChannel<T> {
impl<T: Send + 'static> TestChannel<T> {
/// Creates a new test channel
fn spawn_channel() -> (Sender<T>, Receiver<T>, TestChannelHandle) {
use std::sync::mpsc::channel;
let (original_tx, original_rx) = channel();
let (wrapped_tx, wrapped_rx) = channel();
let (release_tx, release_rx) = channel();
@@ -143,7 +144,9 @@ struct TestHarness {
BasicEngineValidator<MockEthProvider, MockEvmConfig, MockEngineValidator>,
MockEvmConfig,
>,
to_tree_tx: Sender<FromEngine<EngineApiRequest<EthEngineTypes, EthPrimitives>, Block>>,
to_tree_tx: crossbeam_channel::Sender<
FromEngine<EngineApiRequest<EthEngineTypes, EthPrimitives>, Block>,
>,
from_tree_rx: UnboundedReceiver<EngineApiEvent>,
blocks: Vec<ExecutedBlock>,
action_rx: Receiver<PersistenceAction>,
@@ -153,6 +156,7 @@ struct TestHarness {
impl TestHarness {
fn new(chain_spec: Arc<ChainSpec>) -> Self {
use std::sync::mpsc::channel;
let (action_tx, action_rx) = channel();
Self::with_persistence_channel(chain_spec, action_tx, action_rx)
}
@@ -205,7 +209,7 @@ impl TestHarness {
engine_api_tree_state,
canonical_in_memory_state,
persistence_handle,
PersistenceState::default(),
PersistenceState { last_persisted_block: BlockNumHash::default(), rx: None },
payload_builder,
// always assume enough parallelism for tests
TreeConfig::default().with_legacy_state_root(false).with_has_enough_parallelism(true),
@@ -399,10 +403,8 @@ impl ValidatorTestHarness {
/// Configure `PersistenceState` for specific persistence scenarios
fn start_persistence_operation(&mut self, action: CurrentPersistenceAction) {
use tokio::sync::oneshot;
// Create a dummy receiver for testing - it will never receive a value
let (_tx, rx) = oneshot::channel();
let (_tx, rx) = crossbeam_channel::bounded(1);
match action {
CurrentPersistenceAction::SavingBlocks { highest } => {
@@ -498,11 +500,17 @@ fn test_tree_persist_block_batch() {
test_harness.to_tree_tx.send(FromEngine::DownloadedBlocks(blocks)).unwrap();
// process the message
let msg = test_harness.tree.try_recv_engine_message().unwrap().unwrap();
let msg = match test_harness.tree.wait_for_event() {
super::LoopEvent::EngineMessage(msg) => msg,
other => panic!("unexpected event: {other:?}"),
};
let _ = test_harness.tree.on_engine_message(msg).unwrap();
// we now should receive the other batch
let msg = test_harness.tree.try_recv_engine_message().unwrap().unwrap();
let msg = match test_harness.tree.wait_for_event() {
super::LoopEvent::EngineMessage(msg) => msg,
other => panic!("unexpected event: {other:?}"),
};
match msg {
FromEngine::DownloadedBlocks(blocks) => {
assert_eq!(blocks.len(), tree_config.max_execute_block_batch_size());
@@ -753,8 +761,8 @@ async fn test_tree_state_on_new_head_reorg() {
})
);
// after advancing persistence, we should be at `None` for the next action
test_harness.tree.advance_persistence().unwrap();
// after polling persistence completion, we should be at `None` for the next action
test_harness.tree.try_poll_persistence().unwrap();
let current_action = test_harness.tree.persistence_state.current_action().cloned();
assert_eq!(current_action, None);

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

@@ -51,9 +51,15 @@ jemalloc = [
"reth-node-metrics/jemalloc",
]
jemalloc-prof = [
"reth-node-core/jemalloc",
"jemalloc",
"reth-node-metrics/jemalloc-prof",
]
tracy-allocator = []
jemalloc-symbols = [
"jemalloc-prof",
"reth-node-metrics/jemalloc-symbols",
]
tracy-allocator = ["tracy"]
tracy = ["reth-tracing/tracy", "reth-node-core/tracy"]
# Because jemalloc is default and preferred over snmalloc when both features are
# enabled, `--no-default-features` should be used when enabling snmalloc or
@@ -81,3 +87,5 @@ min-trace-logs = [
"tracing/release_max_level_trace",
"reth-node-core/min-trace-logs",
]
edge = ["reth-cli-commands/edge"]

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

@@ -84,17 +84,15 @@ where
B: Block,
ChainSpec: EthChainSpec<Header = B::Header> + EthereumHardforks + Debug + Send + Sync,
{
type Error = ConsensusError;
fn validate_body_against_header(
&self,
body: &B::Body,
header: &SealedHeader<B::Header>,
) -> Result<(), Self::Error> {
) -> Result<(), ConsensusError> {
validate_body_against_header(body, header.header())
}
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), Self::Error> {
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError> {
validate_block_pre_execution(block, &self.chain_spec)
}
}
@@ -228,10 +226,12 @@ mod tests {
let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
let child = header_with_gas_limit((parent.gas_limit + 5) as u64);
assert_eq!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()),
Ok(())
);
assert!(validate_against_parent_gas_limit(
&child,
&parent,
&ChainSpec::<Header>::default()
)
.is_ok());
}
#[test]
@@ -239,10 +239,11 @@ mod tests {
let parent = header_with_gas_limit(MINIMUM_GAS_LIMIT);
let child = header_with_gas_limit(MINIMUM_GAS_LIMIT - 1);
assert_eq!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()),
Err(ConsensusError::GasLimitInvalidMinimum { child_gas_limit: child.gas_limit as u64 })
);
assert!(matches!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
ConsensusError::GasLimitInvalidMinimum { child_gas_limit }
if child_gas_limit == child.gas_limit as u64
));
}
#[test]
@@ -252,13 +253,11 @@ mod tests {
parent.gas_limit + parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR + 1,
);
assert_eq!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()),
Err(ConsensusError::GasLimitInvalidIncrease {
parent_gas_limit: parent.gas_limit,
child_gas_limit: child.gas_limit,
})
);
assert!(matches!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
ConsensusError::GasLimitInvalidIncrease { parent_gas_limit, child_gas_limit }
if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
));
}
#[test]
@@ -266,10 +265,12 @@ mod tests {
let parent = header_with_gas_limit(GAS_LIMIT_BOUND_DIVISOR * 10);
let child = header_with_gas_limit(parent.gas_limit - 5);
assert_eq!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()),
Ok(())
);
assert!(validate_against_parent_gas_limit(
&child,
&parent,
&ChainSpec::<Header>::default()
)
.is_ok());
}
#[test]
@@ -279,13 +280,11 @@ mod tests {
parent.gas_limit - parent.gas_limit / GAS_LIMIT_BOUND_DIVISOR - 1,
);
assert_eq!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()),
Err(ConsensusError::GasLimitInvalidDecrease {
parent_gas_limit: parent.gas_limit,
child_gas_limit: child.gas_limit,
})
);
assert!(matches!(
validate_against_parent_gas_limit(&child, &parent, &ChainSpec::<Header>::default()).unwrap_err(),
ConsensusError::GasLimitInvalidDecrease { parent_gas_limit, child_gas_limit }
if parent_gas_limit == parent.gas_limit && child_gas_limit == child.gas_limit
));
}
#[test]
@@ -300,9 +299,8 @@ mod tests {
..Default::default()
};
assert_eq!(
EthBeaconConsensus::new(chain_spec).validate_header(&SealedHeader::seal_slow(header,)),
Ok(())
);
assert!(EthBeaconConsensus::new(chain_spec)
.validate_header(&SealedHeader::seal_slow(header,))
.is_ok());
}
}

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
@@ -172,18 +170,16 @@ mod tests {
let expected_receipts_root = B256::random();
let expected_logs_bloom = calculated_logs_bloom;
assert_eq!(
assert!(matches!(
compare_receipts_root_and_logs_bloom(
calculated_receipts_root,
calculated_logs_bloom,
expected_receipts_root,
expected_logs_bloom
),
Err(ConsensusError::BodyReceiptRootDiff(
GotExpected { got: calculated_receipts_root, expected: expected_receipts_root }
.into()
))
);
).unwrap_err(),
ConsensusError::BodyReceiptRootDiff(diff)
if diff.got == calculated_receipts_root && diff.expected == expected_receipts_root
));
}
#[test]
@@ -194,16 +190,15 @@ mod tests {
let expected_receipts_root = calculated_receipts_root;
let expected_logs_bloom = Bloom::random();
assert_eq!(
assert!(matches!(
compare_receipts_root_and_logs_bloom(
calculated_receipts_root,
calculated_logs_bloom,
expected_receipts_root,
expected_logs_bloom
),
Err(ConsensusError::BodyBloomLogDiff(
GotExpected { got: calculated_logs_bloom, expected: expected_logs_bloom }.into()
))
);
).unwrap_err(),
ConsensusError::BodyBloomLogDiff(diff)
if diff.got == calculated_logs_bloom && diff.expected == expected_logs_bloom
));
}
}

View File

@@ -24,8 +24,10 @@ use crate::BuiltPayloadConversionError;
/// Contains the built payload.
///
/// According to the [engine API specification](https://github.com/ethereum/execution-apis/blob/main/src/engine/README.md) the execution layer should build the initial version of the payload with an empty transaction set and then keep update it in order to maximize the revenue.
/// Therefore, the empty-block here is always available and full-block will be set/updated
/// afterward.
///
/// This struct represents a single built block at a point in time. The payload building process
/// creates a sequence of these payloads, starting with an empty block and progressively including
/// more transactions.
#[derive(Debug, Clone)]
pub struct EthBuiltPayload<N: NodePrimitives = EthPrimitives> {
/// Identifier of the payload

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