Compare commits

...

172 Commits

Author SHA1 Message Date
Emma Jamieson-Hoare
f04db57d3a chore(ci): continue on error when codspeed is flakey/cancelled on main merge 2026-02-27 09:49:17 +00:00
Delweng
09adb83922 fix(engine/tree): continue sync-target progression for already-seen downloaded blocks (#22628)
Signed-off-by: Delweng <delweng@gmail.com>
2026-02-27 08:12:06 +00:00
Delweng
c12b6d4c90 fix(rpc): return -38003 for FCU beacon-root payloadAttributes mismatches (#22634)
Signed-off-by: Delweng <delweng@gmail.com>
2026-02-27 07:54:20 +00:00
Derek Cofausper
7a78044587 chore(libmdbx): fix MDB_ -> MDBX_ typos (#22630)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-27 06:06:07 +00:00
figtracer
f88538e033 refactor(net): add peers() accessors on Swarm to flatten accessor chains (#22616)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 05:35:14 +00:00
DaniPopes
63dff64b8a chore: simplify tx iterator (#22365) 2026-02-27 05:09:13 +00:00
DaniPopes
233590cefd chore: use better hasher for precompile cache (#22360) 2026-02-27 05:09:12 +00:00
Derek Cofausper
40962ef6fc chore(hive): remove engine-withdrawals from ignored tests (#22625)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-27 03:57:43 +00:00
github-actions[bot]
2f121b099b chore(deps): weekly cargo update (#22624)
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-27 03:36:42 +00:00
Delweng
0470050c05 fix(engine): continue downloading head block after making non-head sync target canonical (#22613)
Signed-off-by: Delweng <delweng@gmail.com>
2026-02-27 03:15:52 +00:00
MagicJoshh
cbc416b82a fix(rpc-provider): state_root delegates to stub that always returns zero (#22610) 2026-02-27 02:53:57 +00:00
MagicJoshh
3fddefbd38 fix(rpc): prevent u64 underflow when re-executing genesis block (#22532) 2026-02-27 02:48:59 +00:00
Julian Meyer
f97a6530c1 chore: make cached overlay fetch public (#22619) 2026-02-27 02:47:50 +00:00
Derek Cofausper
80e3e1c79d docs: add storage v2 guide (#22620)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Emma Jamieson-Hoare <emmajam@users.noreply.github.com>
2026-02-26 20:22:52 +00:00
Arsenii Kulikov
ee37c25a4b perf: use more multiproof workers (#22615) 2026-02-26 19:59:06 +00:00
Derek Cofausper
c01f9688e2 feat: add transaction iterator helpers to Chain (#22618)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 19:39:34 +00:00
bigbear
815a75833e refactor(exex): remove redundant update_capacity call (#22603) 2026-02-26 13:09:41 +00:00
cui
59c4e24296 fix(downloaders): reset metrics on clear (#21858) 2026-02-26 12:38:55 +00:00
Derek Cofausper
d5b5caa439 docs: add PR title and description guidelines to CLAUDE.md (#22602)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 12:20:54 +00:00
Julio
47f1999654 fix(net): abort discv4 and DNS discovery tasks on Discovery drop (#22590)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-26 10:37:57 +00:00
MergeBot
3ac5637bd1 chore(ci): fix collapsible_match clippy lint in chainspec (#22594)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 10:04:19 +00:00
Derek Cofausper
4cec99ed13 chore(bench): include core count in Slack notification when non-default (#22584)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 21:58:39 +00:00
Arsenii Kulikov
2f73835483 feat(reth-bench): support benchmarking via rlp blocks (#22581) 2026-02-25 20:28:47 +00:00
stevencartavia
ed20a40649 refactor(rpc): fetch block before tracing to avoid double lookups (#22503) 2026-02-25 20:17:45 +00:00
MergeBot
080a9cfc10 fix(rpc): add missing apply_pre_execution_changes in spawn_replay_transaction (#22575) 2026-02-25 20:04:02 +00:00
MergeBot
c4cd5c9b7b fix(rpc): add missing apply_pre_execution_changes in debug_traceCallMany (#22577) 2026-02-25 20:00:12 +00:00
Dan Cline
ce2a194fb7 feat(cli): add db stage-checkpoints command (#22579)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 19:58:59 +00:00
Vitalyr
6dcab51c97 fix(rpc): respect pending-block=none for provider blocks (#22556) 2026-02-25 19:45:42 +00:00
Derek Cofausper
4db23809cc fix(storage): return early in RocksDB healing when checkpoint is 0 (#22576)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 19:29:45 +00:00
Dan Cline
f84d5e6d7f chore: add Rjected as crates/cli codeowner (#22580) 2026-02-25 20:44:00 +01:00
Arsenii Kulikov
e63b6239d7 ci(bench): support configuring number of cores (#22573) 2026-02-25 17:28:35 +00:00
Matthias Seitz
660a0dee90 feat(net): persist richer peer metadata to peers file (#22557)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 17:03:25 +00:00
Arsenii Kulikov
f92c9b4370 perf: delay branch masks updates (#22565) 2026-02-25 15:35:12 +00:00
Brian Picciano
f0e2522294 perf: Remove unnecessary single-target storage proofs (#22539)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-02-25 14:35:23 +00:00
Matthias Seitz
7103088adc feat(txpool): support additional custom validation checks in EthTransactionValidator (#22559)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 13:32:21 +00:00
Derek Cofausper
663765af5c ci(bench): skip DM when results are posted to channel (#22563)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 13:18:25 +00:00
Zac Holme
20cfb2d517 fix: compute hashed post state in RpcBlockchainStateProvider (#22546)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 10:48:26 +00:00
Georgios Konstantopoulos
0bdf6e2f2e chore(engine): add debug log in spawned tx iterator after yielding tx index (#22558)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 08:49:33 +00:00
Georgios Konstantopoulos
85abd41824 perf: add thread-priority utils and boost engine/sparse-trie priority (#22541)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 08:33:35 +00:00
James Niken
70fb03a530 refactor(chainspec): use existing paris difficulty getter (#22474)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-25 05:39:12 +00:00
Georgios Konstantopoulos
96fce4dc4f chore: remove unmaintained shellexpand dependency (#22514)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 05:38:39 +00:00
Elaela Solis
728c7acd08 feat(exex): expose ExExManager buffer capacity in ExExLauncher (#22553) 2026-02-25 05:15:55 +00:00
stevencartavia
626c82db33 refactor(rpc): use replay_transactions_until in debug_trace_call_at_tx_index (#22542) 2026-02-25 05:00:48 +00:00
stevencartavia
624fcbd345 refactor(rpc): extract proof window validation into reusable helper (#22552) 2026-02-25 04:55:05 +00:00
Georgios Konstantopoulos
aed47bc3f8 fix(ci): add fallback for BENCH_JOB_URL in bench failure step (#22550)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 21:21:41 +00:00
Dan Cline
7680c1e4f6 fix: detect and remove stale CLI doc pages (#22433)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-24 19:34:55 +00:00
Dan Cline
93cb4068d2 fix: handle payload builder stream termination gracefully (#21710) 2026-02-24 19:24:24 +00:00
Georgios Konstantopoulos
2fba05dc67 feat(rpc): add reth_forkchoiceUpdated endpoint (#22536)
Co-authored-by: Arsenii Kulikov <klkvr@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 18:29:38 +00:00
Alexey Shekhirin
ea143d4d31 ci(bench): report panics and error logs in comments (#22544)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 18:19:22 +00:00
Matthias Seitz
fddb7dad10 feat(net): use fork_id as tiebreaker in peer selection (#22545)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 18:12:25 +00:00
Arsenii Kulikov
af6d674cac perf: decrease chunk size (#22527)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 17:56:39 +00:00
Georgios Konstantopoulos
de5688a76e perf(engine): remove spawn for prewarm pool init (#22543)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 17:35:20 +00:00
figtracer
d4cb91f0a5 perf(txpool): use BTree range queries in pending_txs_by_sender/queued_txs_by_sender (#22528)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 17:01:59 +00:00
Georgios Konstantopoulos
d122c7b49c chore(tasks): remove quanta upkeep from runtime (#22540)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 16:39:10 +00:00
Arsenii Kulikov
aed9014e1e chore: don't include spans for noops (#22538) 2026-02-24 16:21:38 +00:00
Arsenii Kulikov
d340114d52 refactor: don't return hashes for blinded nodes (#22535) 2026-02-24 16:08:40 +00:00
Georgios Konstantopoulos
7fc22f7b5b feat(rpc): accept RLP-encoded blocks in reth_newPayload (#22533)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-02-24 15:46:02 +00:00
Georgios Konstantopoulos
c8c5f8886d perf(engine): use rayon par_iter for tx prewarming instead of manual workers (#22521)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-02-24 15:42:06 +00:00
Arsenii Kulikov
2f3c8d7d03 feat(bench): enable --log.samply when samply is configred (#22526) 2026-02-24 13:03:19 +00:00
Georgios Konstantopoulos
a90f8be67b revert: "perf(trie): replace Box clone with unsafe reborrow in prune (#22516)" (#22525) 2026-02-24 12:14:22 +00:00
Georgios Konstantopoulos
7faca05344 refactor(engine): use spawn_blocking_named for tx_iterator thread (#22522)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 11:07:19 +00:00
Matthias Seitz
2827b0aca0 refactor: simplify uncle block fetching in RPC (#22523)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 12:11:33 +01:00
Georgios Konstantopoulos
d3bb2faf28 refactor(rpc): extract RethEngineApi into standalone struct (#22504)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 10:57:13 +00:00
Arsenii Kulikov
ef292ffa00 fix: don't produce both updates and removals for trie nodes (#22507)
Co-authored-by: Brian Picciano <me@mediocregopher.com>
2026-02-24 10:36:34 +00:00
Georgios Konstantopoulos
ea98d37bb3 ci: use native ARM runner for aarch64 linux release builds (#22519)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 09:16:38 +00:00
Emma Jamieson-Hoare
f2b3201187 chore(release-builds): remove the riscv builds from release pipeline (#22499) 2026-02-24 09:00:48 +00:00
Georgios Konstantopoulos
d1cbf6ca5a perf(trie): reserve capacity in apply_subtrie_update_actions (#22517)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 08:38:09 +00:00
Georgios Konstantopoulos
56bb47709c perf(trie): replace Box clone with unsafe reborrow in prune (#22516)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 08:22:52 +00:00
Elaela Solis
3703255d5d fix: make SerdeBincodeCompat generic for EthereumTxEnvelope (#22513) 2026-02-24 08:11:15 +00:00
DaniPopes
b431caf806 fix: avoid duplicate runtime initialization on startup (#22515) 2026-02-24 07:49:48 +00:00
Matthias Seitz
21dadb71c3 fix: update shellexpand to 3.1.2 and unpin nightly (#22506)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-23 21:42:58 +01:00
Arsenii Kulikov
98c45a4245 fix: remove debug_asssert! (#22505) 2026-02-23 20:13:51 +00:00
Arsenii Kulikov
ac2cc7b4e2 fix: proper SerdeBincodeCompat for EthereumReceipt (#22461) 2026-02-23 19:31:24 +00:00
Arsenii Kulikov
3931affcf2 revert: feat(rpc): move reth_newPayload from EngineApi to RethApi (#22500) 2026-02-23 18:33:59 +00:00
Alexey Shekhirin
93b7ae9286 chore(storage): propagate span context across rayon thread boundaries (#22497)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-23 18:17:18 +00:00
Emma Jamieson-Hoare
7e7717bdaa chore: release 1.11.1 (#22496)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-23 18:07:50 +00:00
Georgios Konstantopoulos
815037e27d feat(storage): slot preimage DB for plain changeset keys in v2 (#22379)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 18:01:44 +00:00
Georgios Konstantopoulos
80bf5532ac perf(trie): pack StoredNibblesSubKey from 65→33 bytes, generic cursor factory (#22158)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
2026-02-23 17:02:43 +00:00
Arsenii Kulikov
028e99191a perf: optimize sparse trie (#22418)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
Co-authored-by: Brian Picciano <me@mediocregopher.com>
2026-02-23 16:18:45 +00:00
Georgios Konstantopoulos
dc35fc8251 feat(rpc): move reth_newPayload from EngineApi to RethApi (#22425)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-02-23 15:43:20 +00:00
Georgios Konstantopoulos
285c325d71 feat(re-execute): work-stealing parallelization (#22242)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-02-23 15:39:24 +00:00
Arsenii Kulikov
ca47a7e9f9 fix: overlay preparation on tokio (#22492) 2026-02-23 15:37:55 +00:00
MergeBot
6d718d0c21 fix(rpc): use actual configured limit in trace_filter (#22477) 2026-02-23 13:14:19 +00:00
YK
949111c953 perf(engine): precompute tx root during payload validation (#22489) 2026-02-23 10:35:22 +00:00
Georgios Konstantopoulos
742eb56949 perf(engine): add tracing spans for post-execution validation wait times (#22483)
Co-authored-by: Yong Kang <yongkangc@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: YK <chiayongkang@hotmail.com>
2026-02-23 09:56:09 +00:00
Matthias Seitz
4af4836ec1 ci: pin nightly to 2026-02-21 (#22485)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-23 10:26:08 +01:00
figtracer
3bc71e7ec0 chore: use ValidPoolTransaction methods instead of reaching into inner field (#22475)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:15:49 +01:00
VolodymyrBg
03fbb6cafe fix(rpc): stop IPC handle in AuthServerHandle::stop() (#22467) 2026-02-22 07:56:52 +01:00
Alexey Shekhirin
b09b097a0b chore(ci): enhance benchmark artifact collection (#22457)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-21 13:43:59 +00:00
MergeBot
0fffdcdd23 fix(tracing): handle file_writer in LogFmt format (#22429) 2026-02-21 09:12:32 +00:00
strmfos
bc33eb764a fix(txpool): prevent underflow in blobstore versioned hash lookup (#22454)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-21 08:54:09 +00:00
Georgios Konstantopoulos
190157636e chore: remove unused Default impl for ExecutionEnv (#22451)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-21 08:29:59 +00:00
figtracer
8e3bc6567c chore(txpool): use to_consensus helper instead of reaching into inner field (#22426)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 08:28:57 +00:00
Georgios Konstantopoulos
45b961c7b3 chore: deprecate reth-primitives crate (#22450)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-21 08:16:34 +00:00
stevencartavia
94818d7676 feat(rpc): add reth_getBlockExecutionOutcome endpoint (#22397)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-21 05:47:05 +00:00
Alexey Shekhirin
4c2a9a9b4a feat(bench): add Slack notifications with Block Kit (#22447)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 00:09:07 +00:00
Alexey Shekhirin
76c37f0f80 ci(bench): install all runner dependencies from job (#22445)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 21:54:45 +00:00
figtracer
0275ff35fd refactor(net): add methods to PropagatedTransactions instead of exposing .0 (#22441)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-20 20:51:20 +00:00
Alexey Shekhirin
3f011c8328 ci(bench): add median lines to benchmark charts (#22439)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 20:40:32 +00:00
figtracer
beac28dbb2 chore(payload): use Transaction::blob_versioned_hashes() directly (#22440)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-20 18:59:06 +00:00
Alexey Shekhirin
bce100c6c8 ci(bench): add samply profiling support (#22432)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 18:16:28 +00:00
Alexey Shekhirin
40e99a4a4f ci(bench): switch to @decofe bot and new secret names (#22434)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 17:55:16 +00:00
Dan Cline
1ff88e43cd fix: handle missing rocksdb gracefully in read-only db commands (#22394)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 17:18:43 +00:00
joshieDo
d23c244cd1 fix: align static-file changeset checksum with MDBX semantics (#22389)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 17:11:08 +00:00
Dan Cline
3de9259026 docs: add CLI docs regeneration guide (#22395)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 16:46:18 +00:00
Alexey Shekhirin
d24f0b1e05 fix: update PR comment when bench workflow is cancelled (#22430)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 15:20:10 +00:00
iPLAY888
bb1b9ec611 fix(net): log transaction count instead of bool in broadcast debug log (#22417) 2026-02-20 12:15:40 +00:00
Arsenii Kulikov
70cab0d163 fix: properly reveal trie nodes (#22415) 2026-02-20 11:51:04 +00:00
Alexey Shekhirin
e530b1f6a1 refactor(bench): push charts to external repo instead of bench-charts branch (#22414)
Co-authored-by: Alexey Shekhirin <shekhirin@shekhirin-tempo.tail388b2e.ts.net>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 11:42:15 +00:00
Emma Jamieson-Hoare
ff5d375526 docs(hive): add comments explaining why flaky tests are ignored (#22383)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-20 11:18:20 +00:00
YK
d1a92afb57 feat(engine): add sub-phase timing histograms for sparse trie event loop (#22368)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-20 06:56:04 +00:00
Dan Cline
0517c12c90 docs: remove stale db settings subcommands (#22396)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-20 05:34:01 +00:00
YK
237eb1675c perf(trie): cache pending sparse trie target count (#22355) 2026-02-20 04:38:13 +00:00
Arsenii Kulikov
b6bcd7e6bd fix: catch panics of named tasks (#22386) 2026-02-19 22:32:02 +00:00
Alexey Shekhirin
48122300d7 fix(bench): validate cached binaries match expected commit SHA (#22392)
Co-authored-by: Alexey Shekhirin <shekhirin@shekhirin-tempo.tail388b2e.ts.net>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-19 21:35:11 +00:00
Alexey Shekhirin
13f214f160 ci(bench): use schelk promote instead of recover when updating snapshot (#22391)
Co-authored-by: Alexey Shekhirin <shekhirin@shekhirin-tempo.tail388b2e.ts.net>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-19 21:29:38 +00:00
Georgios Konstantopoulos
f17592670d fix(bench): checkout feature source to correct ref instead of symlinking (#22390)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 20:42:21 +00:00
Alexey Shekhirin
c225132b81 ci(bench): drop root privileges for reth-bench (#22380) 2026-02-19 14:46:52 +00:00
radik878
dcc5d9ec30 fix(events): handle PipelineEvent::Unwound to clean up current_stage (#22340) 2026-02-19 13:48:57 +00:00
Alexey Shekhirin
6cd56b645b ci(bench): support running benchmarks on closed/merged PRs (#22378) 2026-02-19 13:16:03 +00:00
Emma Jamieson-Hoare
794dbff26e ci(hive): remove EIP-6110 deposit tests from expected failures (now passing) (#22377)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 12:56:36 +00:00
Emma Jamieson-Hoare
fcfbed0bbc ci(hive): ignore flaky reorg and sync timeout tests (#22376)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 12:55:20 +00:00
Alexey Shekhirin
70bcd475fe ci(bench): ABBA run order (#22335) 2026-02-19 12:40:44 +00:00
Emma Jamieson-Hoare
cd6e895a97 fix(rpc): return -32602 for PayloadAttributes structure validation errors (#22374)
Co-authored-by: yongkangc <chiayongkang@hotmail.com>
2026-02-19 12:32:31 +00:00
Emma Jamieson-Hoare
6552a3a9ab ci(hive): fix eels runner OOM crashes (#22373)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 12:04:22 +00:00
Derek Cofausper
6a91089542 ci(bench): fix cleanup to use sudo pkill and lazy unmount (#22372)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-02-19 11:23:03 +00:00
YK
a9a1e504b4 refactor(trie): simplify encode_account_leaf_value (#22366) 2026-02-19 10:36:44 +00:00
YK
e280f25885 feat(trie): expose storage_wait_time as dedicated Prometheus metric (#22359) 2026-02-19 10:36:26 +00:00
Arsenii Kulikov
37c4f908fa perf: store blinded node hashes on SparseNode::Branch (#22290)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-02-19 09:34:42 +00:00
Georgios Konstantopoulos
a157be3f3b perf(tasks): add LazyHandle<T>, use for hash post state (#22347)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 08:48:24 +00:00
Georgios Konstantopoulos
e0eb306b2b chore(engine): rename finish span to BlockExecutor::finish (#22356)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 07:29:18 +00:00
Micke
7f4f3f1eb9 fix(prune): correct deleted entries count when skip_filter is used (#22312) 2026-02-19 06:19:02 +00:00
Georgios Konstantopoulos
8970f82aaf perf(engine): prefetch first txs sequentially to avoid rayon scheduling stall (#22305)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-02-19 05:53:52 +00:00
Georgios Konstantopoulos
8529da976f fix(cli): store extradata as Bytes, decode hex in parser (#22344)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 04:38:16 +00:00
stevencartavia
8fa539225b refactor: remove duplicate apply_pre_execution_changes from Trace trait (#22333) 2026-02-19 04:32:42 +00:00
Doohyun Cho
93d546a36d perf(trie): preserve allocations in sparse trie wipe() (#21089) 2026-02-19 04:02:20 +00:00
zhygis
5c83eb0b06 feat(log): disable file logging by default for non-node commands (#21521)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
2026-02-19 03:16:47 +00:00
Georgios Konstantopoulos
cd32e3cc05 feat(reth-bench): add prometheus metrics scraper (#22244)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-02-19 03:13:40 +00:00
MergeBot
26470cadfc perf(trie): remove redundant HashMap lookup in sparse trie account state query (#22328)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 03:09:04 +00:00
Brian Picciano
506ab806e4 fix: propagate trie update diff result to trigger debug recorder writes (#22331)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 02:52:43 +00:00
Forostovec
c2e846093e fix(net): use continue instead of return in buffer_hashes loop (#22337) 2026-02-19 02:46:33 +00:00
dependabot[bot]
5df22b12d8 chore(deps): bump actions/upload-artifact from 4 to 6 (#22338)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 02:33:00 +00:00
dependabot[bot]
ff9700bb3b chore(deps): bump actions/github-script from 7 to 8 (#22339)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 02:32:57 +00:00
Georgios Konstantopoulos
85d35fa6c0 feat(tasks): add WorkerMap for named single-thread workers (#22262)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 02:27:02 +00:00
Georgios Konstantopoulos
47544d9a7e fix(txpool): ensure transactions are added to pending subpool in nonce order (#22308)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-18 20:31:04 +00:00
Alexey Shekhirin
ef33961aff ci(bench): download snapshot in parallel with builds (#22332) 2026-02-18 17:40:17 +00:00
Georgios Konstantopoulos
0e01a694a7 fix(storage): clarify storage settings mismatch warning (#22330)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 16:35:28 +00:00
Alexey Shekhirin
ee19320ee8 ci(bench): use ABBA run order to reduce variance (#22321) 2026-02-18 15:33:31 +00:00
Alexey Shekhirin
9251997c1f ci(bench): build baseline and feature binaries in parallel (#22323) 2026-02-18 14:30:58 +00:00
Brian Picciano
302993b45a feat(trie-debug): record SetRoot op in ParallelSparseTrie::set_root (#22324)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 14:03:42 +00:00
Brian Picciano
8d97ab63c6 perf: use stack-allocated [u8; 65] for StoredNibblesSubKey encoding (#22314)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-02-18 13:35:20 +00:00
Matthias Seitz
251f83ab0b refactor: replace TryFrom*Response traits with unified RpcResponseConverter (#22320)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 13:32:47 +00:00
Alexey Shekhirin
e6e0dde903 ci(bench): queue reth-bench jobs and report queue position in PR comment (#22318) 2026-02-18 12:53:12 +00:00
Georgios Konstantopoulos
b1b51261af feat(ci): granular status updates for reth-bench workflow (#22297)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 12:05:59 +00:00
Georgios Konstantopoulos
2ae5ef475e feat(ci): add workflow_dispatch trigger for reth-bench (#22298)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 11:22:22 +00:00
drhgencer
8861e2724f fix(txpool): notify subscribers when set_block_info promotes transaction (#22243)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 09:08:16 +00:00
Georgios Konstantopoulos
734ec4ffe6 feat(engine): add tracing spans to execute_block setup (#22304)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 06:23:24 +00:00
Georgios Konstantopoulos
cbcdf8dac0 chore(tracing): use underscores instead of spaces in span names (#22307)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 06:21:04 +00:00
Georgios Konstantopoulos
826e387c87 refactor(rpc): use ..Default::default() for SimCallResult initialization (#22309)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 05:42:22 +00:00
Forostovec
1c40188993 fix: correct message ID in NodeData version error (#22291) 2026-02-18 05:02:33 +00:00
Matthias Seitz
49a2df0d7a chore: bump alloy deps 1.7.1 -> 1.7.3 (#22296)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 06:02:04 +01:00
DaniPopes
a1d1b6def6 fix: prevent ANSI escape codes leaking into Tracy zone text (#22306) 2026-02-18 03:49:34 +00:00
Georgios Konstantopoulos
56bbb3ce2c feat(cli): add reth db prune-checkpoints command (#22288)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 01:25:53 +00:00
Georgios Konstantopoulos
5b1010322c docs: clarify StateWriteConfig is about database (MDBX) writes vs static files (#22299)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 01:13:31 +00:00
Georgios Konstantopoulos
a195b777eb perf(storage): skip plain state conversion in write_state for storage v2 (#22294)
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
2026-02-18 00:48:06 +00:00
Georgios Konstantopoulos
5045e6ef8b feat(bench): add wait time breakdown tables to CI report (#22293) 2026-02-17 23:44:03 +00:00
Alexey Shekhirin
b49cadb346 ci(bench): rename main/branch to baseline/feature, add ref args (#22284)
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 23:00:01 +00:00
Georgios Konstantopoulos
aeb2c6e731 chore(primitives): remove legacy transaction roundtrip tests (#22292)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 21:15:14 +00:00
stevencartavia
477fed7a11 refactor(primitives): use alloy's EthereumReceipt type (#22254)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 20:30:52 +00:00
MergeBot
59993b974a fix(rpc): resolve AtBlockHash to single block in eth_getFilterChanges (#22283)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 20:15:33 +00:00
Georgios Konstantopoulos
9ecef47aff fix(provider): skip sender pruning during reorg when sender_recovery is full (#22271)
Co-authored-by: Amp <amp@ampcode.com>
2026-02-17 19:15:03 +00:00
DaniPopes
0ba685386d refactor: dedup runtime initializations (#22263)
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
2026-02-17 17:35:31 +00:00
412 changed files with 12850 additions and 9066 deletions

View File

@@ -0,0 +1,5 @@
---
reth-transaction-pool: minor
---
Added support for optional custom stateless and stateful validation hooks in `EthTransactionValidator` via `set_additional_stateless_validation` and `set_additional_stateful_validation` methods. Also implemented a manual `Debug` impl to handle the non-`Debug` function pointer fields.

View File

@@ -0,0 +1,5 @@
---
reth-trie-sparse: patch
---
Added recording of `SetRoot` operation in `ParallelSparseTrie::set_root` when the `trie-debug` feature is enabled.

View File

@@ -0,0 +1,6 @@
---
reth-rpc-convert: minor
reth-storage-rpc-provider: minor
---
Replaced the separate `TryFromBlockResponse`, `TryFromReceiptResponse`, and `TryFromTransactionResponse` traits with a unified `RpcResponseConverter` trait and default `EthRpcConverter` implementation. Removed the `op-alloy-network` dependency and refactored `RpcBlockchainProvider` to store a dynamic converter instance instead of relying on per-type trait bounds.

View File

@@ -0,0 +1,5 @@
---
reth-engine-tree: patch
---
Added sub-phase timing histograms to the sparse trie event loop, tracking channel wait, proof coalescing, multiproof reveal, and trie update durations separately.

View File

@@ -0,0 +1,5 @@
---
reth-engine-tree: patch
---
Fixed `compare_trie_updates` to return `bool` indicating whether differences were found, and updated the caller to properly use the return value instead of treating all successful comparisons as having no differences.

View File

@@ -0,0 +1,5 @@
---
reth-db-api: patch
---
Changed `StoredNibblesSubKey` encoding to use a stack-allocated `[u8; 65]` array instead of a heap-allocated `Vec<u8>`, avoiding unnecessary heap allocation.

View File

@@ -0,0 +1,5 @@
---
reth-provider: patch
---
Fixed sender pruning during block reorg to skip when sender_recovery is fully pruned, preventing a fatal crash when no sender data exists in static files.

View File

@@ -0,0 +1,7 @@
---
reth-network-types: minor
reth-network: minor
reth-node-core: patch
---
Added `PersistedPeerInfo` struct to persist richer peer metadata (kind, fork ID, reputation) to disk. Updated `PeersConfig::with_basic_nodes_from_file` to support both the new `PersistedPeerInfo` format and the legacy `Vec<NodeRecord>` format with automatic conversion, and updated `write_peers_to_file` to exclude backed-off and banned peers.

View File

@@ -0,0 +1,5 @@
---
reth-network: minor
---
Added `fork_id` as a tiebreaker in peer selection when reputations are equal, preferring peers with a discovered `fork_id` as it indicates fork compatibility. Added a test to verify the tiebreaker behavior.

View File

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

View File

@@ -0,0 +1,5 @@
---
reth-transaction-pool: patch
---
Fixed a bug where transactions from the same sender were added to the pending subpool out of nonce order. Ensured `process_updates` runs before `add_new_transaction` so that lower-nonce promotions are enqueued before the newly inserted higher-nonce transaction, preserving correct ordering for live `BestTransactions` iterators.

2
.github/CODEOWNERS vendored
View File

@@ -1,7 +1,7 @@
* @gakonst
crates/chain-state/ @fgimenez @mattsse
crates/chainspec/ @Rjected @joshieDo @mattsse
crates/cli/ @mattsse
crates/cli/ @mattsse @Rjected
crates/config/ @shekhirin @mattsse @Rjected
crates/consensus/ @mattsse @Rjected
crates/e2e-test-utils/ @mattsse @Rjected @klkvr @fgimenez

View File

@@ -2,55 +2,88 @@
#
# Builds (or fetches from cache) reth binaries for benchmarking.
#
# Usage: bench-reth-build.sh <main|branch> <commit> [branch-sha]
# Usage: bench-reth-build.sh <baseline|feature> <source-dir> <commit> [branch-sha]
#
# main — build/fetch the baseline binary at <commit> (merge-base)
# branch — build/fetch the candidate binary + reth-bench at <commit>
# optional branch-sha is the PR head commit for cache key
# baseline — build/fetch the baseline binary at <commit> (merge-base)
# source-dir must be checked out at <commit>
# feature — build/fetch the candidate binary + reth-bench at <commit>
# source-dir must be checked out at <commit>
# optional branch-sha is the PR head commit for cache key
#
# Outputs:
# main: target/profiling-baseline/reth
# branch: target/profiling/reth, reth-bench installed to cargo bin
# baseline: <source-dir>/target/profiling/reth
# feature: <source-dir>/target/profiling/reth, reth-bench installed to cargo bin
#
# Required: mc (MinIO client) configured at /home/ubuntu/.mc
# Required: mc (MinIO client) with a configured alias
set -euo pipefail
MC="mc --config-dir /home/ubuntu/.mc"
MC="mc"
MODE="$1"
COMMIT="$2"
SOURCE_DIR="$2"
COMMIT="$3"
# Verify a cached reth binary was built from the expected commit.
# `reth --version` outputs "Commit SHA: <full-sha>" on its own line.
verify_binary() {
local binary="$1" expected_commit="$2"
local version binary_sha
version=$("$binary" --version 2>/dev/null) || return 1
binary_sha=$(echo "$version" | sed -n 's/^Commit SHA: *//p')
if [ -z "$binary_sha" ]; then
echo "Warning: could not extract commit SHA from version output"
return 1
fi
if [ "$binary_sha" = "$expected_commit" ]; then
return 0
fi
echo "Cache mismatch: binary built from ${binary_sha} but expected ${expected_commit}"
return 1
}
case "$MODE" in
main)
baseline|main)
BUCKET="minio/reth-binaries/${COMMIT}"
mkdir -p target/profiling-baseline
mkdir -p "${SOURCE_DIR}/target/profiling"
CACHE_VALID=false
if $MC stat "${BUCKET}/reth" &>/dev/null; then
echo "Cache hit for main (${COMMIT}), downloading binary..."
$MC cp "${BUCKET}/reth" target/profiling-baseline/reth
chmod +x target/profiling-baseline/reth
else
echo "Cache miss for main (${COMMIT}), building from source..."
CURRENT_REF=$(git rev-parse HEAD)
git checkout "${COMMIT}"
echo "Cache hit for baseline (${COMMIT}), downloading binary..."
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
chmod +x "${SOURCE_DIR}/target/profiling/reth"
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
CACHE_VALID=true
else
echo "Cached baseline binary is stale, rebuilding..."
fi
fi
if [ "$CACHE_VALID" = false ]; then
echo "Building baseline (${COMMIT}) from source..."
cd "${SOURCE_DIR}"
cargo build --profile profiling --bin reth
cp target/profiling/reth target/profiling-baseline/reth
$MC cp target/profiling-baseline/reth "${BUCKET}/reth"
git checkout "${CURRENT_REF}"
$MC cp target/profiling/reth "${BUCKET}/reth"
fi
;;
branch)
BRANCH_SHA="${3:-$COMMIT}"
feature|branch)
BRANCH_SHA="${4:-$COMMIT}"
BUCKET="minio/reth-binaries/${BRANCH_SHA}"
CACHE_VALID=false
if $MC stat "${BUCKET}/reth" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
echo "Cache hit for ${BRANCH_SHA}, downloading binaries..."
mkdir -p target/profiling
$MC cp "${BUCKET}/reth" target/profiling/reth
mkdir -p "${SOURCE_DIR}/target/profiling"
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
$MC cp "${BUCKET}/reth-bench" /home/ubuntu/.cargo/bin/reth-bench
chmod +x target/profiling/reth /home/ubuntu/.cargo/bin/reth-bench
else
echo "Cache miss for ${BRANCH_SHA}, building from source..."
chmod +x "${SOURCE_DIR}/target/profiling/reth" /home/ubuntu/.cargo/bin/reth-bench
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
CACHE_VALID=true
else
echo "Cached feature binary is stale, rebuilding..."
fi
fi
if [ "$CACHE_VALID" = false ]; then
echo "Building feature (${COMMIT}) from source..."
cd "${SOURCE_DIR}"
rustup show active-toolchain || rustup default stable
make profiling
make install-reth-bench
@@ -60,7 +93,7 @@ case "$MODE" in
;;
*)
echo "Usage: $0 <main|branch> <commit> [branch-sha]"
echo "Usage: $0 <baseline|feature> <source-dir> <commit> [branch-sha]"
exit 1
;;
esac

View File

@@ -53,7 +53,7 @@ def parse_combined_csv(path: str) -> list[dict]:
def plot_latency_and_throughput(
feature: list[dict], baseline: list[dict] | None, out: Path,
baseline_name: str = "main", branch_name: str = "branch",
baseline_name: str = "baseline", feature_name: str = "feature",
):
num_plots = 3 if baseline else 2
fig, axes = plt.subplots(num_plots, 1, figsize=(12, 4 * num_plots), sharex=True)
@@ -73,22 +73,24 @@ def plot_latency_and_throughput(
for r in baseline:
lat_s = r["new_payload_latency_us"] / 1_000_000
base_ggas.append(r["gas_used"] / lat_s / GIGAGAS if lat_s > 0 else 0)
ax1.plot(base_x, base_lat, linewidth=0.8, label=baseline_name, alpha=0.7)
ax2.plot(base_x, base_ggas, linewidth=0.8, label=baseline_name, alpha=0.7)
l, = ax1.plot(base_x, base_lat, linewidth=0.8, label=baseline_name, alpha=0.7)
ax1.axhline(np.median(base_lat), color=l.get_color(), linestyle="--", linewidth=1, alpha=0.7, label=f"{baseline_name} median")
l, = ax2.plot(base_x, base_ggas, linewidth=0.8, label=baseline_name, alpha=0.7)
ax2.axhline(np.median(base_ggas), color=l.get_color(), linestyle="--", linewidth=1, alpha=0.7, label=f"{baseline_name} median")
ax1.plot(feat_x, feat_lat, linewidth=0.8, label=branch_name)
l, = ax1.plot(feat_x, feat_lat, linewidth=0.8, label=feature_name)
ax1.axhline(np.median(feat_lat), color=l.get_color(), linestyle="--", linewidth=1, label=f"{feature_name} median")
ax1.set_ylabel("Latency (ms)")
ax1.set_title("newPayload Latency per Block")
ax1.grid(True, alpha=0.3)
if baseline:
ax1.legend()
ax1.legend()
ax2.plot(feat_x, feat_ggas, linewidth=0.8, label=branch_name)
l, = ax2.plot(feat_x, feat_ggas, linewidth=0.8, label=feature_name)
ax2.axhline(np.median(feat_ggas), color=l.get_color(), linestyle="--", linewidth=1, label=f"{feature_name} median")
ax2.set_ylabel("Ggas/s")
ax2.set_title("Execution Throughput per Block")
ax2.grid(True, alpha=0.3)
if baseline:
ax2.legend()
ax2.legend()
if baseline:
ax3 = axes[2]
@@ -105,7 +107,7 @@ def plot_latency_and_throughput(
ax3.bar(blocks, diffs, width=1.0, color=colors, alpha=0.7, edgecolor="none")
ax3.axhline(0, color="black", linewidth=0.5)
ax3.set_ylabel("Δ Latency (%)")
ax3.set_title("Per-Block newPayload Latency Change (branch vs main)")
ax3.set_title("Per-Block newPayload Latency Change (feature vs baseline)")
ax3.grid(True, alpha=0.3, axis="y")
axes[-1].set_xlabel("Block Number")
@@ -116,7 +118,7 @@ def plot_latency_and_throughput(
def plot_wait_breakdown(
feature: list[dict], baseline: list[dict] | None, out: Path,
baseline_name: str = "main", branch_name: str = "branch",
baseline_name: str = "baseline", feature_name: str = "feature",
):
series = [
("Persistence Wait", "persistence_wait_us"),
@@ -135,7 +137,7 @@ def plot_wait_breakdown(
fx = [r["block_number"] for r in feature if r[key] is not None]
fy = [r[key] / 1_000 for r in feature if r[key] is not None]
if fx:
ax.plot(fx, fy, linewidth=0.8, label=branch_name)
ax.plot(fx, fy, linewidth=0.8, label=feature_name)
ax.set_ylabel("ms")
ax.set_title(label)
@@ -163,7 +165,7 @@ def _add_regression(ax, x, y, color, label):
def plot_gas_vs_latency(
feature: list[dict], baseline: list[dict] | None, out: Path,
baseline_name: str = "main", branch_name: str = "branch",
baseline_name: str = "baseline", feature_name: str = "feature",
):
fig, ax = plt.subplots(figsize=(8, 6))
@@ -176,7 +178,7 @@ def plot_gas_vs_latency(
fgas = [r["gas_used"] / 1_000_000 for r in feature]
flat = [r["new_payload_latency_us"] / 1_000 for r in feature]
ax.scatter(fgas, flat, s=8, alpha=0.6)
_add_regression(ax, fgas, flat, "tab:orange", branch_name)
_add_regression(ax, fgas, flat, "tab:orange", feature_name)
ax.set_xlabel("Gas Used (Mgas)")
ax.set_ylabel("newPayload Latency (ms)")
@@ -188,30 +190,56 @@ def plot_gas_vs_latency(
plt.close(fig)
def merge_csvs(paths: list[str]) -> list[dict]:
"""Parse and merge multiple CSVs, averaging values for duplicate blocks."""
by_block: dict[int, list[dict]] = {}
for path in paths:
for row in parse_combined_csv(path):
by_block.setdefault(row["block_number"], []).append(row)
merged = []
for bn in sorted(by_block):
rows = by_block[bn]
if len(rows) == 1:
merged.append(rows[0])
else:
avg = {"block_number": bn}
for key in ("gas_used", "new_payload_latency_us"):
avg[key] = int(sum(r[key] for r in rows) / len(rows))
for key in ("persistence_wait_us", "execution_cache_wait_us", "sparse_trie_wait_us"):
vals = [r[key] for r in rows if r[key] is not None]
avg[key] = int(sum(vals) / len(vals)) if vals else None
merged.append(avg)
return merged
def main():
parser = argparse.ArgumentParser(description="Generate benchmark charts")
parser.add_argument("combined_csv", help="Path to combined_latency.csv (feature)")
parser.add_argument(
"--feature", nargs="+", required=True,
help="Path(s) to feature combined_latency.csv",
)
parser.add_argument(
"--output-dir", required=True, help="Output directory for PNG charts"
)
parser.add_argument(
"--baseline", help="Path to baseline (main) combined_latency.csv"
"--baseline", nargs="+", help="Path(s) to baseline combined_latency.csv"
)
parser.add_argument("--baseline-name", default="main", help="Label for baseline")
parser.add_argument("--branch-name", default="branch", help="Label for branch")
parser.add_argument("--baseline-name", default="baseline", help="Label for baseline")
parser.add_argument("--feature-name", "--branch-name", default="feature", help="Label for feature")
args = parser.parse_args()
feature = parse_combined_csv(args.combined_csv)
feature = merge_csvs(args.feature)
if not feature:
print("No results found in combined CSV", file=sys.stderr)
print("No results found in feature CSV(s)", file=sys.stderr)
sys.exit(1)
baseline = None
if args.baseline:
baseline = parse_combined_csv(args.baseline)
baseline = merge_csvs(args.baseline)
if not baseline:
print(
"Warning: no results in baseline CSV, skipping comparison",
"Warning: no results in baseline CSV(s), skipping comparison",
file=sys.stderr,
)
baseline = None
@@ -220,7 +248,7 @@ def main():
out_dir.mkdir(parents=True, exist_ok=True)
bname = args.baseline_name
fname = args.branch_name
fname = args.feature_name
plot_latency_and_throughput(feature, baseline, out_dir / "latency_throughput.png", bname, fname)
plot_wait_breakdown(feature, baseline, out_dir / "wait_breakdown.png", bname, fname)
plot_gas_vs_latency(feature, baseline, out_dir / "gas_vs_latency.png", bname, fname)

View File

@@ -12,19 +12,45 @@ LABEL="$1"
BINARY="$2"
OUTPUT_DIR="$3"
DATADIR="$SCHELK_MOUNT/datadir"
LOG="/tmp/reth-bench-node-${LABEL}.log"
mkdir -p "$OUTPUT_DIR"
LOG="${OUTPUT_DIR}/node.log"
cleanup() {
kill "$TAIL_PID" 2>/dev/null || true
if [ -n "${RETH_PID:-}" ] && sudo kill -0 "$RETH_PID" 2>/dev/null; then
sudo kill "$RETH_PID"
for i in $(seq 1 30); do
sudo kill -0 "$RETH_PID" 2>/dev/null || break
sleep 1
done
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
# Send SIGINT to the inner reth process by exact name (not -f which
# would also match samply's cmdline containing "reth"). Samply will
# capture reth's exit and save the profile.
sudo pkill -INT -x reth 2>/dev/null || true
# Wait for samply to finish writing the profile and exit
for i in $(seq 1 120); do
sudo pgrep -x samply > /dev/null 2>&1 || break
if [ $((i % 10)) -eq 0 ]; then
echo "Waiting for samply to finish writing profile... (${i}s)"
fi
sleep 1
done
if sudo pgrep -x samply > /dev/null 2>&1; then
echo "Samply still running after 120s, sending SIGTERM..."
sudo pkill -x samply 2>/dev/null || true
fi
else
sudo kill "$RETH_PID"
for i in $(seq 1 30); do
sudo kill -0 "$RETH_PID" 2>/dev/null || break
sleep 1
done
fi
sudo kill -9 "$RETH_PID" 2>/dev/null || true
sleep 1
fi
# Fix ownership of reth-created files (reth runs as root)
sudo chown -R "$(id -un):$(id -gn)" "$OUTPUT_DIR" 2>/dev/null || true
if mountpoint -q "$SCHELK_MOUNT"; then
sudo umount -l "$SCHELK_MOUNT" || true
sudo schelk recover -y || true
fi
mountpoint -q "$SCHELK_MOUNT" && sudo schelk recover -y || true
}
TAIL_PID=
trap cleanup EXIT
@@ -41,18 +67,38 @@ grep Cached /proc/meminfo
# CPU layout: core 0 = OS/IRQs/reth-bench/aux, cores 1+ = reth node
RETH_BENCH="$(which reth-bench)"
ONLINE=$(nproc --all)
RETH_CPUS="1-$(( ONLINE - 1 ))"
sudo taskset -c "$RETH_CPUS" nice -n -20 "$BINARY" node \
--datadir "$DATADIR" \
--engine.accept-execution-requests-hash \
--http \
--http.port 8545 \
--ws \
--ws.api all \
--authrpc.port 8551 \
--disable-discovery \
--no-persist-peers \
> "$LOG" 2>&1 &
MAX_RETH=$(( ONLINE - 1 ))
if [ "${BENCH_CORES:-0}" -gt 0 ] && [ "$BENCH_CORES" -lt "$MAX_RETH" ]; then
MAX_RETH=$BENCH_CORES
fi
RETH_CPUS="1-${MAX_RETH}"
RETH_ARGS=(
node
--datadir "$DATADIR"
--log.file.directory "$OUTPUT_DIR/reth-logs"
--engine.accept-execution-requests-hash
--http
--http.port 8545
--ws
--ws.api all
--authrpc.port 8551
--disable-discovery
--no-persist-peers
)
if [ "${BENCH_SAMPLY:-false}" = "true" ]; then
RETH_ARGS+=(--log.samply)
SAMPLY="$(which samply)"
sudo taskset -c "$RETH_CPUS" nice -n -20 \
"$SAMPLY" record --save-only --presymbolicate --rate 10000 \
--output "$OUTPUT_DIR/samply-profile.json.gz" \
-- "$BINARY" "${RETH_ARGS[@]}" \
> "$LOG" 2>&1 &
else
sudo taskset -c "$RETH_CPUS" nice -n -20 "$BINARY" "${RETH_ARGS[@]}" \
> "$LOG" 2>&1 &
fi
RETH_PID=$!
stdbuf -oL tail -f "$LOG" | sed -u "s/^/[reth] /" &
@@ -74,8 +120,12 @@ for i in $(seq 1 60); do
sleep 1
done
# Run reth-bench with high priority but as the current user so output
# files are not root-owned (avoids EACCES on next checkout).
BENCH_NICE="sudo nice -n -20 sudo -u $(id -un)"
# Warmup
sudo nice -n -20 "$RETH_BENCH" new-payload-fcu \
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
--rpc-url "$BENCH_RPC_URL" \
--engine-rpc-url http://127.0.0.1:8551 \
--jwt-secret "$DATADIR/jwt.hex" \
@@ -83,7 +133,7 @@ sudo nice -n -20 "$RETH_BENCH" new-payload-fcu \
--reth-new-payload 2>&1 | sed -u "s/^/[bench] /"
# Benchmark
sudo nice -n -20 "$RETH_BENCH" new-payload-fcu \
$BENCH_NICE "$RETH_BENCH" new-payload-fcu \
--rpc-url "$BENCH_RPC_URL" \
--engine-rpc-url http://127.0.0.1:8551 \
--jwt-secret "$DATADIR/jwt.hex" \

127
.github/scripts/bench-reth-snapshot.sh vendored Executable file
View File

@@ -0,0 +1,127 @@
#!/usr/bin/env bash
#
# Downloads the latest nightly snapshot into the schelk volume with
# progress reporting to the GitHub PR comment.
#
# Skips the download if the local ETag marker matches the remote one.
#
# Usage: bench-reth-snapshot.sh [--check]
# --check Only check if a download is needed; exits 0 if up-to-date, 1 if not.
#
# Required env:
# SCHELK_MOUNT schelk mount point (e.g. /reth-bench)
# GITHUB_TOKEN token for GitHub API calls (only for download)
# BENCH_COMMENT_ID PR comment ID to update (optional)
# BENCH_REPO owner/repo (e.g. paradigmxyz/reth)
# BENCH_JOB_URL link to the Actions job
# BENCH_ACTOR user who triggered the benchmark
# BENCH_CONFIG config summary line
set -euo pipefail
BUCKET="minio/reth-snapshots/reth-1-minimal-nightly-previous.tar.zst"
DATADIR="$SCHELK_MOUNT/datadir"
ETAG_FILE="$HOME/.reth-bench-snapshot-etag"
# Get remote metadata via JSON for reliable parsing
MC_STAT=$(mc stat --json "$BUCKET" 2>/dev/null || true)
REMOTE_ETAG=$(echo "$MC_STAT" | jq -r '.etag // empty')
if [ -z "$REMOTE_ETAG" ]; then
echo "::warning::Failed to get ETag from mc stat, will re-download"
REMOTE_ETAG="unknown-$(date +%s)"
fi
LOCAL_ETAG=""
[ -f "$ETAG_FILE" ] && LOCAL_ETAG=$(cat "$ETAG_FILE")
if [ "$REMOTE_ETAG" = "$LOCAL_ETAG" ]; then
echo "Snapshot is up-to-date (ETag: ${REMOTE_ETAG})"
if [ "${1:-}" = "--check" ]; then
exit 0
fi
exit 0
fi
echo "Snapshot needs update (local: ${LOCAL_ETAG:-<none>}, remote: ${REMOTE_ETAG})"
if [ "${1:-}" = "--check" ]; then
exit 1
fi
# Get compressed size for progress tracking
TOTAL_BYTES=$(echo "$MC_STAT" | jq -r '.size // empty')
if [ -z "$TOTAL_BYTES" ] || [ "$TOTAL_BYTES" = "0" ]; then
echo "::error::Failed to get snapshot size from mc stat"
exit 1
fi
echo "Snapshot size: $TOTAL_BYTES bytes ($(numfmt --to=iec "$TOTAL_BYTES"))"
# Prepare mount
mountpoint -q "$SCHELK_MOUNT" && sudo schelk recover -y || true
sudo schelk mount -y
sudo rm -rf "$DATADIR"
sudo mkdir -p "$DATADIR"
update_comment() {
local pct="$1"
[ -z "${BENCH_COMMENT_ID:-}" ] && return 0
local status="Building binaries & downloading snapshot… ${pct}%"
local body
body="$(printf 'cc @%s\n\n🚀 Benchmark started! [View job](%s)\n\n⏳ **Status:** %s\n\n%s' \
"$BENCH_ACTOR" "$BENCH_JOB_URL" "$status" "$BENCH_CONFIG")"
curl -sf -X PATCH \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${BENCH_REPO}/issues/comments/${BENCH_COMMENT_ID}" \
-d "$(jq -nc --arg body "$body" '{body: $body}')" \
> /dev/null 2>&1 || true
}
# Track compressed bytes flowing through the pipe
DL_BYTES_FILE=$(mktemp)
echo 0 > "$DL_BYTES_FILE"
# Start progress reporter in background
(
while true; do
sleep 10
CURRENT=$(cat "$DL_BYTES_FILE" 2>/dev/null || echo 0)
if [ "$TOTAL_BYTES" -gt 0 ]; then
PCT=$(( CURRENT * 100 / TOTAL_BYTES ))
[ "$PCT" -gt 100 ] && PCT=100
echo "Snapshot download: $(numfmt --to=iec "$CURRENT") / $(numfmt --to=iec "$TOTAL_BYTES") (${PCT}%)"
update_comment "$PCT"
fi
done
) &
PROGRESS_PID=$!
trap 'kill $PROGRESS_PID 2>/dev/null || true; rm -f "$DL_BYTES_FILE"' EXIT
# Download and extract; python byte counter tracks compressed bytes received
mc cat "$BUCKET" | python3 -c "
import sys
count = 0
while True:
data = sys.stdin.buffer.read(1048576)
if not data:
break
count += len(data)
sys.stdout.buffer.write(data)
with open('$DL_BYTES_FILE', 'w') as f:
f.write(str(count))
" | pzstd -d -p 6 | sudo tar -xf - -C "$DATADIR"
# Stop progress reporter
kill $PROGRESS_PID 2>/dev/null || true
wait $PROGRESS_PID 2>/dev/null || true
update_comment "100"
echo "Snapshot download complete"
# Promote the new snapshot to become the schelk baseline (virgin volume).
# This copies changed blocks from scratch → virgin so that future
# `schelk recover` calls restore to this new state.
sync
sudo schelk promote -y
# Save ETag marker
echo "$REMOTE_ETAG" > "$ETAG_FILE"
echo "Snapshot promoted to schelk baseline (ETag: ${REMOTE_ETAG})"

View File

@@ -8,12 +8,12 @@ Usage:
--baseline-csv <baseline_combined.csv> \
[--repo <owner/repo>] \
[--baseline-ref <sha>] \
[--branch-name <name>] \
[--branch-sha <sha>]
[--feature-name <name>] \
[--feature-sha <sha>]
Generates a paired statistical comparison between baseline (main) and branch.
Generates a paired statistical comparison between baseline and feature.
Matches blocks by number and computes per-block diffs to cancel out gas
variance. Fails if baseline or branch CSV is missing or empty.
variance. Fails if baseline or feature CSV is missing or empty.
"""
import argparse
@@ -28,6 +28,14 @@ T_CRITICAL = 1.96 # two-tailed 95% confidence
BOOTSTRAP_ITERATIONS = 10_000
def _opt_int(row: dict, key: str) -> int | None:
"""Return int value for a CSV field, or None if missing/empty."""
v = row.get(key)
if v is None or v == "":
return None
return int(v)
def parse_combined_csv(path: str) -> list[dict]:
"""Parse combined_latency.csv into a list of per-block dicts."""
rows = []
@@ -43,11 +51,9 @@ def parse_combined_csv(path: str) -> list[dict]:
"new_payload_latency_us": int(row["new_payload_latency"]),
"fcu_latency_us": int(row["fcu_latency"]),
"total_latency_us": int(row["total_latency"]),
"persistence_wait_us": int(row["persistence_wait"])
if row.get("persistence_wait")
else None,
"execution_cache_wait_us": int(row.get("execution_cache_wait", 0)),
"sparse_trie_wait_us": int(row.get("sparse_trie_wait", 0)),
"persistence_wait_us": _opt_int(row, "persistence_wait"),
"execution_cache_wait_us": _opt_int(row, "execution_cache_wait"),
"sparse_trie_wait_us": _opt_int(row, "sparse_trie_wait"),
}
)
return rows
@@ -112,26 +118,45 @@ def compute_stats(combined: list[dict]) -> dict:
}
def compute_wait_stats(combined: list[dict], field: str) -> dict:
"""Compute mean/p50/p95 for a wait time field (in ms)."""
values_ms = []
for r in combined:
v = r.get(field)
if v is not None:
values_ms.append(v / 1_000)
if not values_ms:
return {}
n = len(values_ms)
mean_val = sum(values_ms) / n
sorted_vals = sorted(values_ms)
return {
"mean_ms": mean_val,
"p50_ms": percentile(sorted_vals, 50),
"p95_ms": percentile(sorted_vals, 95),
}
def _paired_data(
baseline: list[dict], branch: list[dict]
baseline: list[dict], feature: list[dict]
) -> tuple[list[tuple[float, float]], list[float], list[float]]:
"""Match blocks and return paired latencies and per-block diffs.
Returns:
pairs: list of (baseline_ms, branch_ms) tuples
lat_diffs_ms: list of branch baseline latency diffs in ms
mgas_diffs: list of branch baseline Mgas/s diffs
pairs: list of (baseline_ms, feature_ms) tuples
lat_diffs_ms: list of feature baseline latency diffs in ms
mgas_diffs: list of feature baseline Mgas/s diffs
"""
baseline_by_block = {r["block_number"]: r for r in baseline}
branch_by_block = {r["block_number"]: r for r in branch}
common_blocks = sorted(set(baseline_by_block) & set(branch_by_block))
feature_by_block = {r["block_number"]: r for r in feature}
common_blocks = sorted(set(baseline_by_block) & set(feature_by_block))
pairs = []
lat_diffs_ms = []
mgas_diffs = []
for bn in common_blocks:
b = baseline_by_block[bn]
f = branch_by_block[bn]
f = feature_by_block[bn]
b_ms = b["new_payload_latency_us"] / 1_000
f_ms = f["new_payload_latency_us"] / 1_000
pairs.append((b_ms, f_ms))
@@ -148,21 +173,23 @@ def _paired_data(
def compute_paired_stats(
baseline_runs: list[list[dict]],
branch_runs: list[list[dict]],
feature_runs: list[list[dict]],
) -> dict:
"""Compute paired statistics between baseline and branch runs.
"""Compute paired statistics between baseline and feature runs.
Each pair (baseline_runs[i], branch_runs[i]) produces per-block diffs.
Each pair (baseline_runs[i], feature_runs[i]) produces per-block diffs.
All diffs are pooled for the final CI.
"""
all_pairs = []
all_lat_diffs = []
all_mgas_diffs = []
for baseline, branch in zip(baseline_runs, branch_runs):
pairs, lat_diffs, mgas_diffs = _paired_data(baseline, branch)
blocks_per_pair = []
for baseline, feature in zip(baseline_runs, feature_runs):
pairs, lat_diffs, mgas_diffs = _paired_data(baseline, feature)
all_pairs.extend(pairs)
all_lat_diffs.extend(lat_diffs)
all_mgas_diffs.extend(mgas_diffs)
blocks_per_pair.append(len(pairs))
if not all_lat_diffs:
return {}
@@ -175,10 +202,10 @@ def compute_paired_stats(
# Bootstrap CI on difference-of-percentiles (resample paired blocks)
base_lats = sorted([p[0] for p in all_pairs])
branch_lats = sorted([p[1] for p in all_pairs])
p50_diff = percentile(branch_lats, 50) - percentile(base_lats, 50)
p90_diff = percentile(branch_lats, 90) - percentile(base_lats, 90)
p99_diff = percentile(branch_lats, 99) - percentile(base_lats, 99)
feature_lats = sorted([p[1] for p in all_pairs])
p50_diff = percentile(feature_lats, 50) - percentile(base_lats, 50)
p90_diff = percentile(feature_lats, 90) - percentile(base_lats, 90)
p99_diff = percentile(feature_lats, 99) - percentile(base_lats, 99)
rng = random.Random(42)
p50_boot, p90_boot, p99_boot = [], [], []
@@ -212,16 +239,10 @@ def compute_paired_stats(
"p99_ci_ms": (p99_boot[hi] - p99_boot[lo]) / 2,
"mean_mgas_diff": mean_mgas_diff,
"mgas_ci": mgas_ci,
"blocks": max(blocks_per_pair),
}
def compute_summary(combined: list[dict], gas: list[dict]) -> dict:
"""Compute aggregate metrics from parsed CSV data."""
blocks = len(combined)
return {
"blocks": blocks,
}
def format_duration(seconds: float) -> str:
if seconds >= 60:
@@ -246,33 +267,68 @@ def fmt_mgas(v: float) -> str:
return f"{v:.2f}"
def significance(pct: float, ci_pct: float, lower_is_better: bool) -> str:
"""Return significance label: 'good', 'bad', or 'neutral'."""
significant = abs(pct) > ci_pct
if not significant:
return "neutral"
elif (pct < 0) == lower_is_better:
return "good"
else:
return "bad"
def change_str(pct: float, ci_pct: float, lower_is_better: bool) -> str:
"""Format change% with paired CI significance.
Significant if the CI doesn't cross zero (i.e. |pct| > ci_pct).
"""
significant = abs(pct) > ci_pct
if not significant:
emoji = ""
elif (pct < 0) == lower_is_better:
emoji = ""
else:
emoji = ""
sig = significance(pct, ci_pct, lower_is_better)
emoji = {"good": "", "bad": "", "neutral": ""}[sig]
return f"{pct:+.2f}% {emoji}{ci_pct:.2f}%)"
def compute_changes(
baseline_stats: dict, feature_stats: dict, paired_stats: dict
) -> dict:
"""Pre-compute change percentages and significance for each metric."""
def pct(base: float, feat: float) -> float:
return (feat - base) / base * 100.0 if base > 0 else 0.0
def ci_pct(ci_ms: float, base_ms: float) -> float:
return ci_ms / base_ms * 100.0 if base_ms > 0 else 0.0
metrics = [
("mean", "mean_ms", "ci_ms", "mean_ms", True),
("p50", "p50_ms", "p50_ci_ms", "p50_ms", True),
("p90", "p90_ms", "p90_ci_ms", "p90_ms", True),
("p99", "p99_ms", "p99_ci_ms", "p99_ms", True),
("mgas_s", "mean_mgas_s", "mgas_ci", "mean_mgas_s", False),
]
changes = {}
for name, stat_key, ci_key, base_key, lower_is_better in metrics:
p = pct(baseline_stats[stat_key], feature_stats[stat_key])
c = ci_pct(paired_stats[ci_key], baseline_stats[base_key])
changes[name] = {
"pct": round(p, 4),
"ci_pct": round(c, 4),
"sig": significance(p, c, lower_is_better),
}
return changes
def generate_comparison_table(
run1: dict,
run2: dict,
paired: dict,
repo: str,
baseline_ref: str,
branch_name: str,
branch_sha: str,
baseline_name: str,
feature_name: str,
feature_sha: str,
) -> str:
"""Generate a markdown comparison table between baseline (main) and branch."""
n = paired["n"]
"""Generate a markdown comparison table between baseline and feature."""
n = paired["blocks"]
def pct(base: float, feat: float) -> float:
return (feat - base) / base * 100.0 if base > 0 else 0.0
@@ -294,11 +350,11 @@ def generate_comparison_table(
mgas_ci_pct = paired["mgas_ci"] / run1["mean_mgas_s"] * 100.0 if run1["mean_mgas_s"] > 0 else 0.0
base_url = f"https://github.com/{repo}/commit"
baseline_label = f"[`main`]({base_url}/{baseline_ref})"
branch_label = f"[`{branch_name}`]({base_url}/{branch_sha})"
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
feature_label = f"[`{feature_name}`]({base_url}/{feature_sha})"
lines = [
f"| Metric | {baseline_label} | {branch_label} | Change |",
f"| Metric | {baseline_label} | {feature_label} | Change |",
"|--------|------|--------|--------|",
f"| Mean | {fmt_ms(run1['mean_ms'])} | {fmt_ms(run2['mean_ms'])} | {change_str(mean_pct, lat_ci_pct, lower_is_better=True)} |",
f"| StdDev | {fmt_ms(run1['stddev_ms'])} | {fmt_ms(run2['stddev_ms'])} | |",
@@ -312,17 +368,51 @@ def generate_comparison_table(
return "\n".join(lines)
def generate_wait_time_table(
title: str,
baseline_stats: dict,
feature_stats: dict,
baseline_label: str,
feature_label: str,
) -> str:
"""Generate a markdown table for a wait time metric."""
if not baseline_stats or not feature_stats:
return ""
lines = [
f"### {title}",
"",
f"| Metric | {baseline_label} | {feature_label} |",
"|--------|------|--------|",
f"| Mean | {fmt_ms(baseline_stats['mean_ms'])} | {fmt_ms(feature_stats['mean_ms'])} |",
f"| P50 | {fmt_ms(baseline_stats['p50_ms'])} | {fmt_ms(feature_stats['p50_ms'])} |",
f"| P95 | {fmt_ms(baseline_stats['p95_ms'])} | {fmt_ms(feature_stats['p95_ms'])} |",
]
return "\n".join(lines)
def generate_markdown(
summary: dict, comparison_table: str,
behind_main: int = 0, repo: str = "", baseline_ref: str = "",
wait_time_tables: list[str] | None = None,
behind_baseline: int = 0, repo: str = "", baseline_ref: str = "", baseline_name: str = "",
) -> str:
"""Generate a markdown comment body."""
lines = ["## Benchmark Results", "", comparison_table]
if behind_main > 0:
s = "s" if behind_main > 1 else ""
diff_link = f"https://github.com/{repo}/compare/{baseline_ref[:12]}...main"
lines = ["## Benchmark Results", ""]
if behind_baseline > 0:
s = "s" if behind_baseline > 1 else ""
diff_link = f"https://github.com/{repo}/compare/{baseline_ref[:12]}...{baseline_name}"
lines.append(f"> ⚠️ Feature is [**{behind_baseline} commit{s} behind `{baseline_name}`**]({diff_link}). Consider rebasing for accurate results.")
lines.append("")
lines.append(f"> ⚠️ Branch is [**{behind_main} commit{s} behind `main`**]({diff_link}). Consider rebasing for accurate results.")
lines.append(comparison_table)
if wait_time_tables:
lines.append("")
lines.append("<details>")
lines.append("<summary>Wait Time Breakdown</summary>")
lines.append("")
for table in wait_time_tables:
if table:
lines.append(table)
lines.append("")
lines.append("</details>")
return "\n".join(lines)
@@ -333,8 +423,8 @@ def main():
help="Baseline combined_latency.csv files (A1, A2)",
)
parser.add_argument(
"--branch-csv", nargs="+", required=True,
help="Branch combined_latency.csv files (B1, B2)",
"--feature-csv", "--branch-csv", nargs="+", required=True,
help="Feature combined_latency.csv files (B1, B2)",
)
parser.add_argument("--gas-csv", required=True, help="Path to total_gas.csv")
parser.add_argument(
@@ -345,65 +435,113 @@ def main():
"--repo", default="paradigmxyz/reth", help="GitHub repo (owner/name)"
)
parser.add_argument("--baseline-ref", default=None, help="Baseline commit SHA")
parser.add_argument("--branch-name", default=None, help="Branch name")
parser.add_argument("--branch-sha", default=None, help="Branch commit SHA")
parser.add_argument("--behind-main", type=int, default=0, help="Commits behind main")
parser.add_argument("--baseline-name", default=None, help="Baseline display name")
parser.add_argument("--feature-name", "--branch-name", default=None, help="Feature branch name")
parser.add_argument("--feature-ref", "--branch-sha", "--feature-sha", default=None, help="Feature commit SHA")
parser.add_argument("--behind-baseline", "--behind-main", type=int, default=0, help="Commits behind baseline")
args = parser.parse_args()
if len(args.baseline_csv) != len(args.branch_csv):
print("Must provide equal number of baseline and branch CSVs", file=sys.stderr)
if len(args.baseline_csv) != len(args.feature_csv):
print("Must provide equal number of baseline and feature CSVs", file=sys.stderr)
sys.exit(1)
baseline_runs = []
branch_runs = []
feature_runs = []
for path in args.baseline_csv:
data = parse_combined_csv(path)
if not data:
print(f"No results in {path}", file=sys.stderr)
sys.exit(1)
baseline_runs.append(data)
for path in args.branch_csv:
for path in args.feature_csv:
data = parse_combined_csv(path)
if not data:
print(f"No results in {path}", file=sys.stderr)
sys.exit(1)
branch_runs.append(data)
feature_runs.append(data)
gas = parse_gas_csv(args.gas_csv)
all_baseline = [r for run in baseline_runs for r in run]
all_branch = [r for run in branch_runs for r in run]
summary = compute_summary(all_branch, gas)
with open(args.output_summary, "w") as f:
json.dump(summary, f, indent=2)
print(f"Summary written to {args.output_summary}")
all_feature = [r for run in feature_runs for r in run]
baseline_stats = compute_stats(all_baseline)
branch_stats = compute_stats(all_branch)
paired_stats = compute_paired_stats(baseline_runs, branch_runs)
feature_stats = compute_stats(all_feature)
paired_stats = compute_paired_stats(baseline_runs, feature_runs)
if not paired_stats:
print("No common blocks between baseline and branch runs", file=sys.stderr)
print("No common blocks between baseline and feature runs", file=sys.stderr)
sys.exit(1)
baseline_ref = args.baseline_ref or "main"
baseline_name = args.baseline_name or "baseline"
feature_name = args.feature_name or "feature"
feature_sha = args.feature_ref or "unknown"
comparison_table = generate_comparison_table(
baseline_stats,
branch_stats,
feature_stats,
paired_stats,
repo=args.repo,
baseline_ref=args.baseline_ref or "main",
branch_name=args.branch_name or "branch",
branch_sha=args.branch_sha or "unknown",
baseline_ref=baseline_ref,
baseline_name=baseline_name,
feature_name=feature_name,
feature_sha=feature_sha,
)
print(f"Generated comparison ({paired_stats['n']} paired blocks, "
f"mean diff {paired_stats['mean_diff_ms']:+.3f}ms ± {paired_stats['ci_ms']:.3f}ms)")
base_url = f"https://github.com/{args.repo}/commit"
baseline_label = f"[`{baseline_name}`]({base_url}/{baseline_ref})"
feature_label = f"[`{feature_name}`]({base_url}/{feature_sha})"
wait_fields = [
("persistence_wait_us", "Persistence Wait"),
("sparse_trie_wait_us", "Trie Cache Update Wait"),
("execution_cache_wait_us", "Execution Cache Update Wait"),
]
wait_time_tables = []
wait_time_data = {}
for field, title in wait_fields:
b_stats = compute_wait_stats(all_baseline, field)
f_stats = compute_wait_stats(all_feature, field)
if b_stats and f_stats:
wait_time_data[field] = {
"title": title,
"baseline": b_stats,
"feature": f_stats,
}
table = generate_wait_time_table(title, b_stats, f_stats, baseline_label, feature_label)
if table:
wait_time_tables.append(table)
summary = {
"blocks": paired_stats["blocks"],
"baseline": {
"name": baseline_name,
"ref": baseline_ref,
"stats": baseline_stats,
},
"feature": {
"name": feature_name,
"ref": feature_sha,
"stats": feature_stats,
},
"paired": paired_stats,
"changes": compute_changes(baseline_stats, feature_stats, paired_stats),
"wait_times": wait_time_data,
}
with open(args.output_summary, "w") as f:
json.dump(summary, f, indent=2)
print(f"Summary written to {args.output_summary}")
markdown = generate_markdown(
summary, comparison_table,
behind_main=args.behind_main,
wait_time_tables=wait_time_tables,
behind_baseline=args.behind_baseline,
repo=args.repo,
baseline_ref=args.baseline_ref or "",
baseline_ref=baseline_ref,
baseline_name=baseline_name,
)
with open(args.output_markdown, "w") as f:

342
.github/scripts/bench-slack-notify.js vendored Normal file
View File

@@ -0,0 +1,342 @@
// Sends Slack notifications for reth-bench results.
//
// Reads from environment:
// SLACK_BENCH_BOT_TOKEN Slack Bot User OAuth Token (xoxb-...)
// SLACK_BENCH_CHANNEL Public channel ID for significant improvements
// BENCH_WORK_DIR Directory containing summary.json
// BENCH_PR PR number (may be empty)
// BENCH_ACTOR GitHub user who triggered the bench
// BENCH_JOB_URL URL to the Actions job page
// BENCH_SAMPLY 'true' if samply profiling was enabled
//
// Usage from actions/github-script:
// const notify = require('./.github/scripts/bench-slack-notify.js');
// await notify.success({ core, context });
// await notify.failure({ core, context, failedStep: '...' });
const fs = require('fs');
const path = require('path');
const SLACK_API = 'https://slack.com/api/chat.postMessage';
function loadSlackUsers(repoRoot) {
try {
const raw = fs.readFileSync(path.join(repoRoot, '.github', 'scripts', 'bench-slack-users.json'), 'utf8');
const data = JSON.parse(raw);
// Filter out non-user-ID entries (like _comment)
const users = {};
for (const [k, v] of Object.entries(data)) {
if (!k.startsWith('_') && typeof v === 'string' && v.startsWith('U')) {
users[k] = v;
}
}
return users;
} catch {
return {};
}
}
async function postToSlack(token, channel, blocks, text, core, threadTs) {
const payload = { channel, blocks, text, unfurl_links: false };
if (threadTs) payload.thread_ts = threadTs;
const resp = await fetch(SLACK_API, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
const data = await resp.json();
if (!data.ok) {
core.warning(`Slack API error (channel ${channel}): ${JSON.stringify(data)}`);
}
return data;
}
function cell(text) {
const s = String(text);
return { type: 'raw_text', text: s || ' ' };
}
function buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, repo, samplyUrls }) {
const b = summary.baseline.stats;
const f = summary.feature.stats;
const c = summary.changes;
const sigEmoji = { good: '\u2705', bad: '\u274c', neutral: '\u26aa' };
function fmtMs(v) { return v.toFixed(2) + 'ms'; }
function fmtMgas(v) { return v.toFixed(2); }
function fmtChange(ch) {
if (!ch.pct && !ch.ci_pct) return ' ';
const pctStr = `${ch.pct >= 0 ? '+' : ''}${ch.pct.toFixed(2)}%`;
const ciStr = ch.ci_pct ? ` (\u00b1${ch.ci_pct.toFixed(2)}%)` : '';
return `${pctStr}${ciStr} ${sigEmoji[ch.sig]}`;
}
// Overall result for header
const vals = Object.values(c);
const hasBad = vals.some(v => v.sig === 'bad');
const hasGood = vals.some(v => v.sig === 'good');
let headerEmoji, headerResult;
if (hasBad && hasGood) {
headerEmoji = ':warning:';
headerResult = 'Mixed Results';
} else if (hasBad) {
headerEmoji = ':x:';
headerResult = 'Regression';
} else if (hasGood) {
headerEmoji = ':white_check_mark:';
headerResult = 'Improvement';
} else {
headerEmoji = ':white_circle:';
headerResult = 'No Difference';
}
const prUrl = prNumber ? `https://github.com/${repo}/pull/${prNumber}` : '';
const commitUrl = `https://github.com/${repo}/commit`;
const baselineLink = `<${commitUrl}/${summary.baseline.ref}|${summary.baseline.name}>`;
const featureLink = `<${commitUrl}/${summary.feature.ref}|${summary.feature.name}>`;
// Meta line
const metaParts = [];
if (prNumber) metaParts.push(`*<${prUrl}|PR #${prNumber}>*`);
metaParts.push(`triggered by ${actorSlackId ? `<@${actorSlackId}>` : `@${actor}`}`);
// Baseline/feature lines with samply profile links
let baselineLine = `*Baseline:* ${baselineLink}`;
const bl1 = samplyUrls['baseline-1'];
const bl2 = samplyUrls['baseline-2'];
if (bl1) baselineLine += ` | <${bl1}|Samply 1>`;
if (bl2) baselineLine += ` | <${bl2}|Samply 2>`;
let featureLine = `*Feature:* ${featureLink}`;
const fl1 = samplyUrls['feature-1'];
const fl2 = samplyUrls['feature-2'];
if (fl1) featureLine += ` | <${fl1}|Samply 1>`;
if (fl2) featureLine += ` | <${fl2}|Samply 2>`;
const warmup = summary.warmup_blocks || process.env.BENCH_WARMUP_BLOCKS || '';
const cores = process.env.BENCH_CORES || '0';
const countsParts = [];
if (warmup) countsParts.push(`*Warmup:* ${warmup}`);
countsParts.push(`*Blocks:* ${summary.blocks}`);
if (cores !== '0') countsParts.push(`*Cores:* ${cores}`);
const countsLine = countsParts.join(' | ');
const sectionText = [metaParts.join(' | '), '', baselineLine, featureLine, countsLine].join('\n');
// Action buttons
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
const buttons = [
{
type: 'button',
text: { type: 'plain_text', text: 'CI :github:', emoji: true },
url: jobUrl,
action_id: 'ci_button',
},
{
type: 'button',
text: { type: 'plain_text', text: 'Diff :github:', emoji: true },
url: diffUrl,
action_id: 'diff_button',
},
];
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `${headerEmoji} ${headerResult}`, emoji: true },
},
{
type: 'section',
text: { type: 'mrkdwn', text: sectionText },
},
{
type: 'table',
column_settings: [
{ align: 'left' },
{ align: 'right' },
{ align: 'right' },
{ align: 'right' },
],
rows: [
[cell('Metric'), cell('Baseline'), cell('Feature'), cell('Change')],
[cell('Mean'), cell(fmtMs(b.mean_ms)), cell(fmtMs(f.mean_ms)), cell(fmtChange(c.mean))],
[cell('StdDev'), cell(fmtMs(b.stddev_ms)), cell(fmtMs(f.stddev_ms)), cell(' ')],
[cell('P50'), cell(fmtMs(b.p50_ms)), cell(fmtMs(f.p50_ms)), cell(fmtChange(c.p50))],
[cell('P90'), cell(fmtMs(b.p90_ms)), cell(fmtMs(f.p90_ms)), cell(fmtChange(c.p90))],
[cell('P99'), cell(fmtMs(b.p99_ms)), cell(fmtMs(f.p99_ms)), cell(fmtChange(c.p99))],
[cell('Mgas/s'), cell(fmtMgas(b.mean_mgas_s)), cell(fmtMgas(f.mean_mgas_s)), cell(fmtChange(c.mgas_s))],
],
},
{
type: 'actions',
elements: buttons,
},
];
// Wait times as a separate table block (sent as threaded reply due to Slack one-table limit)
const threadBlocks = [];
const waitTimes = summary.wait_times || {};
const waitKeys = Object.keys(waitTimes);
if (waitKeys.length > 0) {
const waitRows = [
[cell('Wait Time'), cell('Baseline'), cell('Feature')],
];
for (const key of waitKeys) {
const wt = waitTimes[key];
waitRows.push([cell(wt.title), cell(fmtMs(wt.baseline.mean_ms)), cell(fmtMs(wt.feature.mean_ms))]);
}
threadBlocks.push({
type: 'table',
column_settings: [
{ align: 'left' },
{ align: 'right' },
{ align: 'right' },
],
rows: waitRows,
});
}
return { blocks, threadBlocks };
}
function buildFailureBlocks({ prNumber, actor, actorSlackId, jobUrl, repo, failedStep }) {
const prUrl = prNumber ? `https://github.com/${repo}/pull/${prNumber}` : '';
const actorMention = actorSlackId ? `<@${actorSlackId}>` : `@${actor}`;
const parts = [
prNumber ? `*<${prUrl}|PR #${prNumber}>*` : '',
`by ${actorMention}`,
`failed while *${failedStep}*`,
].filter(Boolean);
const buttons = [
{
type: 'button',
text: { type: 'plain_text', text: 'CI :github:', emoji: true },
url: jobUrl,
action_id: 'ci_button',
},
];
return [
{
type: 'header',
text: { type: 'plain_text', text: ':rotating_light: Bench Failed', emoji: true },
},
{
type: 'section',
text: { type: 'mrkdwn', text: parts.join(' | ') },
},
{
type: 'actions',
elements: buttons,
},
];
}
async function success({ core, context }) {
const token = process.env.SLACK_BENCH_BOT_TOKEN;
if (!token) {
core.info('SLACK_BENCH_BOT_TOKEN not set, skipping Slack notification');
return;
}
let summary;
try {
summary = JSON.parse(fs.readFileSync(process.env.BENCH_WORK_DIR + '/summary.json', 'utf8'));
} catch (e) {
core.warning('Could not read summary.json for Slack notification');
return;
}
const repo = `${context.repo.owner}/${context.repo.repo}`;
const prNumber = process.env.BENCH_PR;
const actor = process.env.BENCH_ACTOR;
const jobUrl = process.env.BENCH_JOB_URL ||
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
// Load samply profile URLs (files exist when samply profiling was enabled)
const samplyUrls = {};
for (const run of ['baseline-1', 'baseline-2', 'feature-1', 'feature-2']) {
try {
const url = fs.readFileSync(
path.join(process.env.BENCH_WORK_DIR, run, 'samply-profile-url.txt'), 'utf8'
).trim();
if (url) samplyUrls[run] = url;
} catch {}
}
const slackUsers = loadSlackUsers(process.env.GITHUB_WORKSPACE || '.');
const actorSlackId = slackUsers[actor];
const { blocks, threadBlocks } = buildSuccessBlocks({ summary, prNumber, actor, actorSlackId, jobUrl, repo, samplyUrls });
const text = `Bench: ${summary.baseline.name} vs ${summary.feature.name}`;
async function sendWithThread(ch) {
const res = await postToSlack(token, ch, blocks, text, core);
if (res.ok && res.ts && threadBlocks.length > 0) {
for (const tb of threadBlocks) {
await postToSlack(token, ch, [tb], 'Wait time breakdown', core, res.ts);
}
}
}
// Post to public channel if any metric shows significant improvement or regression
const channel = process.env.SLACK_BENCH_CHANNEL;
let postedToChannel = false;
if (channel) {
const changes = summary.changes || {};
const hasImprovement = Object.values(changes).some(c => c.sig === 'good');
if (hasImprovement) {
await sendWithThread(channel);
postedToChannel = true;
} else {
core.info('No significant improvement, skipping public channel notification');
}
}
// DM the actor only when results were not posted to the public channel
if (!postedToChannel) {
if (actorSlackId) {
await sendWithThread(actorSlackId);
} else {
core.info(`No Slack user mapping for GitHub user '${actor}', skipping DM`);
}
} else {
core.info(`Results posted to channel, skipping DM to ${actor}`);
}
}
async function failure({ core, context, failedStep }) {
const token = process.env.SLACK_BENCH_BOT_TOKEN;
if (!token) {
core.info('SLACK_BENCH_BOT_TOKEN not set, skipping Slack notification');
return;
}
const repo = `${context.repo.owner}/${context.repo.repo}`;
const prNumber = process.env.BENCH_PR;
const actor = process.env.BENCH_ACTOR;
const jobUrl = process.env.BENCH_JOB_URL ||
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const slackUsers = loadSlackUsers(process.env.GITHUB_WORKSPACE || '.');
const actorSlackId = slackUsers[actor];
const blocks = buildFailureBlocks({ prNumber, actor, actorSlackId, jobUrl, repo, failedStep });
const text = `Bench failed while ${failedStep}`;
// Always DM the actor
if (actorSlackId) {
await postToSlack(token, actorSlackId, blocks, text, core);
} else {
core.info(`No Slack user mapping for GitHub user '${actor}', skipping DM`);
}
// Only DM for failures, don't post to public channel
}
module.exports = { success, failure };

13
.github/scripts/bench-slack-users.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"_comment": "Maps GitHub usernames to Slack user IDs. Find yours: Slack profile > ··· > Copy member ID.",
"shekhirin": "U09FAL2UMLJ",
"mattsse": "U09FQNPMRT3",
"klkvr": "U09FAK95FC2",
"joshieDo": "U09LHN6GYAU",
"mediocregopher": "U09FF75KMQU",
"yongkangc": "U09FB0ECTD4",
"gakonst": "U092SEPDM40",
"Rjected": "U09F6SCKRGT",
"DaniPopes": "U09FAT8EK2A",
"emmajam": "U0A34UN92HW"
}

27
.github/scripts/bench-update-status.js vendored Normal file
View File

@@ -0,0 +1,27 @@
// Updates the reth-bench PR comment with current status.
//
// Reads from environment:
// BENCH_COMMENT_ID GitHub comment ID to update
// BENCH_JOB_URL URL to the Actions job page
// BENCH_CONFIG Config line (blocks, warmup, refs)
// BENCH_ACTOR User who triggered the benchmark
//
// Usage from actions/github-script:
// const s = require('./.github/scripts/bench-update-status.js');
// await s({github, context, status: 'Building baseline binary...'});
function buildBody(status) {
return `cc @${process.env.BENCH_ACTOR}\n\n🚀 Benchmark started! [View job](${process.env.BENCH_JOB_URL})\n\n⏳ **Status:** ${status}\n\n${process.env.BENCH_CONFIG}`;
}
async function updateStatus({ github, context, status }) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: parseInt(process.env.BENCH_COMMENT_ID),
body: buildBody(status),
});
}
updateStatus.buildBody = buildBody;
module.exports = updateStatus;

View File

@@ -59,10 +59,6 @@ engine-auth: [ ]
#
# System contract tests (already fixed and deployed):
#
# tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout and test_invalid_log_length
# System contract is already fixed and deployed; tests cover scenarios where contract is
# malformed which can't happen retroactively. No point in adding checks.
#
# tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment
# tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment
# Post-fork system contract deployment tests. Should fix for spec compliance but not realistic
@@ -71,32 +67,8 @@ eels/consume-engine:
- tests/prague/eip7702_set_code_tx/test_set_code_txs.py::test_set_code_to_non_empty_storage[fork_Prague-blockchain_test_engine-zero_nonce]-reth
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
- tests/prague/eip7251_consolidations/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_index_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Prague-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-nonzero_balance]-reth
- tests/prague/eip7002_el_triggerable_withdrawals/test_contract_deployment.py::test_system_contract_deployment[fork_CancunToPragueAtTime15k-blockchain_test_engine-deploy_after_fork-zero_balance]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_False]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Prague-blockchain_test_engine-slice_bytes_True]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_amount_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_index_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_pubkey_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_signature_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_offset-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_layout[fork_Osaka-blockchain_test_engine-log_argument_withdrawal_credentials_size-value_zero]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_False]-reth
- tests/prague/eip6110_deposits/test_modified_contract.py::test_invalid_log_length[fork_Osaka-blockchain_test_engine-slice_bytes_True]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Osaka-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-revert-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Prague-tx_type_0-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth
- tests/paris/eip7610_create_collision/test_initcollision.py::test_init_collision_create_tx[fork_Paris-tx_type_1-blockchain_test_engine_from_state_test-non-empty-balance-correct-initcode]-reth

View File

@@ -11,17 +11,16 @@
#
# When a test should no longer be ignored, remove it from this list.
# flaky
engine-withdrawals:
- Withdrawals Fork on Block 1 - 8 Block Re-Org NewPayload (Paris) (reth)
- Withdrawals Fork on Block 8 - 10 Block Re-Org NewPayload (Paris) (reth)
- Withdrawals Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org (Paris) (reth)
- Sync after 128 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts (Paris) (reth)
engine-cancun:
- Transaction Re-Org, New Payload on Revert Back (Cancun) (reth)
- Transaction Re-Org, Re-Org to Different Block (Cancun) (reth)
- Transaction Re-Org, Re-Org Out (Cancun) (reth)
- Invalid Missing Ancestor ReOrg, StateRoot, EmptyTxs=False, Invalid P9 (Cancun) (reth)
# Hive test infra bug: geth sidecar switched to PathScheme for state storage, which has
# strict trie integrity requirements incompatible with inserting intentionally invalid blocks.
# Affects all clients, not just reth. Tracked: https://github.com/ethereum/hive/issues/1382
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=False, Invalid P8 (Cancun) (reth)
- Invalid Missing Ancestor Syncing ReOrg, Timestamp, EmptyTxs=False, CanonicalReOrg=True, Invalid P8 (Cancun) (reth)
- Multiple New Payloads Extending Canonical Chain, Wait for Canonical Payload (Cancun) (reth)
engine-api:
- Transaction Re-Org, Re-Org Out (Paris) (reth)

View File

@@ -6,8 +6,14 @@ cd hivetests/
sim="${1}"
limit="${2}"
# Use lower parallelism for eels tests to avoid OOM-killing the runner
parallelism=16
if [[ "${sim}" == *"eels"* ]]; then
parallelism=4
fi
run_hive() {
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism 16 --client reth 2>&1 | tee /tmp/log || true
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism "${parallelism}" --client reth 2>&1 | tee /tmp/log || true
}
check_log() {

File diff suppressed because it is too large Load Diff

View File

@@ -188,7 +188,8 @@ jobs:
- build-reth-edge
- prepare-hive
name: ${{ matrix.storage }} / ${{ matrix.scenario.sim }}${{ matrix.scenario.limit && format(' - {0}', matrix.scenario.limit) }}
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest-4' || 'ubuntu-latest' }}
# Use larger runners for eels tests to avoid OOM runner crashes
runs-on: ${{ github.repository == 'paradigmxyz/reth' && (contains(matrix.scenario.sim, 'eels') && 'depot-ubuntu-latest-8' || 'depot-ubuntu-latest-4') || 'ubuntu-latest' }}
permissions:
issues: write
steps:

View File

@@ -74,7 +74,7 @@ jobs:
profile: maxperf
allow_fail: false
- target: aarch64-unknown-linux-gnu
os: ubuntu-24.04
os: ubuntu-24.04-arm
profile: maxperf
allow_fail: false
- target: x86_64-apple-darwin
@@ -85,10 +85,6 @@ jobs:
os: macos-14
profile: maxperf
allow_fail: false
- target: riscv64gc-unknown-linux-gnu
os: ubuntu-24.04
profile: maxperf
allow_fail: true
build:
- command: build
binary: reth

View File

@@ -172,10 +172,97 @@ Before submitting changes, ensure:
2. **Clippy**: No warnings
3. **Tests Pass**: All unit and integration tests
4. **Documentation**: Update relevant docs and add doc comments with `cargo docs --document-private-items`
5. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
5. **CLI Docs** (if CLI changed): Run `make update-book-cli` (see below)
6. **Commit Messages**: Follow conventional format (feat:, fix:, chore:, etc.)
### CLI Reference Docs (`book` CI Job)
The CLI reference pages under `docs/vocs/docs/pages/cli/` are **auto-generated** from the `reth` binary's `--help` output. **Do not edit these files manually** — any hand edits will be overwritten and CI will fail regardless.
When you add, remove, or modify CLI commands, subcommands, or flags, regenerate the CLI docs by running:
```bash
make update-book-cli
```
This builds `reth` in debug mode and runs `docs/cli/update.sh` to regenerate all CLI pages. Commit the resulting changes.
The `book` CI job (`.github/workflows/lint.yml`) enforces this by regenerating the docs and running `git diff --exit-code`. If the committed docs don't match the generated output, CI fails. Manually editing these pages is never productive — always use `make update-book-cli`.
### Opening PRs against <https://github.com/paradigmxyz/reth>
#### Titles
Use [Conventional Commits](https://www.conventionalcommits.org/) with an optional scope:
```
<type>(<scope>): <short description>
```
**Types**: `feat`, `fix`, `perf`, `refactor`, `docs`, `test`, `chore`
**Scope** (optional): crate or area, e.g. `evm`, `trie`, `rpc`, `engine`, `net`
Examples:
- `fix(rpc): correct gas estimation for ERC-20 transfers`
- `perf: batch trie updates to reduce cursor overhead`
- `feat(engine): add new_payload_interval metric`
#### Descriptions
Keep it short. Say what changed and why — nothing more.
**Do:**
- Write 13 sentences summarizing the change
- Explain _why_ if the diff doesn't make it obvious
- Link related issues or EIPs
- Include benchmark numbers for perf changes
**Don't:**
- List every file changed — that's what the diff is for
- Repeat the title in the body
- Add "Files changed" or "Changes" sections
- Write walls of text that go stale when the diff is updated
- Use filler like "This PR introduces...", "comprehensive", "robust", "enhance", "leverage"
**Template:**
```
Closes #<issue>
<what changed, 1-3 sentences>
<why, if not obvious from the diff>
```
**Good example:**
```
Closes #16800
Adds fallback for external IP resolution so node startup doesn't fail
when STUN is unreachable. Falls back to the configured default.
```
**Bad example:**
```
## Summary
This PR introduces comprehensive improvements to the IP resolution system.
## Changes
- Modified `crates/net/discv4/src/lib.rs` to add fallback
- Modified `crates/net/discv4/src/config.rs` to add default IP
- Added tests in `crates/net/discv4/src/tests/ip.rs`
## Files Changed
- crates/net/discv4/src/lib.rs
- crates/net/discv4/src/config.rs
- crates/net/discv4/src/tests/ip.rs
```
#### Labels and CI
Label PRs appropriately, first check the available labels and then apply the relevant ones:
* when changes are RPC related, add A-rpc label
* when changes are docs related, add C-docs label
@@ -455,5 +542,8 @@ cargo build --release
cargo check --workspace --all-features
# Check documentation
cargo docs --document-private-items
cargo docs --document-private-items
# Regenerate CLI reference docs (after CLI changes)
make update-book-cli
```

988
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
[workspace.package]
version = "1.11.0"
version = "1.11.1"
edition = "2024"
rust-version = "1.93"
license = "MIT OR Apache-2.0"
@@ -138,6 +138,7 @@ members = [
"examples/exex-subscription",
"examples/exex-test",
"examples/full-contract-state",
"examples/migrate-trie-to-packed",
"examples/manual-p2p/",
"examples/network-txpool/",
"examples/network/",
@@ -397,7 +398,7 @@ reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" }
reth-payload-primitives = { path = "crates/payload/primitives" }
reth-payload-validator = { path = "crates/payload/validator" }
reth-payload-util = { path = "crates/payload/util" }
reth-primitives = { path = "crates/primitives", default-features = false }
reth-primitives = { path = "crates/primitives", default-features = false, features = ["__internal"] }
reth-primitives-traits = { path = "crates/primitives-traits", default-features = false }
reth-provider = { path = "crates/storage/provider" }
reth-prune = { path = "crates/prune/prune" }
@@ -459,33 +460,33 @@ alloy-trie = { version = "0.9.4", default-features = false }
alloy-hardforks = "0.4.5"
alloy-consensus = { version = "1.6.3", default-features = false }
alloy-contract = { version = "1.6.3", default-features = false }
alloy-eips = { version = "1.6.3", default-features = false }
alloy-genesis = { version = "1.6.3", default-features = false }
alloy-json-rpc = { version = "1.6.3", default-features = false }
alloy-network = { version = "1.6.3", default-features = false }
alloy-network-primitives = { version = "1.6.3", default-features = false }
alloy-provider = { version = "1.6.3", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.6.3", default-features = false }
alloy-rpc-client = { version = "1.6.3", default-features = false }
alloy-rpc-types = { version = "1.6.3", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.6.3", default-features = false }
alloy-rpc-types-anvil = { version = "1.6.3", default-features = false }
alloy-rpc-types-beacon = { version = "1.6.3", default-features = false }
alloy-rpc-types-debug = { version = "1.6.3", default-features = false }
alloy-rpc-types-engine = { version = "1.6.3", default-features = false }
alloy-rpc-types-eth = { version = "1.6.3", default-features = false }
alloy-rpc-types-mev = { version = "1.6.3", default-features = false }
alloy-rpc-types-trace = { version = "1.6.3", default-features = false }
alloy-rpc-types-txpool = { version = "1.6.3", default-features = false }
alloy-serde = { version = "1.6.3", default-features = false }
alloy-signer = { version = "1.6.3", default-features = false }
alloy-signer-local = { version = "1.6.3", default-features = false }
alloy-transport = { version = "1.6.3" }
alloy-transport-http = { version = "1.6.3", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.6.3", default-features = false }
alloy-transport-ws = { version = "1.6.3", default-features = false }
alloy-consensus = { version = "1.7.3", default-features = false }
alloy-contract = { version = "1.7.3", default-features = false }
alloy-eips = { version = "1.7.3", default-features = false }
alloy-genesis = { version = "1.7.3", default-features = false }
alloy-json-rpc = { version = "1.7.3", default-features = false }
alloy-network = { version = "1.7.3", default-features = false }
alloy-network-primitives = { version = "1.7.3", default-features = false }
alloy-provider = { version = "1.7.3", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.7.3", default-features = false }
alloy-rpc-client = { version = "1.7.3", default-features = false }
alloy-rpc-types = { version = "1.7.3", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.7.3", default-features = false }
alloy-rpc-types-anvil = { version = "1.7.3", default-features = false }
alloy-rpc-types-beacon = { version = "1.7.3", default-features = false }
alloy-rpc-types-debug = { version = "1.7.3", default-features = false }
alloy-rpc-types-engine = { version = "1.7.3", default-features = false }
alloy-rpc-types-eth = { version = "1.7.3", default-features = false }
alloy-rpc-types-mev = { version = "1.7.3", default-features = false }
alloy-rpc-types-trace = { version = "1.7.3", default-features = false }
alloy-rpc-types-txpool = { version = "1.7.3", default-features = false }
alloy-serde = { version = "1.7.3", default-features = false }
alloy-signer = { version = "1.7.3", default-features = false }
alloy-signer-local = { version = "1.7.3", default-features = false }
alloy-transport = { version = "1.7.3" }
alloy-transport-http = { version = "1.7.3", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.7.3", default-features = false }
alloy-transport-ws = { version = "1.7.3", default-features = false }
# op
alloy-op-evm = { version = "0.27.2", default-features = false }
@@ -532,13 +533,13 @@ quanta = "0.12"
paste = "1.0"
rand = "0.9"
rayon = "1.7"
thread-priority = "3.0.0"
rustc-hash = { version = "2.0", default-features = false }
schnellru = "0.2"
serde = { version = "1.0", default-features = false }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
serde_with = { version = "3", default-features = false, features = ["macros"] }
sha2 = { version = "0.10", default-features = false }
shellexpand = "3.0.0"
shlex = "1.3"
smallvec = "1"
strum = { version = "0.27", default-features = false }
@@ -651,7 +652,7 @@ ethereum_ssz_derive = "0.10.1"
jemalloc_pprof = { version = "0.8", default-features = false }
tikv-jemalloc-ctl = "0.6"
tikv-jemallocator = "0.6"
tracy-client = "0.18.0"
tracy-client = { version = "0.18.0", features = ["demangle"] }
snmalloc-rs = { version = "0.3.7", features = ["build_cc"] }
aes = "0.8.1"

View File

@@ -45,9 +45,6 @@ serde_json.workspace = true
# Time handling
chrono = { workspace = true, features = ["serde"] }
# Path manipulation
shellexpand.workspace = true
# CSV handling
csv.workspace = true

View File

@@ -289,11 +289,7 @@ impl Args {
/// Get the JWT secret path - either provided or derived from datadir
pub(crate) fn jwt_secret_path(&self) -> PathBuf {
match &self.jwt_secret {
Some(path) => {
let jwt_secret_str = path.to_string_lossy();
let expanded = shellexpand::tilde(&jwt_secret_str);
PathBuf::from(expanded.as_ref())
}
Some(path) => path.clone(),
None => {
// Use the same logic as reth: <datadir>/<chain>/jwt.hex
let chain_path = self.datadir.clone().resolve_datadir(self.chain);
@@ -308,10 +304,9 @@ impl Args {
chain_path.data_dir().to_path_buf()
}
/// Get the expanded output directory path
/// Get the output directory path
pub(crate) fn output_dir_path(&self) -> PathBuf {
let expanded = shellexpand::tilde(&self.output_dir);
PathBuf::from(expanded.as_ref())
PathBuf::from(&self.output_dir)
}
/// Get the effective warmup blocks value - either specified or defaults to blocks

View File

@@ -31,6 +31,8 @@ pub(crate) struct BenchContext {
pub(crate) is_optimism: bool,
/// Whether to use `reth_newPayload` endpoint instead of `engine_newPayload*`.
pub(crate) use_reth_namespace: bool,
/// Whether to fetch and replay RLP-encoded blocks.
pub(crate) rlp_blocks: bool,
}
impl BenchContext {
@@ -142,7 +144,8 @@ impl BenchContext {
};
let next_block = first_block.header.number + 1;
let use_reth_namespace = bench_args.reth_new_payload;
let rlp_blocks = bench_args.rlp_blocks;
let use_reth_namespace = bench_args.reth_new_payload || rlp_blocks;
Ok(Self {
auth_provider,
block_provider,
@@ -150,6 +153,7 @@ impl BenchContext {
next_block,
is_optimism,
use_reth_namespace,
rlp_blocks,
})
}
}

View File

@@ -6,7 +6,9 @@ use crate::{
helpers::{build_payload, parse_gas_limit, prepare_payload_request, rpc_block_to_header},
output::GasRampPayloadFile,
},
valid_payload::{call_forkchoice_updated, call_new_payload_with_reth, payload_to_new_payload},
valid_payload::{
call_forkchoice_updated_with_reth, call_new_payload_with_reth, payload_to_new_payload,
},
};
use alloy_eips::BlockNumberOrTag;
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
@@ -19,6 +21,7 @@ use reth_chainspec::ChainSpec;
use reth_cli_runner::CliContext;
use reth_ethereum_primitives::TransactionSigned;
use reth_primitives_traits::constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK};
use reth_rpc_api::RethNewPayloadInput;
use std::{path::PathBuf, time::Instant};
use tracing::info;
@@ -147,7 +150,7 @@ impl Command {
}
}
if self.reth_new_payload {
info!("Using reth_newPayload endpoint");
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
}
let mut blocks_processed = 0u64;
@@ -182,28 +185,32 @@ impl Command {
Some(new_payload_version),
)?;
let (version, params) = if self.reth_new_payload {
(None, serde_json::to_value((RethNewPayloadInput::ExecutionData(execution_data),))?)
} else {
(Some(version), params)
};
// Save payload to file with version info for replay
let payload_path =
self.output.join(format!("payload_block_{}.json", block.header.number));
let file = GasRampPayloadFile {
version: version as u8,
version: version.map(|v| v as u8),
block_hash,
params: params.clone(),
execution_data: Some(execution_data.clone()),
};
let payload_json = serde_json::to_string_pretty(&file)?;
std::fs::write(&payload_path, &payload_json)?;
info!(target: "reth-bench", block_number = block.header.number, path = %payload_path.display(), "Saved payload");
let reth_data = self.reth_new_payload.then_some(execution_data);
let _ = call_new_payload_with_reth(&provider, version, params, reth_data).await?;
let _ = call_new_payload_with_reth(&provider, version, params).await?;
let forkchoice_state = ForkchoiceState {
head_block_hash: block_hash,
safe_block_hash: block_hash,
finalized_block_hash: block_hash,
};
call_forkchoice_updated(&provider, version, forkchoice_state, None).await?;
call_forkchoice_updated_with_reth(&provider, version, forkchoice_state).await?;
parent_header = block.header;
parent_hash = block_hash;

View File

@@ -0,0 +1,156 @@
//! Prometheus metrics scraper for reth-bench.
//!
//! Scrapes a node's Prometheus metrics endpoint after each block to record
//! execution and state root durations with block-level granularity.
use csv::Writer;
use eyre::Context;
use reqwest::Client;
use serde::Serialize;
use std::{path::Path, time::Duration};
use tracing::info;
/// Suffix for the metrics CSV output file.
pub(crate) const METRICS_OUTPUT_SUFFIX: &str = "metrics.csv";
/// A single row of scraped prometheus metrics for one block.
#[derive(Debug, Clone, Serialize)]
pub(crate) struct MetricsRow {
/// The block number.
pub(crate) block_number: u64,
/// EVM execution duration in seconds (from `sync_execution_execution_duration` gauge).
pub(crate) execution_duration_secs: Option<f64>,
/// State root computation duration in seconds (from
/// `sync_block_validation_state_root_duration` gauge).
pub(crate) state_root_duration_secs: Option<f64>,
}
/// Scrapes a Prometheus metrics endpoint after each block to collect
/// execution and state root durations.
pub(crate) struct MetricsScraper {
/// The full URL of the Prometheus metrics endpoint.
url: String,
/// Reusable HTTP client.
client: Client,
/// Collected metrics rows, one per block.
rows: Vec<MetricsRow>,
}
impl MetricsScraper {
/// Creates a new scraper if a URL is provided.
pub(crate) fn maybe_new(url: Option<String>) -> Option<Self> {
url.map(|url| {
info!(target: "reth-bench", %url, "Prometheus metrics scraping enabled");
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()
.expect("failed to build reqwest client");
Self { url, client, rows: Vec::new() }
})
}
/// Scrapes the metrics endpoint and records values for the given block.
pub(crate) async fn scrape_after_block(&mut self, block_number: u64) -> eyre::Result<()> {
let text = self
.client
.get(&self.url)
.send()
.await
.wrap_err("failed to fetch metrics endpoint")?
.error_for_status()
.wrap_err("metrics endpoint returned error status")?
.text()
.await
.wrap_err("failed to read metrics response body")?;
let execution = parse_gauge(&text, "sync_execution_execution_duration");
let state_root = parse_gauge(&text, "sync_block_validation_state_root_duration");
self.rows.push(MetricsRow {
block_number,
execution_duration_secs: execution,
state_root_duration_secs: state_root,
});
Ok(())
}
/// Writes collected metrics to a CSV file in the output directory.
pub(crate) fn write_csv(&self, output_dir: &Path) -> eyre::Result<()> {
let path = output_dir.join(METRICS_OUTPUT_SUFFIX);
info!(target: "reth-bench", "Writing scraped metrics to file: {:?}", path);
let mut writer = Writer::from_path(&path)?;
for row in &self.rows {
writer.serialize(row)?;
}
writer.flush()?;
Ok(())
}
}
/// Parses a Prometheus gauge value from exposition-format text.
///
/// Searches for lines starting with `name` followed by either a space or `{`
/// (for labeled metrics), then parses the numeric value. Returns the last
/// matching sample to handle metrics emitted with multiple label sets.
fn parse_gauge(text: &str, name: &str) -> Option<f64> {
let mut result = None;
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if !line.starts_with(name) {
continue;
}
// Ensure we match the full metric name, not a prefix of another metric.
let rest = &line[name.len()..];
if !rest.starts_with(' ') && !rest.starts_with('{') {
continue;
}
// Format: `metric_name{labels} value [timestamp]` or `metric_name value [timestamp]`
// Value is always the second whitespace-separated token.
let mut parts = line.split_whitespace();
if let Some(value_str) = parts.nth(1) &&
let Ok(v) = value_str.parse::<f64>()
{
result = Some(v);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_gauge_simple() {
let text = r#"# HELP sync_execution_execution_duration Duration of execution
# TYPE sync_execution_execution_duration gauge
sync_execution_execution_duration 0.123456
"#;
assert_eq!(parse_gauge(text, "sync_execution_execution_duration"), Some(0.123456));
}
#[test]
fn test_parse_gauge_missing() {
let text = "some_other_metric 1.0\n";
assert_eq!(parse_gauge(text, "sync_execution_execution_duration"), None);
}
#[test]
fn test_parse_gauge_with_labels() {
let text = "sync_block_validation_state_root_duration{instance=\"node1\"} 0.5\n";
assert_eq!(parse_gauge(text, "sync_block_validation_state_root_duration"), Some(0.5));
}
#[test]
fn test_parse_gauge_prefix_no_false_match() {
let text =
"sync_execution_execution_duration_total 99.0\nsync_execution_execution_duration 0.5\n";
assert_eq!(parse_gauge(text, "sync_execution_execution_duration"), Some(0.5));
}
}

View File

@@ -12,6 +12,7 @@ pub(crate) mod helpers;
pub use generate_big_block::{
RawTransaction, RpcTransactionSource, TransactionCollector, TransactionSource,
};
pub(crate) mod metrics_scraper;
mod new_payload_fcu;
mod new_payload_only;
mod output;

View File

@@ -13,6 +13,7 @@ use crate::{
bench::{
context::BenchContext,
helpers::parse_duration,
metrics_scraper::MetricsScraper,
output::{
write_benchmark_results, CombinedResult, NewPayloadResult, TotalGasOutput, TotalGasRow,
},
@@ -20,9 +21,11 @@ use crate::{
derive_ws_rpc_url, setup_persistence_subscription, PersistenceWaiter,
},
},
valid_payload::{block_to_new_payload, call_forkchoice_updated, call_new_payload_with_reth},
valid_payload::{
block_to_new_payload, call_forkchoice_updated_with_reth, call_new_payload_with_reth,
},
};
use alloy_provider::Provider;
use alloy_provider::{ext::DebugApi, Provider};
use alloy_rpc_types_engine::ForkchoiceState;
use clap::Parser;
use eyre::{Context, OptionExt};
@@ -30,7 +33,7 @@ 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 tracing::{debug, info, warn};
/// `reth benchmark new-payload-fcu` command
#[derive(Debug, Parser)]
@@ -151,12 +154,15 @@ impl Command {
mut next_block,
is_optimism,
use_reth_namespace,
rlp_blocks,
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
let total_blocks = benchmark_mode.total_blocks();
let mut metrics_scraper = MetricsScraper::maybe_new(self.benchmark.metrics_url.clone());
if use_reth_namespace {
info!("Using reth_newPayload endpoint");
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
}
let buffer_size = self.rpc_block_buffer_size;
@@ -181,6 +187,21 @@ impl Command {
}
};
let rlp = if rlp_blocks {
let rlp = match block_provider.debug_get_raw_block(next_block.into()).await {
Ok(rlp) => rlp,
Err(e) => {
tracing::error!(target: "reth-bench", "Failed to fetch raw block {next_block}: {e}");
let _ = error_sender
.send(eyre::eyre!("Failed to fetch raw block {next_block}: {e}"));
break;
}
};
Some(rlp)
} else {
None
};
let head_block_hash = block.header.hash;
let safe_block_hash = block_provider
.get_block_by_number(block.header.number.saturating_sub(32).into());
@@ -202,7 +223,7 @@ impl Command {
next_block += 1;
if let Err(e) = sender
.send((block, head_block_hash, safe_block_hash, finalized_block_hash))
.send((block, head_block_hash, safe_block_hash, finalized_block_hash, rlp))
.await
{
tracing::error!(target: "reth-bench", "Failed to send block data: {e}");
@@ -216,7 +237,7 @@ impl Command {
let total_benchmark_duration = Instant::now();
let mut total_wait_time = Duration::ZERO;
while let Some((block, head, safe, finalized)) = {
while let Some((block, head, safe, finalized, rlp)) = {
let wait_start = Instant::now();
let result = receiver.recv().await;
total_wait_time += wait_start.elapsed();
@@ -235,11 +256,11 @@ impl Command {
finalized_block_hash: finalized,
};
let (version, params, execution_data) = block_to_new_payload(block, is_optimism)?;
let (version, params) =
block_to_new_payload(block, is_optimism, rlp, use_reth_namespace)?;
let start = Instant::now();
let reth_data = use_reth_namespace.then_some(execution_data);
let server_timings =
call_new_payload_with_reth(&auth_provider, version, params, reth_data).await?;
call_new_payload_with_reth(&auth_provider, version, params).await?;
let np_latency =
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
@@ -258,7 +279,7 @@ impl Command {
};
let fcu_start = Instant::now();
call_forkchoice_updated(&auth_provider, version, forkchoice_state, None).await?;
call_forkchoice_updated_with_reth(&auth_provider, version, forkchoice_state).await?;
let fcu_latency = fcu_start.elapsed();
let total_latency = if server_timings.is_some() {
@@ -288,6 +309,12 @@ impl Command {
};
info!(target: "reth-bench", progress, %combined_result);
if let Some(scraper) = metrics_scraper.as_mut() &&
let Err(err) = scraper.scrape_after_block(block_number).await
{
warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
}
if let Some(w) = &mut waiter {
w.on_block(block_number).await?;
}
@@ -313,6 +340,10 @@ impl Command {
write_benchmark_results(path, &gas_output_results, &combined_results)?;
}
if let (Some(path), Some(scraper)) = (&self.benchmark.output, &metrics_scraper) {
scraper.write_csv(path)?;
}
let gas_output =
TotalGasOutput::with_combined_results(gas_output_results, &combined_results)?;

View File

@@ -3,6 +3,7 @@
use crate::{
bench::{
context::BenchContext,
metrics_scraper::MetricsScraper,
output::{
NewPayloadResult, TotalGasOutput, TotalGasRow, GAS_OUTPUT_SUFFIX,
NEW_PAYLOAD_OUTPUT_SUFFIX,
@@ -10,7 +11,7 @@ use crate::{
},
valid_payload::{block_to_new_payload, call_new_payload_with_reth},
};
use alloy_provider::Provider;
use alloy_provider::{ext::DebugApi, Provider};
use clap::Parser;
use csv::Writer;
use eyre::{Context, OptionExt};
@@ -50,10 +51,13 @@ impl Command {
mut next_block,
is_optimism,
use_reth_namespace,
rlp_blocks,
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
let total_blocks = benchmark_mode.total_blocks();
let mut metrics_scraper = MetricsScraper::maybe_new(self.benchmark.metrics_url.clone());
if use_reth_namespace {
info!("Using reth_newPayload endpoint");
}
@@ -80,8 +84,21 @@ impl Command {
}
};
let rlp = if rlp_blocks {
let Ok(rlp) = block_provider.debug_get_raw_block(next_block.into()).await
else {
tracing::error!(target: "reth-bench", "Failed to fetch raw block {next_block}");
let _ = error_sender
.send(eyre::eyre!("Failed to fetch raw block {next_block}"));
break;
};
Some(rlp)
} else {
None
};
next_block += 1;
if let Err(e) = sender.send(block).await {
if let Err(e) = sender.send((block, rlp)).await {
tracing::error!(target: "reth-bench", "Failed to send block data: {e}");
break;
}
@@ -93,7 +110,7 @@ impl Command {
let total_benchmark_duration = Instant::now();
let mut total_wait_time = Duration::ZERO;
while let Some(block) = {
while let Some((block, rlp)) = {
let wait_start = Instant::now();
let result = receiver.recv().await;
total_wait_time += wait_start.elapsed();
@@ -105,12 +122,12 @@ impl Command {
debug!(target: "reth-bench", number=?block.header.number, "Sending payload to engine");
let (version, params, execution_data) = block_to_new_payload(block, is_optimism)?;
let (version, params) =
block_to_new_payload(block, is_optimism, rlp, use_reth_namespace)?;
let start = Instant::now();
let reth_data = use_reth_namespace.then_some(execution_data);
let server_timings =
call_new_payload_with_reth(&auth_provider, version, params, reth_data).await?;
call_new_payload_with_reth(&auth_provider, version, params).await?;
let latency =
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
@@ -142,6 +159,12 @@ impl Command {
let row =
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
results.push((row, new_payload_result));
if let Some(scraper) = metrics_scraper.as_mut() &&
let Err(err) = scraper.scrape_after_block(block_number).await
{
tracing::warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
}
}
// Check if the spawned task encountered an error
@@ -172,6 +195,10 @@ impl Command {
}
writer.flush()?;
if let Some(scraper) = &metrics_scraper {
scraper.write_csv(&path)?;
}
info!(target: "reth-bench", "Finished writing benchmark output files to {:?}.", path);
}

View File

@@ -22,14 +22,14 @@ pub(crate) const NEW_PAYLOAD_OUTPUT_SUFFIX: &str = "new_payload_latency.csv";
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct GasRampPayloadFile {
/// Engine API version (1-5).
pub(crate) version: u8,
///
/// `None` indicates that `reth_newPayload` should be used.
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) version: Option<u8>,
/// The block hash for FCU.
pub(crate) block_hash: B256,
/// The params to pass to newPayload.
pub(crate) params: serde_json::Value,
/// The execution data for `reth_newPayload`.
#[serde(skip_serializing_if = "Option::is_none", default)]
pub(crate) execution_data: Option<alloy_rpc_types_engine::ExecutionData>,
}
/// This represents the results of a single `newPayload` call in the benchmark, containing the gas

View File

@@ -15,6 +15,7 @@ use crate::{
authenticated_transport::AuthenticatedTransportConnect,
bench::{
helpers::parse_duration,
metrics_scraper::MetricsScraper,
output::{
write_benchmark_results, CombinedResult, GasRampPayloadFile, NewPayloadResult,
TotalGasOutput, TotalGasRow,
@@ -23,10 +24,10 @@ use crate::{
derive_ws_rpc_url, setup_persistence_subscription, PersistenceWaiter,
},
},
valid_payload::{call_forkchoice_updated, call_new_payload_with_reth},
valid_payload::{call_forkchoice_updated_with_reth, call_new_payload_with_reth},
};
use alloy_primitives::B256;
use alloy_provider::{ext::EngineApi, network::AnyNetwork, Provider, RootProvider};
use alloy_provider::{network::AnyNetwork, Provider, RootProvider};
use alloy_rpc_client::ClientBuilder;
use alloy_rpc_types_engine::{
CancunPayloadFields, ExecutionData, ExecutionPayloadEnvelopeV4, ExecutionPayloadSidecar,
@@ -37,6 +38,7 @@ use eyre::Context;
use reth_cli_runner::CliContext;
use reth_engine_primitives::config::DEFAULT_PERSISTENCE_THRESHOLD;
use reth_node_api::EngineApiMessageVersion;
use reth_rpc_api::RethNewPayloadInput;
use std::{
path::PathBuf,
time::{Duration, Instant},
@@ -135,6 +137,14 @@ pub struct Command {
/// and returns server-side timing breakdowns (latency, persistence wait, cache wait).
#[arg(long, default_value = "false", verbatim_doc_comment)]
reth_new_payload: bool,
/// Optional Prometheus metrics endpoint to scrape after each block.
///
/// When provided, reth-bench will fetch metrics from this URL after each
/// payload, recording per-block execution and state root durations.
/// Results are written to `metrics.csv` in the output directory.
#[arg(long = "metrics-url", value_name = "URL", verbatim_doc_comment)]
metrics_url: Option<String>,
}
/// A loaded payload ready for execution.
@@ -152,7 +162,9 @@ struct GasRampPayload {
/// Block number from filename.
block_number: u64,
/// Engine API version for newPayload.
version: EngineApiMessageVersion,
///
/// `None` indicates that `reth_newPayload` should be used.
version: Option<EngineApiMessageVersion>,
/// The file contents.
file: GasRampPayloadFile,
}
@@ -175,7 +187,7 @@ impl Command {
);
}
if self.reth_new_payload {
info!("Using reth_newPayload endpoint");
info!("Using reth_newPayload and reth_forkchoiceUpdated endpoints");
}
// Set up waiter based on configured options
@@ -204,6 +216,8 @@ impl Command {
(None, false) => None,
};
let mut metrics_scraper = MetricsScraper::maybe_new(self.metrics_url.clone());
// Set up authenticated engine provider
let jwt =
std::fs::read_to_string(&self.jwt_secret).wrap_err("Failed to read JWT secret file")?;
@@ -262,13 +276,10 @@ impl Command {
"Executing gas ramp payload (newPayload + FCU)"
);
let reth_data =
if self.reth_new_payload { payload.file.execution_data.clone() } else { None };
let _ = call_new_payload_with_reth(
&auth_provider,
payload.version,
payload.file.params.clone(),
reth_data,
)
.await?;
@@ -277,7 +288,7 @@ impl Command {
safe_block_hash: parent_hash,
finalized_block_hash: parent_hash,
};
call_forkchoice_updated(&auth_provider, payload.version, fcu_state, None).await?;
call_forkchoice_updated_with_reth(&auth_provider, payload.version, fcu_state).await?;
info!(target: "reth-bench", gas_ramp_payload = i + 1, "Gas ramp payload executed successfully");
@@ -325,31 +336,34 @@ impl Command {
"Sending newPayload"
);
let params = serde_json::to_value((
execution_payload.clone(),
Vec::<B256>::new(),
B256::ZERO,
envelope.execution_requests.to_vec(),
))?;
let (version, params) = if self.reth_new_payload {
let reth_data = ExecutionData {
payload: execution_payload.clone().into(),
sidecar: ExecutionPayloadSidecar::v4(
CancunPayloadFields {
versioned_hashes: Vec::new(),
parent_beacon_block_root: B256::ZERO,
},
PraguePayloadFields {
requests: envelope.execution_requests.clone().into(),
},
),
};
(None, serde_json::to_value((RethNewPayloadInput::ExecutionData(reth_data),))?)
} else {
(
Some(EngineApiMessageVersion::V4),
serde_json::to_value((
execution_payload.clone(),
Vec::<B256>::new(),
B256::ZERO,
envelope.execution_requests.to_vec(),
))?,
)
};
let reth_data = self.reth_new_payload.then(|| ExecutionData {
payload: execution_payload.clone().into(),
sidecar: ExecutionPayloadSidecar::v4(
CancunPayloadFields {
versioned_hashes: Vec::new(),
parent_beacon_block_root: B256::ZERO,
},
PraguePayloadFields { requests: envelope.execution_requests.clone().into() },
),
});
let server_timings = call_new_payload_with_reth(
&auth_provider,
EngineApiMessageVersion::V4,
params,
reth_data,
)
.await?;
let server_timings =
call_new_payload_with_reth(&auth_provider, version, params).await?;
let np_latency =
server_timings.as_ref().map(|t| t.latency).unwrap_or_else(|| start.elapsed());
@@ -373,10 +387,8 @@ impl Command {
finalized_block_hash: parent_hash,
};
debug!(target: "reth-bench", method = "engine_forkchoiceUpdatedV3", ?fcu_state, "Sending forkchoiceUpdated");
let fcu_start = Instant::now();
let fcu_result = auth_provider.fork_choice_updated_v3(fcu_state, None).await?;
call_forkchoice_updated_with_reth(&auth_provider, version, fcu_state).await?;
let fcu_latency = fcu_start.elapsed();
let total_latency =
@@ -395,6 +407,12 @@ impl Command {
let progress = format!("{}/{}", i + 1, payloads.len());
info!(target: "reth-bench", progress, %combined_result);
if let Some(scraper) = metrics_scraper.as_mut() &&
let Err(err) = scraper.scrape_after_block(block_number).await
{
tracing::warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
}
if let Some(w) = &mut waiter {
w.on_block(block_number).await?;
}
@@ -403,7 +421,6 @@ impl Command {
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
results.push((gas_row, combined_result));
debug!(target: "reth-bench", ?fcu_result, "Payload executed successfully");
parent_hash = block_hash;
}
@@ -418,6 +435,10 @@ impl Command {
write_benchmark_results(path, &gas_output_results, &combined_results)?;
}
if let (Some(path), Some(scraper)) = (&self.output, &metrics_scraper) {
scraper.write_csv(path)?;
}
let gas_output =
TotalGasOutput::with_combined_results(gas_output_results, &combined_results)?;
info!(
@@ -528,13 +549,18 @@ impl Command {
let file: GasRampPayloadFile = serde_json::from_str(&content)
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
let version = match file.version {
1 => EngineApiMessageVersion::V1,
2 => EngineApiMessageVersion::V2,
3 => EngineApiMessageVersion::V3,
4 => EngineApiMessageVersion::V4,
5 => EngineApiMessageVersion::V5,
v => return Err(eyre::eyre!("Invalid version {} in {:?}", v, path)),
let version = if let Some(version) = file.version {
match version {
1 => EngineApiMessageVersion::V1,
2 => EngineApiMessageVersion::V2,
3 => EngineApiMessageVersion::V3,
4 => EngineApiMessageVersion::V4,
5 => EngineApiMessageVersion::V5,
v => return Err(eyre::eyre!("Invalid version {} in {:?}", v, path)),
}
.into()
} else {
None
};
info!(

View File

@@ -3,7 +3,7 @@
//! before sending additional calls.
use alloy_eips::eip7685::Requests;
use alloy_primitives::B256;
use alloy_primitives::{Bytes, B256};
use alloy_provider::{ext::EngineApi, network::AnyRpcBlock, Network, Provider};
use alloy_rpc_types_engine::{
ExecutionData, ExecutionPayload, ExecutionPayloadInputV2, ExecutionPayloadSidecar,
@@ -12,6 +12,7 @@ use alloy_rpc_types_engine::{
use alloy_transport::TransportResult;
use op_alloy_rpc_types_engine::OpExecutionPayloadV4;
use reth_node_api::EngineApiMessageVersion;
use reth_rpc_api::RethNewPayloadInput;
use serde::Deserialize;
use std::time::Duration;
use tracing::{debug, error};
@@ -169,7 +170,15 @@ where
pub(crate) fn block_to_new_payload(
block: AnyRpcBlock,
is_optimism: bool,
) -> eyre::Result<(EngineApiMessageVersion, serde_json::Value, ExecutionData)> {
rlp: Option<Bytes>,
reth_new_payload: bool,
) -> eyre::Result<(Option<EngineApiMessageVersion>, serde_json::Value)> {
if let Some(rlp) = rlp {
return Ok((
None,
serde_json::to_value((RethNewPayloadInput::<ExecutionData>::BlockRlp(rlp),))?,
));
}
let block = block
.into_inner()
.map_header(|header| header.map(|h| h.into_header_with_defaults()))
@@ -181,7 +190,14 @@ pub(crate) fn block_to_new_payload(
// Convert to execution payload
let (payload, sidecar) = ExecutionPayload::from_block_slow(&block);
payload_to_new_payload(payload, sidecar, is_optimism, block.withdrawals_root, None)
let (version, params, execution_data) =
payload_to_new_payload(payload, sidecar, is_optimism, block.withdrawals_root, None)?;
if reth_new_payload {
Ok((None, serde_json::to_value((RethNewPayloadInput::ExecutionData(execution_data),))?))
} else {
Ok((Some(version), params))
}
}
/// Converts an execution payload and sidecar into versioned engine API params and an
@@ -266,17 +282,15 @@ pub(crate) fn payload_to_new_payload(
#[allow(dead_code)]
pub(crate) async fn call_new_payload<N: Network, P: Provider<N>>(
provider: P,
version: EngineApiMessageVersion,
version: Option<EngineApiMessageVersion>,
params: serde_json::Value,
) -> TransportResult<Option<NewPayloadTimingBreakdown>> {
call_new_payload_with_reth(provider, version, params, None).await
) -> eyre::Result<Option<NewPayloadTimingBreakdown>> {
call_new_payload_with_reth(provider, version, params).await
}
/// Response from `reth_newPayload` endpoint, which includes server-measured latency.
#[derive(Debug, Deserialize)]
struct RethPayloadStatus {
#[serde(flatten)]
status: PayloadStatus,
latency_us: u64,
#[serde(default)]
persistence_wait_us: Option<u64>,
@@ -300,72 +314,50 @@ pub(crate) struct NewPayloadTimingBreakdown {
}
/// Calls either `engine_newPayload*` or `reth_newPayload` depending on whether
/// `reth_execution_data` is provided.
/// `version` is provided.
///
/// When `reth_execution_data` is `Some`, uses the `reth_newPayload` endpoint which takes
/// `ExecutionData` directly and waits for persistence and cache updates to complete.
/// When `version` is `None`, uses `reth_newPayload` endpoint with provided params.
///
/// Returns the server-reported timing breakdown when using the reth namespace, or `None` for
/// the standard engine namespace.
pub(crate) async fn call_new_payload_with_reth<N: Network, P: Provider<N>>(
provider: P,
version: EngineApiMessageVersion,
version: Option<EngineApiMessageVersion>,
params: serde_json::Value,
reth_execution_data: Option<ExecutionData>,
) -> TransportResult<Option<NewPayloadTimingBreakdown>> {
if let Some(execution_data) = reth_execution_data {
let method = "reth_newPayload";
let reth_params = serde_json::to_value((execution_data.clone(),))
.expect("ExecutionData serialization cannot fail");
) -> eyre::Result<Option<NewPayloadTimingBreakdown>> {
let method = version.map(|v| v.method_name()).unwrap_or("reth_newPayload");
debug!(target: "reth-bench", method, "Sending newPayload");
debug!(target: "reth-bench", method, "Sending newPayload");
let mut resp: RethPayloadStatus = provider.client().request(method, &reth_params).await?;
let resp = loop {
let resp: serde_json::Value = provider.client().request(method, &params).await?;
let status = PayloadStatus::deserialize(&resp)?;
while !resp.status.is_valid() {
if resp.status.is_invalid() {
error!(target: "reth-bench", status=?resp.status, "Invalid {method}");
return Err(alloy_json_rpc::RpcError::LocalUsageError(Box::new(
std::io::Error::other(format!("Invalid {method}: {:?}", resp.status)),
)))
}
if resp.status.is_syncing() {
return Err(alloy_json_rpc::RpcError::UnsupportedFeature(
"invalid range: no canonical state found for parent of requested block",
))
}
resp = provider.client().request(method, &reth_params).await?;
if status.is_valid() {
break resp;
}
Ok(Some(NewPayloadTimingBreakdown {
latency: Duration::from_micros(resp.latency_us),
persistence_wait: resp.persistence_wait_us.map(Duration::from_micros),
execution_cache_wait: Duration::from_micros(resp.execution_cache_wait_us),
sparse_trie_wait: Duration::from_micros(resp.sparse_trie_wait_us),
}))
} else {
let method = version.method_name();
debug!(target: "reth-bench", method, "Sending newPayload");
let mut status: PayloadStatus = provider.client().request(method, &params).await?;
while !status.is_valid() {
if status.is_invalid() {
error!(target: "reth-bench", ?status, ?params, "Invalid {method}",);
return Err(alloy_json_rpc::RpcError::LocalUsageError(Box::new(
std::io::Error::other(format!("Invalid {method}: {status:?}")),
)))
}
if status.is_syncing() {
return Err(alloy_json_rpc::RpcError::UnsupportedFeature(
"invalid range: no canonical state found for parent of requested block",
))
}
status = provider.client().request(method, &params).await?;
if status.is_invalid() {
return Err(eyre::eyre!("Invalid {method}: {status:?}"));
}
Ok(None)
if status.is_syncing() {
return Err(eyre::eyre!(
"invalid range: no canonical state found for parent of requested block"
));
}
};
if version.is_some() {
return Ok(None);
}
let resp: RethPayloadStatus = serde_json::from_value(resp)?;
Ok(Some(NewPayloadTimingBreakdown {
latency: Duration::from_micros(resp.latency_us),
persistence_wait: resp.persistence_wait_us.map(Duration::from_micros),
execution_cache_wait: Duration::from_micros(resp.execution_cache_wait_us),
sparse_trie_wait: Duration::from_micros(resp.sparse_trie_wait_us),
}))
}
/// Calls the correct `engine_forkchoiceUpdated` method depending on the given
@@ -392,3 +384,47 @@ pub(crate) async fn call_forkchoice_updated<N, P: EngineApiValidWaitExt<N>>(
}
}
}
/// Calls either `reth_forkchoiceUpdated` or the standard `engine_forkchoiceUpdated*` depending
/// on `use_reth`.
///
/// When `use_reth` is true, uses the `reth_forkchoiceUpdated` endpoint which sends a regular FCU
/// with no payload attributes.
pub(crate) async fn call_forkchoice_updated_with_reth<
N: Network,
P: Provider<N> + EngineApiValidWaitExt<N>,
>(
provider: P,
message_version: Option<EngineApiMessageVersion>,
forkchoice_state: ForkchoiceState,
) -> TransportResult<ForkchoiceUpdated> {
if let Some(message_version) = message_version {
call_forkchoice_updated(provider, message_version, forkchoice_state, None).await
} else {
let method = "reth_forkchoiceUpdated";
let reth_params = serde_json::to_value((forkchoice_state,))
.expect("ForkchoiceState serialization cannot fail");
debug!(target: "reth-bench", method, "Sending forkchoiceUpdated");
loop {
let resp: ForkchoiceUpdated = provider.client().request(method, &reth_params).await?;
if resp.is_valid() {
break Ok(resp)
}
if resp.is_invalid() {
error!(target: "reth-bench", ?resp, "Invalid {method}");
return Err(alloy_json_rpc::RpcError::LocalUsageError(Box::new(
std::io::Error::other(format!("Invalid {method}: {resp:?}")),
)))
}
if resp.is_syncing() {
return Err(alloy_json_rpc::RpcError::UnsupportedFeature(
"invalid range: no canonical state found for parent of requested block",
))
}
}
}
}

View File

@@ -30,7 +30,8 @@ workspace = true
# reth
reth-ethereum-cli.workspace = true
reth-chainspec.workspace = true
reth-primitives.workspace = true
reth-primitives-traits.workspace = true
reth-ethereum-primitives.workspace = true
reth-db = { workspace = true, features = ["mdbx"] }
reth-provider.workspace = true
reth-revm.workspace = true
@@ -110,7 +111,6 @@ dev = ["reth-ethereum-cli/dev"]
asm-keccak = [
"reth-node-core/asm-keccak",
"reth-primitives/asm-keccak",
"reth-ethereum-cli/asm-keccak",
"reth-node-ethereum/asm-keccak",
"alloy-primitives/asm-keccak",

View File

@@ -124,9 +124,11 @@ pub mod providers {
pub use reth_provider::*;
}
/// Re-exported from `reth_primitives`.
/// Re-exported primitives.
#[allow(ambiguous_glob_reexports)]
pub mod primitives {
pub use reth_primitives::*;
pub use reth_ethereum_primitives::*;
pub use reth_primitives_traits::*;
}
/// Re-exported from `reth_ethereum_consensus`.

View File

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

View File

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

View File

@@ -132,6 +132,6 @@ impl<H: BlockHeader> EthChainSpec for ChainSpec<H> {
}
fn final_paris_total_difficulty(&self) -> Option<U256> {
self.paris_block_and_final_difficulty.map(|(_, final_difficulty)| final_difficulty)
self.get_final_paris_total_difficulty()
}
}

View File

@@ -855,15 +855,9 @@ impl From<Genesis> for ChainSpec {
// those networks we use the activation
// blocks of those networks
match genesis.config.chain_id {
1 => {
if ttd == MAINNET_PARIS_TTD {
return Some(MAINNET_PARIS_BLOCK)
}
}
11155111 => {
if ttd == SEPOLIA_PARIS_TTD {
return Some(SEPOLIA_PARIS_BLOCK)
}
1 if ttd == MAINNET_PARIS_TTD => return Some(MAINNET_PARIS_BLOCK),
11155111 if ttd == SEPOLIA_PARIS_TTD => {
return Some(SEPOLIA_PARIS_BLOCK)
}
_ => {}
};

View File

@@ -18,6 +18,5 @@ alloy-genesis.workspace = true
# misc
clap.workspace = true
shellexpand.workspace = true
eyre.workspace = true
serde_json.workspace = true

View File

@@ -73,7 +73,7 @@ pub trait ChainSpecParser: Clone + Send + Sync + 'static {
/// A helper to parse a [`Genesis`](alloy_genesis::Genesis) as argument or from disk.
pub fn parse_genesis(s: &str) -> eyre::Result<alloy_genesis::Genesis> {
// try to read json from path first
let raw = match fs::read_to_string(PathBuf::from(shellexpand::full(s)?.into_owned())) {
let raw = match fs::read_to_string(PathBuf::from(s)) {
Ok(raw) => raw,
Err(io_err) => {
// valid json may start with "\n", but must contain "{"

View File

@@ -43,7 +43,7 @@ reth-node-metrics.workspace = true
reth-ethereum-primitives = { workspace = true, optional = true }
reth-provider.workspace = true
reth-prune.workspace = true
reth-prune-types = { workspace = true, optional = true }
reth-prune-types.workspace = true
reth-revm.workspace = true
reth-stages.workspace = true
reth-stages-types = { workspace = true, optional = true }
@@ -53,7 +53,7 @@ reth-tasks.workspace = true
reth-storage-api.workspace = true
reth-trie = { workspace = true, features = ["metrics"] }
reth-trie-db = { workspace = true, features = ["metrics"] }
reth-trie-common.workspace = true
reth-trie-common = { workspace = true, optional = true }
reth-primitives-traits.workspace = true
reth-discv4.workspace = true
reth-discv5.workspace = true
@@ -113,6 +113,7 @@ arbitrary = [
"dep:proptest",
"dep:arbitrary",
"dep:proptest-arbitrary-interop",
"dep:reth-trie-common",
"reth-db-api/arbitrary",
"reth-eth-wire/arbitrary",
"reth-db/arbitrary",
@@ -123,11 +124,11 @@ arbitrary = [
"reth-codecs/test-utils",
"reth-prune-types/test-utils",
"reth-stages-types/test-utils",
"reth-trie-common/test-utils",
"reth-trie-common?/test-utils",
"reth-codecs/arbitrary",
"reth-prune-types?/arbitrary",
"reth-prune-types/arbitrary",
"reth-stages-types?/arbitrary",
"reth-trie-common/arbitrary",
"reth-trie-common?/arbitrary",
"alloy-consensus/arbitrary",
"reth-primitives-traits/arbitrary",
"reth-ethereum-primitives/arbitrary",

View File

@@ -89,13 +89,15 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
/// Initializes environment according to [`AccessRights`] and returns an instance of
/// [`Environment`].
///
/// Internally builds a [`reth_tasks::Runtime`] attached to the current tokio handle for
/// parallel storage I/O.
pub fn init<N: CliNodeTypes>(&self, access: AccessRights) -> eyre::Result<Environment<N>>
/// The provided `runtime` is used for parallel storage I/O.
pub fn init<N: CliNodeTypes>(
&self,
access: AccessRights,
runtime: reth_tasks::Runtime,
) -> eyre::Result<Environment<N>>
where
C: ChainSpecParser<ChainSpec = N::ChainSpec>,
{
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
let data_dir = self.datadir.clone().resolve_datadir(self.chain.chain());
let db_path = data_dir.db();
let sf_path = data_dir.static_files();
@@ -144,11 +146,24 @@ impl<C: ChainSpecParser> EnvironmentArgs<C> {
})
}
};
let rocksdb_provider = RocksDBProvider::builder(data_dir.rocksdb())
.with_default_tables()
.with_database_log_level(self.db.log_level)
.with_read_only(!access.is_read_write())
.build()?;
let rocksdb_provider = if !access.is_read_write() && !RocksDBProvider::exists(&rocksdb_path)
{
// RocksDB database doesn't exist yet (e.g. datadir restored from a snapshot
// or created before RocksDB storage). Create an empty one so read-only
// commands can proceed.
debug!(target: "reth::cli", ?rocksdb_path, "RocksDB not found, initializing empty database");
reth_fs_util::create_dir_all(&rocksdb_path)?;
RocksDBProvider::builder(data_dir.rocksdb())
.with_default_tables()
.with_database_log_level(self.db.log_level)
.build()?
} else {
RocksDBProvider::builder(data_dir.rocksdb())
.with_default_tables()
.with_database_log_level(self.db.log_level)
.with_read_only(!access.is_read_write())
.build()?
};
let provider_factory =
self.create_provider_factory(&config, db, sfp, rocksdb_provider, access, runtime)?;

View File

@@ -14,7 +14,7 @@ use reth_db_api::{
use reth_db_common::DbTool;
use reth_node_builder::{NodeTypesWithDB, NodeTypesWithDBAdapter};
use reth_provider::{providers::ProviderNodeTypes, DBProvider, StaticFileProviderFactory};
use reth_static_file_types::StaticFileSegment;
use reth_static_file_types::{ChangesetOffset, StaticFileSegment};
use std::{
hash::{BuildHasher, Hasher},
time::{Duration, Instant},
@@ -134,12 +134,12 @@ fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
.ok_or_else(|| eyre::eyre!("No static files found for segment: {}", segment))?;
let start_time = Instant::now();
let mut hasher = checksum_hasher();
let mut total = 0usize;
let limit = limit.unwrap_or(usize::MAX);
let mut checksummer = Checksummer::new(checksum_hasher(), limit);
let start_block = start_block.unwrap_or(0);
let end_block = end_block.unwrap_or(u64::MAX);
let is_change_based = segment.is_change_based();
info!(
"Computing checksum for {} static files, start_block={}, end_block={}, limit={:?}",
@@ -149,7 +149,8 @@ fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
if limit == usize::MAX { None } else { Some(limit) }
);
'outer: for (block_range, _header) in ranges.iter().sorted_by_key(|(range, _)| range.start()) {
let mut reached_limit = false;
for (block_range, _header) in ranges.iter().sorted_by_key(|(range, _)| range.start()) {
if block_range.end() < start_block || block_range.start() > end_block {
continue;
}
@@ -167,28 +168,42 @@ fn checksum_static_file<N: CliNodeTypes<ChainSpec: EthereumHardforks>>(
let mut cursor = jar_provider.cursor()?;
while let Ok(Some(row)) = cursor.next_row() {
for col_data in row.iter() {
hasher.write(col_data);
}
if is_change_based {
let offsets = jar_provider.read_changeset_offsets()?.ok_or_else(|| {
eyre::eyre!(
"Missing changeset offsets sidecar for segment {} at range {}",
segment,
block_range
)
})?;
let input = ChangeBasedChecksumInput {
segment,
block_range_start: block_range.start(),
start_block,
end_block,
offsets: &offsets,
};
total += 1;
if total.is_multiple_of(PROGRESS_LOG_INTERVAL) {
info!("Hashed {total} entries.");
}
if total >= limit {
break 'outer;
reached_limit = checksum_change_based_segment(&mut checksummer, input, &mut cursor)?;
} else {
while let Some(row) = cursor.next_row()? {
if checksummer.write_row(&row) {
reached_limit = true;
break;
}
}
}
// Explicitly drop provider before removing from cache to avoid deadlock
drop(jar_provider);
static_file_provider.remove_cached_provider(segment, fixed_block_range.end());
if reached_limit {
break;
}
}
let checksum = hasher.finish();
let (checksum, total) = checksummer.finish();
let elapsed = start_time.elapsed();
info!(
@@ -267,7 +282,7 @@ impl<N: ProviderNodeTypes> TableViewer<(u64, Duration)> for ChecksumViewer<'_, N
total = index + 1;
if total >= limit {
break
break;
}
}
@@ -285,3 +300,139 @@ impl<N: ProviderNodeTypes> TableViewer<(u64, Duration)> for ChecksumViewer<'_, N
Ok((checksum, elapsed))
}
}
/// Accumulates a checksum over key-value entries, tracking count and limit.
struct Checksummer<H> {
hasher: H,
total: usize,
limit: usize,
}
impl<H: Hasher> Checksummer<H> {
fn new(hasher: H, limit: usize) -> Self {
Self { hasher, total: 0, limit }
}
/// Hash a row's columns (non-changeset segments). Returns `true` if the limit is reached.
fn write_row(&mut self, row: &[&[u8]]) -> bool {
for col in row {
self.hasher.write(col);
}
self.advance()
}
/// Hash a key + value as two separate writes, matching MDBX raw entry semantics.
/// Write boundaries matter: foldhash rotates its accumulator by `len` on each `write`.
fn write_entry(&mut self, key: &[u8], value: &[u8]) -> bool {
self.hasher.write(key);
self.hasher.write(value);
self.advance()
}
fn advance(&mut self) -> bool {
self.total += 1;
if self.total.is_multiple_of(PROGRESS_LOG_INTERVAL) {
info!("Hashed {} entries.", self.total);
}
self.total >= self.limit
}
fn finish(self) -> (u64, usize) {
(self.hasher.finish(), self.total)
}
}
/// Reconstruct MDBX `StorageChangeSets` key/value boundaries from a static-file row.
///
/// MDBX layout:
/// - key: `BlockNumberAddress` => `[8B block_number][20B address]`
/// - value: `StorageEntry` => `[32B storage_key][compact U256 value]`
///
/// Static-file row layout for `StorageBeforeTx`:
/// - `[20B address][32B storage_key][compact U256 value]`
fn split_storage_changeset_row(block_number: u64, row: &[u8]) -> eyre::Result<([u8; 28], &[u8])> {
if row.len() < 20 {
return Err(eyre::eyre!(
"Storage changeset row too short: expected at least 20 bytes, got {}",
row.len()
));
}
let mut key_buf = [0u8; 28];
key_buf[..8].copy_from_slice(&block_number.to_be_bytes());
key_buf[8..].copy_from_slice(&row[..20]);
Ok((key_buf, &row[20..]))
}
struct ChangeBasedChecksumInput<'a> {
segment: StaticFileSegment,
block_range_start: u64,
start_block: u64,
end_block: u64,
offsets: &'a [ChangesetOffset],
}
fn checksum_change_based_segment<H: Hasher>(
checksummer: &mut Checksummer<H>,
input: ChangeBasedChecksumInput<'_>,
cursor: &mut reth_db::static_file::StaticFileCursor<'_>,
) -> eyre::Result<bool> {
let ChangeBasedChecksumInput { segment, block_range_start, start_block, end_block, offsets } =
input;
let is_storage = segment.is_storage_change_sets();
let mut reached_limit = false;
for (offset_index, offset) in offsets.iter().enumerate() {
let block_number = block_range_start + offset_index as u64;
let include = block_number >= start_block && block_number <= end_block;
for _ in 0..offset.num_changes() {
let row = cursor.next_row()?.ok_or_else(|| {
eyre::eyre!(
"Unexpected EOF while checksumming {} static file at range starting {}",
segment,
block_range_start
)
})?;
if !include {
continue;
}
// Reconstruct MDBX key/value write boundaries. foldhash rotates
// its accumulator by `len` on each write(), so boundaries must
// match exactly.
let done = if is_storage {
// StorageChangeSets: MDBX key = BlockNumberAddress (28B),
// value = compact StorageEntry. Column 0 is compact
// StorageBeforeTx = [20B address][32B key][compact U256].
let col = row[0];
let (key, value) = split_storage_changeset_row(block_number, col)?;
checksummer.write_entry(&key, value)
} else {
// AccountChangeSets: MDBX key = BlockNumber (8B),
// value = compact AccountBeforeTx (= column 0).
checksummer.write_entry(&block_number.to_be_bytes(), row[0])
};
if done {
reached_limit = true;
break;
}
}
if reached_limit {
break;
}
}
if !reached_limit && cursor.next_row()?.is_some() {
return Err(eyre::eyre!(
"Changeset offsets do not cover all rows for {} at range starting {}",
segment,
block_range_start
));
}
Ok(reached_limit)
}

View File

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

View File

@@ -16,8 +16,10 @@ mod copy;
mod diff;
mod get;
mod list;
mod prune_checkpoints;
mod repair_trie;
mod settings;
mod stage_checkpoints;
mod state;
mod static_file_header;
mod stats;
@@ -67,6 +69,10 @@ pub enum Subcommands {
Path,
/// Manage storage settings
Settings(settings::Command),
/// View or set prune checkpoints
PruneCheckpoints(prune_checkpoints::Command),
// View or set stage checkpoints
StageCheckpoints(stage_checkpoints::Command),
/// Gets storage size information for an account
AccountStorage(account_storage::Command),
/// Gets account state and storage at a specific block
@@ -83,7 +89,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
/// provided command.
macro_rules! db_exec {
($env:expr, $tool:ident, $N:ident, $access_rights:expr, $command:block) => {
let Environment { provider_factory, .. } = $env.init::<$N>($access_rights)?;
let Environment { provider_factory, .. } =
$env.init::<$N>($access_rights, ctx.task_executor.clone())?;
let $tool = DbTool::new(provider_factory)?;
$command;
@@ -204,6 +211,16 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
command.execute(&tool)?;
});
}
Subcommands::PruneCheckpoints(command) => {
db_exec!(self.env, tool, N, command.access_rights(), {
command.execute(&tool)?;
});
}
Subcommands::StageCheckpoints(command) => {
db_exec!(self.env, tool, N, command.access_rights(), {
command.execute(&tool)?;
});
}
Subcommands::AccountStorage(command) => {
db_exec!(self.env, tool, N, AccessRights::RO, {
command.execute(&tool)?;

View File

@@ -0,0 +1,221 @@
//! `reth db prune-checkpoints` command for viewing and setting prune checkpoint values.
use clap::{Args, Parser, Subcommand, ValueEnum};
use reth_db_common::DbTool;
use reth_provider::{providers::ProviderNodeTypes, DBProvider, DatabaseProviderFactory};
use reth_prune_types::{PruneCheckpoint, PruneMode, PruneSegment};
use reth_storage_api::{PruneCheckpointReader, PruneCheckpointWriter};
use crate::common::AccessRights;
/// `reth db prune-checkpoints` subcommand
#[derive(Debug, Parser)]
pub struct Command {
#[command(subcommand)]
command: Subcommands,
}
impl Command {
/// Returns database access rights required for the command.
pub fn access_rights(&self) -> AccessRights {
match &self.command {
Subcommands::Get { .. } => AccessRights::RO,
Subcommands::Set(_) => AccessRights::RW,
}
}
}
#[derive(Debug, Subcommand)]
enum Subcommands {
/// Get prune checkpoint(s) from database.
///
/// Shows the current prune progress for each segment, including the highest
/// pruned block/tx number and the active prune mode.
Get {
/// Specific segment to query. If omitted, shows all segments.
#[arg(long, value_enum)]
segment: Option<SegmentArg>,
},
/// Set a prune checkpoint for a segment.
///
/// WARNING: Manually setting checkpoints can cause data inconsistencies.
/// Only use this if you know what you're doing (e.g., recovering from a
/// corrupted checkpoint or forcing a re-prune from a specific block).
Set(SetArgs),
}
/// Arguments for the `set` subcommand
#[derive(Debug, Args)]
pub struct SetArgs {
/// The prune segment to update
#[arg(long, value_enum)]
segment: SegmentArg,
/// Highest pruned block number
#[arg(long)]
block_number: Option<u64>,
/// Highest pruned transaction number
#[arg(long)]
tx_number: Option<u64>,
/// Prune mode to write: full, distance, or before
#[arg(long, value_enum)]
mode: PruneModeArg,
/// Value for distance or before mode (required unless mode is full)
#[arg(long, required_if_eq_any([("mode", "distance"), ("mode", "before")]))]
mode_value: Option<u64>,
}
/// CLI-friendly prune segment names (excludes deprecated variants)
#[derive(Debug, Clone, Copy, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum SegmentArg {
SenderRecovery,
TransactionLookup,
Receipts,
ContractLogs,
AccountHistory,
StorageHistory,
Bodies,
}
impl From<SegmentArg> for PruneSegment {
fn from(arg: SegmentArg) -> Self {
match arg {
SegmentArg::SenderRecovery => Self::SenderRecovery,
SegmentArg::TransactionLookup => Self::TransactionLookup,
SegmentArg::Receipts => Self::Receipts,
SegmentArg::ContractLogs => Self::ContractLogs,
SegmentArg::AccountHistory => Self::AccountHistory,
SegmentArg::StorageHistory => Self::StorageHistory,
SegmentArg::Bodies => Self::Bodies,
}
}
}
/// CLI-friendly prune mode
#[derive(Debug, Clone, Copy, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum PruneModeArg {
/// Prune all blocks
Full,
/// Keep the last N blocks (requires --mode-value)
Distance,
/// Prune blocks before a specific block number (requires --mode-value)
Before,
}
impl Command {
/// Execute the command
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
match self.command {
Subcommands::Get { segment } => Self::get(tool, segment),
Subcommands::Set(args) => Self::set(tool, args),
}
}
fn get<N: ProviderNodeTypes>(
tool: &DbTool<N>,
segment: Option<SegmentArg>,
) -> eyre::Result<()> {
let provider = tool.provider_factory.provider()?;
match segment {
Some(seg) => {
let segment: PruneSegment = seg.into();
match provider.get_prune_checkpoint(segment)? {
Some(checkpoint) => print_checkpoint(segment, &checkpoint),
None => println!("No checkpoint found for {segment}"),
}
}
None => {
let mut checkpoints = provider.get_prune_checkpoints()?;
checkpoints.sort_by_key(|(seg, _)| *seg);
if checkpoints.is_empty() {
println!("No prune checkpoints found.");
} else {
println!(
"{:<25} {:>15} {:>15} {:>20}",
"Segment", "Block Number", "Tx Number", "Prune Mode"
);
println!("{}", "-".repeat(80));
for (segment, checkpoint) in &checkpoints {
println!(
"{:<25} {:>15} {:>15} {:>20}",
segment.to_string(),
fmt_opt(checkpoint.block_number),
fmt_opt(checkpoint.tx_number),
fmt_mode(&checkpoint.prune_mode),
);
}
}
}
}
Ok(())
}
fn set<N: ProviderNodeTypes>(tool: &DbTool<N>, args: SetArgs) -> eyre::Result<()> {
eyre::ensure!(
args.block_number.is_some() || args.tx_number.is_some(),
"at least one of --block-number or --tx-number must be provided"
);
let prune_mode = match args.mode {
PruneModeArg::Full => PruneMode::Full,
PruneModeArg::Distance => PruneMode::Distance(
args.mode_value
.ok_or_else(|| eyre::eyre!("--mode-value is required for distance mode"))?,
),
PruneModeArg::Before => PruneMode::Before(
args.mode_value
.ok_or_else(|| eyre::eyre!("--mode-value is required for before mode"))?,
),
};
let segment: PruneSegment = args.segment.into();
let checkpoint = PruneCheckpoint {
block_number: args.block_number,
tx_number: args.tx_number,
prune_mode,
};
let provider_rw = tool.provider_factory.database_provider_rw()?;
// Show previous value if any
if let Some(prev) = provider_rw.get_prune_checkpoint(segment)? {
println!("Previous checkpoint for {segment}:");
print_checkpoint(segment, &prev);
println!();
}
provider_rw.save_prune_checkpoint(segment, checkpoint)?;
provider_rw.commit()?;
println!("Updated checkpoint for {segment}:");
print_checkpoint(segment, &checkpoint);
Ok(())
}
}
fn print_checkpoint(segment: PruneSegment, checkpoint: &PruneCheckpoint) {
println!(" Segment: {segment}");
println!(" Block Number: {}", fmt_opt(checkpoint.block_number));
println!(" Tx Number: {}", fmt_opt(checkpoint.tx_number));
println!(" Prune Mode: {}", fmt_mode(&checkpoint.prune_mode));
}
fn fmt_opt(val: Option<u64>) -> String {
val.map_or("-".to_string(), |n| n.to_string())
}
fn fmt_mode(mode: &PruneMode) -> String {
match mode {
PruneMode::Full => "Full".to_string(),
PruneMode::Distance(d) => format!("Distance({d})"),
PruneMode::Before(b) => format!("Before({b})"),
}
}

View File

@@ -5,7 +5,6 @@ use reth_cli_util::parse_socket_address;
use reth_db_api::{
cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO},
database::Database,
tables,
transaction::{DbTx, DbTxMut},
};
use reth_db_common::DbTool;
@@ -21,13 +20,15 @@ use reth_node_metrics::{
};
use reth_provider::{providers::ProviderNodeTypes, ChainSpecProvider, StageCheckpointReader};
use reth_stages::StageId;
use reth_storage_api::StorageSettingsCache;
use reth_tasks::TaskExecutor;
use reth_trie::{
verify::{Output, Verifier},
Nibbles,
};
use reth_trie_common::{StorageTrieEntry, StoredNibbles, StoredNibblesSubKey};
use reth_trie_db::{DatabaseHashedCursorFactory, DatabaseTrieCursorFactory};
use reth_trie_db::{
DatabaseHashedCursorFactory, DatabaseTrieCursorFactory, StorageTrieEntryLike, TrieTableAdapter,
};
use std::{
net::SocketAddr,
time::{Duration, Instant},
@@ -116,9 +117,13 @@ fn verify_only<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()> {
let mut tx = db.tx()?;
tx.disable_long_read_transaction_safety();
reth_trie_db::with_adapter!(tool.provider_factory, |A| do_verify_only::<_, A>(&tx))
}
fn do_verify_only<TX: DbTx, A: TrieTableAdapter>(tx: &TX) -> eyre::Result<()> {
// Create the verifier
let hashed_cursor_factory = DatabaseHashedCursorFactory::new(&tx);
let trie_cursor_factory = DatabaseTrieCursorFactory::new(&tx);
let hashed_cursor_factory = DatabaseHashedCursorFactory::new(tx);
let trie_cursor_factory = DatabaseTrieCursorFactory::<_, A>::new(tx);
let verifier = Verifier::new(&trie_cursor_factory, hashed_cursor_factory)?;
let metrics = RepairTrieMetrics::new();
@@ -209,17 +214,37 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
// Check that a pipeline sync isn't in progress.
verify_checkpoints(provider_rw.as_ref())?;
let inconsistent_nodes = reth_trie_db::with_adapter!(tool.provider_factory, |A| {
do_verify_and_repair::<_, A>(&mut provider_rw)?
});
if inconsistent_nodes == 0 {
info!("No inconsistencies found");
} else {
provider_rw.commit()?;
info!("Repaired {} inconsistencies and committed changes", inconsistent_nodes);
}
Ok(())
}
fn do_verify_and_repair<N: ProviderNodeTypes, A: TrieTableAdapter>(
provider_rw: &mut reth_provider::DatabaseProviderRW<N::DB, N>,
) -> eyre::Result<usize>
where
<N::DB as reth_db_api::database::Database>::TXMut: DbTxMut + DbTx,
{
// Create cursors for making modifications with
let tx = provider_rw.tx_mut();
tx.disable_long_read_transaction_safety();
let mut account_trie_cursor = tx.cursor_write::<tables::AccountsTrie>()?;
let mut storage_trie_cursor = tx.cursor_dup_write::<tables::StoragesTrie>()?;
let mut account_trie_cursor = tx.cursor_write::<A::AccountTrieTable>()?;
let mut storage_trie_cursor = tx.cursor_dup_write::<A::StorageTrieTable>()?;
// Create the cursor factories. These cannot accept the `&mut` tx above because they require it
// to be AsRef.
// Create the cursor factories. These cannot accept the `&mut` tx above because they
// require it to be AsRef.
let tx = provider_rw.tx_ref();
let hashed_cursor_factory = DatabaseHashedCursorFactory::new(tx);
let trie_cursor_factory = DatabaseTrieCursorFactory::new(tx);
let trie_cursor_factory = DatabaseTrieCursorFactory::<_, A>::new(tx);
// Create the verifier
let verifier = Verifier::new(&trie_cursor_factory, hashed_cursor_factory)?;
@@ -257,17 +282,17 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
match output {
Output::AccountExtra(path, _node) => {
// Extra account node in trie, remove it
let nibbles = StoredNibbles(path);
if account_trie_cursor.seek_exact(nibbles)?.is_some() {
let key: A::AccountKey = path.into();
if account_trie_cursor.seek_exact(key)?.is_some() {
account_trie_cursor.delete_current()?;
}
}
Output::StorageExtra(account, path, _node) => {
// Extra storage node in trie, remove it
let nibbles = StoredNibblesSubKey(path);
let subkey: A::StorageSubKey = path.into();
if storage_trie_cursor
.seek_by_key_subkey(account, nibbles.clone())?
.filter(|e| e.nibbles == nibbles)
.seek_by_key_subkey(account, subkey.clone())?
.filter(|e| *e.nibbles() == subkey)
.is_some()
{
storage_trie_cursor.delete_current()?;
@@ -276,23 +301,23 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
Output::AccountWrong { path, expected: node, .. } |
Output::AccountMissing(path, node) => {
// Wrong/missing account node value, upsert it
let nibbles = StoredNibbles(path);
account_trie_cursor.upsert(nibbles, &node)?;
let key: A::AccountKey = path.into();
account_trie_cursor.upsert(key, &node)?;
}
Output::StorageWrong { account, path, expected: node, .. } |
Output::StorageMissing(account, path, node) => {
// Wrong/missing storage node value, upsert it
// (We can't just use `upsert` method with a dup cursor, it's not properly
// supported)
let nibbles = StoredNibblesSubKey(path);
let subkey: A::StorageSubKey = path.into();
let entry = A::StorageValue::new(subkey.clone(), node);
if storage_trie_cursor
.seek_by_key_subkey(account, nibbles.clone())?
.filter(|v| v.nibbles == nibbles)
.seek_by_key_subkey(account, subkey.clone())?
.filter(|v| *v.nibbles() == subkey)
.is_some()
{
storage_trie_cursor.delete_current()?;
}
let entry = StorageTrieEntry { nibbles, node };
storage_trie_cursor.upsert(account, &entry)?;
}
Output::Progress(path) => {
@@ -304,14 +329,7 @@ fn verify_and_repair<N: ProviderNodeTypes>(tool: &DbTool<N>) -> eyre::Result<()>
}
}
if inconsistent_nodes == 0 {
info!("No inconsistencies found");
} else {
provider_rw.commit()?;
info!("Repaired {} inconsistencies and committed changes", inconsistent_nodes);
}
Ok(())
Ok(inconsistent_nodes as usize)
}
/// Output progress information based on the last seen account path.

View File

@@ -0,0 +1,297 @@
//! `reth db stage-checkpoints` command for viewing and setting stage checkpoint values.
use clap::{Args, Parser, Subcommand, ValueEnum};
use reth_db_common::DbTool;
use reth_provider::{
providers::ProviderNodeTypes, DBProvider, DatabaseProviderFactory, StageCheckpointReader,
StageCheckpointWriter,
};
use reth_stages::StageId;
use crate::common::AccessRights;
/// `reth db stage-checkpoints` subcommand
#[derive(Debug, Parser)]
pub struct Command {
#[command(subcommand)]
command: Subcommands,
}
impl Command {
/// Returns database access rights required for the command.
pub fn access_rights(&self) -> AccessRights {
match &self.command {
Subcommands::Get { .. } => AccessRights::RO,
Subcommands::Set(_) => AccessRights::RW,
}
}
/// Execute the command
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
match self.command {
Subcommands::Get { stage } => Self::get(tool, stage),
Subcommands::Set(args) => Self::set(tool, args),
}
}
fn get<N: ProviderNodeTypes>(tool: &DbTool<N>, stage: Option<StageArg>) -> eyre::Result<()> {
let provider = tool.provider_factory.provider()?;
match stage {
Some(stage) => {
let stage_id = stage.into();
let checkpoint = provider.get_stage_checkpoint(stage_id)?;
println!("{stage_id}: {checkpoint:?}");
}
None => {
let mut checkpoints = provider.get_all_checkpoints()?;
checkpoints.sort_by(|a, b| a.0.cmp(&b.0));
for (stage, checkpoint) in checkpoints {
println!("{stage}: {checkpoint:?}");
}
}
}
Ok(())
}
fn set<N: ProviderNodeTypes>(tool: &DbTool<N>, args: SetArgs) -> eyre::Result<()> {
let stage_id: StageId = args.stage.into();
let provider_rw = tool.provider_factory.database_provider_rw()?;
let previous = provider_rw.get_stage_checkpoint(stage_id)?;
let mut checkpoint = previous.unwrap_or_default();
checkpoint.block_number = args.block_number;
if args.clear_stage_unit {
checkpoint.stage_checkpoint = None;
}
provider_rw.save_stage_checkpoint(stage_id, checkpoint)?;
provider_rw.commit()?;
println!("Updated checkpoint for {stage_id}: {checkpoint:?}");
Ok(())
}
}
#[derive(Debug, Subcommand)]
enum Subcommands {
/// Get stage checkpoint(s) from database.
Get {
/// Specific stage to query. If omitted, shows all stages.
#[arg(long, value_enum)]
stage: Option<StageArg>,
},
/// Set a stage checkpoint.
Set(SetArgs),
}
/// Arguments for the `set` subcommand.
#[derive(Debug, Args)]
pub struct SetArgs {
/// Stage to update.
#[arg(long, value_enum)]
stage: StageArg,
/// Block number to set as stage checkpoint.
#[arg(long)]
block_number: u64,
/// Clear stage-specific unit checkpoint payload.
#[arg(long)]
clear_stage_unit: bool,
}
/// CLI-friendly stage names.
#[derive(Debug, Clone, Copy, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum StageArg {
Era,
Headers,
Bodies,
SenderRecovery,
Execution,
PruneSenderRecovery,
MerkleUnwind,
AccountHashing,
StorageHashing,
MerkleExecute,
TransactionLookup,
IndexStorageHistory,
IndexAccountHistory,
Prune,
Finish,
}
impl From<StageArg> for StageId {
fn from(arg: StageArg) -> Self {
match arg {
StageArg::Era => Self::Era,
StageArg::Headers => Self::Headers,
StageArg::Bodies => Self::Bodies,
StageArg::SenderRecovery => Self::SenderRecovery,
StageArg::Execution => Self::Execution,
StageArg::PruneSenderRecovery => Self::PruneSenderRecovery,
StageArg::MerkleUnwind => Self::MerkleUnwind,
StageArg::AccountHashing => Self::AccountHashing,
StageArg::StorageHashing => Self::StorageHashing,
StageArg::MerkleExecute => Self::MerkleExecute,
StageArg::TransactionLookup => Self::TransactionLookup,
StageArg::IndexStorageHistory => Self::IndexStorageHistory,
StageArg::IndexAccountHistory => Self::IndexAccountHistory,
StageArg::Prune => Self::Prune,
StageArg::Finish => Self::Finish,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use reth_provider::{
test_utils::create_test_provider_factory, DBProvider, DatabaseProviderFactory,
StageCheckpointReader, StageCheckpointWriter,
};
use reth_stages::StageCheckpoint;
#[test]
fn parse_set_args() {
let command = Command::parse_from([
"stage-checkpoints",
"set",
"--stage",
"headers",
"--block-number",
"123",
]);
assert!(matches!(
command.command,
Subcommands::Set(SetArgs {
stage: StageArg::Headers,
block_number: 123,
clear_stage_unit: false,
})
));
}
#[test]
fn set_overwrites_block_number() {
let provider_factory = create_test_provider_factory();
let tool = DbTool::new(provider_factory.clone()).expect("db tool");
{
let provider_rw = provider_factory.database_provider_rw().expect("rw provider");
provider_rw
.save_stage_checkpoint(StageId::Headers, StageCheckpoint::new(10))
.expect("save checkpoint");
provider_rw.commit().expect("commit initial checkpoint");
}
let command = Command {
command: Subcommands::Set(SetArgs {
stage: StageArg::Headers,
block_number: 42,
clear_stage_unit: false,
}),
};
command.execute(&tool).expect("execute command");
let provider = provider_factory.provider().expect("provider");
let checkpoint = provider
.get_stage_checkpoint(StageId::Headers)
.expect("get stage checkpoint")
.expect("missing stage checkpoint");
assert_eq!(checkpoint.block_number, 42);
}
#[test]
fn set_preserves_stage_unit_checkpoint_unless_cleared() {
let provider_factory = create_test_provider_factory();
let tool = DbTool::new(provider_factory.clone()).expect("db tool");
{
let provider_rw = provider_factory.database_provider_rw().expect("rw provider");
let checkpoint = StageCheckpoint::new(10).with_block_range(&StageId::Execution, 5, 10);
provider_rw
.save_stage_checkpoint(StageId::Execution, checkpoint)
.expect("save checkpoint");
provider_rw.commit().expect("commit initial checkpoint");
}
Command {
command: Subcommands::Set(SetArgs {
stage: StageArg::Execution,
block_number: 11,
clear_stage_unit: false,
}),
}
.execute(&tool)
.expect("execute command");
let provider = provider_factory.provider().expect("provider");
let checkpoint = provider
.get_stage_checkpoint(StageId::Execution)
.expect("get stage checkpoint")
.expect("missing stage checkpoint");
assert!(checkpoint.stage_checkpoint.is_some());
Command {
command: Subcommands::Set(SetArgs {
stage: StageArg::Execution,
block_number: 12,
clear_stage_unit: true,
}),
}
.execute(&tool)
.expect("execute command");
let checkpoint = provider_factory
.provider()
.expect("provider")
.get_stage_checkpoint(StageId::Execution)
.expect("get stage checkpoint")
.expect("missing stage checkpoint");
assert!(checkpoint.stage_checkpoint.is_none());
}
#[test]
fn set_preserves_checkpoint_progress() {
let provider_factory = create_test_provider_factory();
let tool = DbTool::new(provider_factory.clone()).expect("db tool");
{
let provider_rw = provider_factory.database_provider_rw().expect("rw provider");
provider_rw
.save_stage_checkpoint(StageId::MerkleExecute, StageCheckpoint::new(10))
.expect("save checkpoint");
provider_rw
.save_stage_checkpoint_progress(StageId::MerkleExecute, vec![1, 2, 3])
.expect("save progress");
provider_rw.commit().expect("commit initial checkpoint");
}
Command {
command: Subcommands::Set(SetArgs {
stage: StageArg::MerkleExecute,
block_number: 20,
clear_stage_unit: false,
}),
}
.execute(&tool)
.expect("execute command");
let provider = provider_factory.provider().expect("provider");
let progress = provider
.get_stage_checkpoint_progress(StageId::MerkleExecute)
.expect("get stage checkpoint progress");
assert_eq!(progress, Some(vec![1, 2, 3]));
}
}

View File

@@ -297,21 +297,18 @@ where
}
match event {
Event::Key(key) => {
if key.kind == event::KeyEventKind::Press {
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') => return Ok(true),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
KeyCode::Right => app.next_page(),
KeyCode::Left => app.previous_page(),
KeyCode::Char('G') => {
app.mode = ViewMode::GoToPage;
}
_ => {}
}
Event::Key(key) if key.kind == event::KeyEventKind::Press => match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') => return Ok(true),
KeyCode::Down => app.next(),
KeyCode::Up => app.previous(),
KeyCode::Right => app.next_page(),
KeyCode::Left => app.previous_page(),
KeyCode::Char('G') => {
app.mode = ViewMode::GoToPage;
}
}
_ => {}
},
Event::Key(_) => {}
Event::Mouse(e) => match e.kind {
MouseEventKind::ScrollDown => app.next(),
MouseEventKind::ScrollUp => app.previous(),

View File

@@ -44,11 +44,11 @@ pub struct ExportArgs {
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ExportEraCommand<C> {
/// Execute `export-era` command
pub async fn execute<N>(self) -> eyre::Result<()>
pub async fn execute<N>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
where
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
{
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO)?;
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
// Either specified path or default to `<data-dir>/<chain>/era1-export/`
let data_dir = match &self.export.path {

View File

@@ -47,6 +47,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
pub async fn execute<N, Comp>(
self,
components: impl FnOnce(Arc<N::ChainSpec>) -> Comp,
runtime: reth_tasks::Runtime,
) -> eyre::Result<()>
where
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
@@ -54,7 +55,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
{
info!(target: "reth::cli", "reth {} starting", version_metadata().short_version);
let Environment { provider_factory, config, .. } = self.env.init::<N>(AccessRights::RW)?;
let Environment { provider_factory, config, .. } =
self.env.init::<N>(AccessRights::RW, runtime.clone())?;
let components = components(provider_factory.chain_spec());
@@ -85,6 +87,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportComm
&config,
executor.clone(),
consensus.clone(),
runtime.clone(),
)
.await?;

View File

@@ -87,6 +87,7 @@ pub async fn import_blocks_from_file<N>(
config: &Config,
executor: impl ConfigureEvm<Primitives = N::Primitives> + 'static,
consensus: Arc<impl FullConsensus<N::Primitives> + 'static>,
runtime: reth_tasks::Runtime,
) -> eyre::Result<ImportResult>
where
N: ProviderNodeTypes,
@@ -139,7 +140,7 @@ where
total_decoded_blocks += file_client.headers_len();
total_decoded_txns += file_client.total_transactions();
let (mut pipeline, events, _runtime) = build_import_pipeline_impl(
let (mut pipeline, events) = build_import_pipeline_impl(
config,
provider_factory.clone(),
&consensus,
@@ -147,6 +148,7 @@ where
static_file_producer.clone(),
import_config.no_state,
executor.clone(),
runtime.clone(),
)?;
// override the tip
@@ -257,6 +259,7 @@ where
///
/// If configured to execute, all stages will run. Otherwise, only stages that don't require state
/// will run.
#[expect(clippy::too_many_arguments)]
pub fn build_import_pipeline_impl<N, C, E>(
config: &Config,
provider_factory: ProviderFactory<N>,
@@ -265,11 +268,8 @@ pub fn build_import_pipeline_impl<N, C, E>(
static_file_producer: StaticFileProducer<ProviderFactory<N>>,
disable_exec: bool,
evm_config: E,
) -> eyre::Result<(
Pipeline<N>,
impl futures::Stream<Item = NodeEvent<N::Primitives>> + use<N, C, E>,
reth_tasks::Runtime,
)>
runtime: reth_tasks::Runtime,
) -> eyre::Result<(Pipeline<N>, impl futures::Stream<Item = NodeEvent<N::Primitives>> + use<N, C, E>)>
where
N: ProviderNodeTypes,
C: FullConsensus<N::Primitives> + 'static,
@@ -285,9 +285,6 @@ where
.sealed_header(last_block_number)?
.ok_or_else(|| ProviderError::HeaderNotFound(last_block_number.into()))?;
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())
.expect("failed to create runtime");
let mut header_downloader = ReverseHeadersDownloaderBuilder::new(config.stages.headers)
.build(file_client.clone(), consensus.clone())
.into_task_with(&runtime);
@@ -333,5 +330,5 @@ where
let events = pipeline.events().map(Into::into);
Ok((pipeline, events, runtime))
Ok((pipeline, events))
}

View File

@@ -64,13 +64,14 @@ impl TryFromChain for ChainKind {
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportEraCommand<C> {
/// Execute `import-era` command
pub async fn execute<N>(self) -> eyre::Result<()>
pub async fn execute<N>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
where
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
{
info!(target: "reth::cli", "reth {} starting", version_metadata().short_version);
let Environment { provider_factory, config, .. } = self.env.init::<N>(AccessRights::RW)?;
let Environment { provider_factory, config, .. } =
self.env.init::<N>(AccessRights::RW, runtime)?;
let mut hash_collector = Collector::new(config.stages.etl.file_size, config.stages.etl.dir);

View File

@@ -18,10 +18,13 @@ pub struct InitCommand<C: ChainSpecParser> {
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitCommand<C> {
/// Execute the `init` command
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(self) -> eyre::Result<()> {
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(
self,
runtime: reth_tasks::Runtime,
) -> eyre::Result<()> {
info!(target: "reth::cli", "reth init starting");
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW, runtime)?;
let genesis_block_number = provider_factory.chain_spec().genesis_header().number();
let hash = provider_factory

View File

@@ -65,7 +65,7 @@ pub struct InitStateCommand<C: ChainSpecParser> {
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitStateCommand<C> {
/// Execute the `init` command
pub async fn execute<N>(self) -> eyre::Result<()>
pub async fn execute<N>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
where
N: CliNodeTypes<
ChainSpec = C::ChainSpec,
@@ -74,7 +74,8 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> InitStateC
{
info!(target: "reth::cli", "Reth init-state starting");
let Environment { config, provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
let Environment { config, provider_factory, .. } =
self.env.init::<N>(AccessRights::RW, runtime)?;
let static_file_provider = provider_factory.static_file_provider();
let provider_rw = provider_factory.database_provider_rw()?;

View File

@@ -16,6 +16,7 @@ use reth_node_core::{
args::{DatadirArgs, NetworkArgs},
utils::get_single_header,
};
use reth_tasks::Runtime;
pub mod bootnode;
pub mod enode;
@@ -194,17 +195,18 @@ impl<C: ChainSpecParser> DownloadArgs<C> {
let rlpx_socket = (self.network.addr, self.network.port).into();
let boot_nodes = self.chain.bootnodes().unwrap_or_default();
let net = NetworkConfigBuilder::<N::NetworkPrimitives>::new(p2p_secret_key)
.peer_config(config.peers_config_with_basic_nodes_from_file(None))
.external_ip_resolver(self.network.nat.clone())
.network_id(self.network.network_id)
.boot_nodes(boot_nodes.clone())
.apply(|builder| {
self.network.discovery.apply_to_builder(builder, rlpx_socket, boot_nodes)
})
.build_with_noop_provider(self.chain.clone())
.manager()
.await?;
let net =
NetworkConfigBuilder::<N::NetworkPrimitives>::new(p2p_secret_key, Runtime::test())
.peer_config(config.peers_config_with_basic_nodes_from_file(None))
.external_ip_resolver(self.network.nat.clone())
.network_id(self.network.network_id)
.boot_nodes(boot_nodes.clone())
.apply(|builder| {
self.network.discovery.apply_to_builder(builder, rlpx_socket, boot_nodes)
})
.build_with_noop_provider(self.chain.clone())
.manager()
.await?;
let handle = net.handle().clone();
tokio::task::spawn(net);

View File

@@ -36,7 +36,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> PruneComma
self,
ctx: CliContext,
) -> eyre::Result<()> {
let env = self.env.init::<N>(AccessRights::RW)?;
let env = self.env.init::<N>(AccessRights::RW, ctx.task_executor.clone())?;
let provider_factory = env.provider_factory;
let config = env.config.prune;
let data_dir = env.data_dir;

View File

@@ -20,7 +20,10 @@ use reth_provider::{
use reth_revm::database::StateProviderDatabase;
use reth_stages::stages::calculate_gas_used_from_headers;
use std::{
sync::Arc,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use tokio::{sync::mpsc, task::JoinSet};
@@ -46,6 +49,10 @@ pub struct Command<C: ChainSpecParser> {
#[arg(long)]
num_tasks: Option<u64>,
/// Number of blocks each worker processes before grabbing the next chunk.
#[arg(long, default_value = "5000")]
blocks_per_chunk: u64,
/// Continues with execution when an invalid block is encountered and collects these blocks.
#[arg(long)]
skip_invalid_blocks: bool,
@@ -60,11 +67,15 @@ impl<C: ChainSpecParser> Command<C> {
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
/// Execute `re-execute` command
pub async fn execute<N>(self, components: impl CliComponentsBuilder<N>) -> eyre::Result<()>
pub async fn execute<N>(
self,
components: impl CliComponentsBuilder<N>,
runtime: reth_tasks::Runtime,
) -> eyre::Result<()>
where
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
{
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO)?;
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO, runtime)?;
let components = components(provider_factory.chain_spec());
@@ -88,12 +99,10 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
std::thread::available_parallelism().map(|n| n.get() as u64).unwrap_or(10)
});
let total_blocks = max_block - min_block;
let total_gas = calculate_gas_used_from_headers(
&provider_factory.static_file_provider(),
min_block..=max_block,
)?;
let blocks_per_task = total_blocks / num_tasks;
let db_at = {
let provider_factory = provider_factory.clone();
@@ -105,18 +114,17 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
};
let skip_invalid_blocks = self.skip_invalid_blocks;
let blocks_per_chunk = self.blocks_per_chunk;
let (stats_tx, mut stats_rx) = mpsc::unbounded_channel();
let (info_tx, mut info_rx) = mpsc::unbounded_channel();
let cancellation = CancellationToken::new();
let _guard = cancellation.drop_guard();
let mut tasks = JoinSet::new();
for i in 0..num_tasks {
let start_block = min_block + i * blocks_per_task;
let end_block =
if i == num_tasks - 1 { max_block } else { start_block + blocks_per_task };
// Shared counter for work stealing: workers atomically grab the next chunk of blocks.
let next_block = Arc::new(AtomicU64::new(min_block));
// Spawn thread executing blocks
let mut tasks = JoinSet::new();
for _ in 0..num_tasks {
let provider_factory = provider_factory.clone();
let evm_config = components.evm_config().clone();
let consensus = components.consensus().clone();
@@ -124,95 +132,122 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
let stats_tx = stats_tx.clone();
let info_tx = info_tx.clone();
let cancellation = cancellation.clone();
let next_block = Arc::clone(&next_block);
tasks.spawn_blocking(move || {
let mut executor = evm_config.batch_executor(db_at(start_block - 1));
let mut executor_created = Instant::now();
let executor_lifetime = Duration::from_secs(120);
'blocks: for block in start_block..end_block {
loop {
if cancellation.is_cancelled() {
// exit if the program is being terminated
break
break;
}
let block = provider_factory
.recovered_block(block.into(), TransactionVariant::NoHash)?
.unwrap();
// Atomically grab the next chunk of blocks.
let chunk_start =
next_block.fetch_add(blocks_per_chunk, Ordering::Relaxed);
if chunk_start >= max_block {
break;
}
let chunk_end = (chunk_start + blocks_per_chunk).min(max_block);
let result = match executor.execute_one(&block) {
Ok(result) => result,
Err(err) => {
if skip_invalid_blocks {
executor = evm_config.batch_executor(db_at(block.number()));
let _ = info_tx.send((block, eyre::Report::new(err)));
continue
}
return Err(err.into())
let mut executor = evm_config.batch_executor(db_at(chunk_start - 1));
let mut executor_created = Instant::now();
'blocks: for block in chunk_start..chunk_end {
if cancellation.is_cancelled() {
break;
}
};
if let Err(err) = consensus
.validate_block_post_execution(&block, &result, None)
.wrap_err_with(|| {
format!("Failed to validate block {} {}", block.number(), block.hash())
})
{
let correct_receipts =
provider_factory.receipts_by_block(block.number().into())?.unwrap();
let block = provider_factory
.recovered_block(block.into(), TransactionVariant::NoHash)?
.unwrap();
for (i, (receipt, correct_receipt)) in
result.receipts.iter().zip(correct_receipts.iter()).enumerate()
{
if receipt != correct_receipt {
let tx_hash = block.body().transactions()[i].tx_hash();
error!(
?receipt,
?correct_receipt,
index = i,
?tx_hash,
"Invalid receipt"
);
let expected_gas_used = correct_receipt.cumulative_gas_used() -
if i == 0 {
0
} else {
correct_receipts[i - 1].cumulative_gas_used()
};
let got_gas_used = receipt.cumulative_gas_used() -
if i == 0 {
0
} else {
result.receipts[i - 1].cumulative_gas_used()
};
if got_gas_used != expected_gas_used {
let mismatch = GotExpected {
expected: expected_gas_used,
got: got_gas_used,
};
error!(number=?block.number(), ?mismatch, "Gas usage mismatch");
if skip_invalid_blocks {
executor = evm_config.batch_executor(db_at(block.number()));
let _ = info_tx.send((block, err));
continue 'blocks;
}
return Err(err);
let result = match executor.execute_one(&block) {
Ok(result) => result,
Err(err) => {
if skip_invalid_blocks {
executor =
evm_config.batch_executor(db_at(block.number()));
let _ =
info_tx.send((block, eyre::Report::new(err)));
continue
}
} else {
continue;
return Err(err.into())
}
};
if let Err(err) = consensus
.validate_block_post_execution(&block, &result, None)
.wrap_err_with(|| {
format!(
"Failed to validate block {} {}",
block.number(),
block.hash()
)
})
{
let correct_receipts = provider_factory
.receipts_by_block(block.number().into())?
.unwrap();
for (i, (receipt, correct_receipt)) in
result.receipts.iter().zip(correct_receipts.iter()).enumerate()
{
if receipt != correct_receipt {
let tx_hash =
block.body().transactions()[i].tx_hash();
error!(
?receipt,
?correct_receipt,
index = i,
?tx_hash,
"Invalid receipt"
);
let expected_gas_used =
correct_receipt.cumulative_gas_used() -
if i == 0 {
0
} else {
correct_receipts[i - 1]
.cumulative_gas_used()
};
let got_gas_used = receipt.cumulative_gas_used() -
if i == 0 {
0
} else {
result.receipts[i - 1].cumulative_gas_used()
};
if got_gas_used != expected_gas_used {
let mismatch = GotExpected {
expected: expected_gas_used,
got: got_gas_used,
};
error!(number=?block.number(), ?mismatch, "Gas usage mismatch");
if skip_invalid_blocks {
executor = evm_config
.batch_executor(db_at(block.number()));
let _ = info_tx.send((block, err));
continue 'blocks;
}
return Err(err);
}
} else {
continue;
}
}
return Err(err);
}
let _ = stats_tx.send(block.gas_used());
return Err(err);
}
let _ = stats_tx.send(block.gas_used());
// Reset DB once in a while to avoid OOM or read tx timeouts
if executor.size_hint() > 1_000_000 ||
executor_created.elapsed() > executor_lifetime
{
executor = evm_config.batch_executor(db_at(block.number()));
executor_created = Instant::now();
// Reset DB once in a while to avoid OOM or read tx timeouts
if executor.size_hint() > 1_000_000 ||
executor_created.elapsed() > executor_lifetime
{
executor =
evm_config.batch_executor(db_at(block.number()));
executor_created = Instant::now();
}
}
}

View File

@@ -37,11 +37,11 @@ pub struct Command<C: ChainSpecParser> {
impl<C: ChainSpecParser> Command<C> {
/// Execute `db` command
pub async fn execute<N: CliNodeTypes>(self) -> eyre::Result<()>
pub async fn execute<N: CliNodeTypes>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
where
C: ChainSpecParser<ChainSpec = N::ChainSpec>,
{
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW)?;
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RW, runtime)?;
let tool = DbTool::new(provider_factory)?;

View File

@@ -16,6 +16,7 @@ use reth_stages::{stages::ExecutionStage, Stage, StageCheckpoint, UnwindInput};
use std::sync::Arc;
use tracing::info;
#[expect(clippy::too_many_arguments)]
pub(crate) async fn dump_execution_stage<N, E, C>(
db_tool: &DbTool<N>,
from: u64,
@@ -24,6 +25,7 @@ pub(crate) async fn dump_execution_stage<N, E, C>(
should_run: bool,
evm_config: E,
consensus: C,
runtime: reth_tasks::Runtime,
) -> eyre::Result<()>
where
N: ProviderNodeTypes<DB = DatabaseEnv>,
@@ -37,7 +39,6 @@ where
unwind_and_copy(db_tool, from, tip_block_number, &output_db, evm_config.clone())?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,

View File

@@ -18,6 +18,7 @@ pub(crate) async fn dump_hashing_account_stage<N: ProviderNodeTypes<DB = Databas
to: BlockNumber,
output_datadir: ChainPath<DataDirPath>,
should_run: bool,
runtime: reth_tasks::Runtime,
) -> Result<()> {
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
@@ -33,7 +34,6 @@ pub(crate) async fn dump_hashing_account_stage<N: ProviderNodeTypes<DB = Databas
unwind_and_copy(db_tool, from, tip_block_number, &output_db)?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,

View File

@@ -17,13 +17,13 @@ pub(crate) async fn dump_hashing_storage_stage<N: ProviderNodeTypes<DB = Databas
to: u64,
output_datadir: ChainPath<DataDirPath>,
should_run: bool,
runtime: reth_tasks::Runtime,
) -> Result<()> {
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
unwind_and_copy(db_tool, from, tip_block_number, &output_db)?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,

View File

@@ -24,6 +24,7 @@ use reth_stages::{
};
use tracing::info;
#[expect(clippy::too_many_arguments)]
pub(crate) async fn dump_merkle_stage<N>(
db_tool: &DbTool<N>,
from: BlockNumber,
@@ -32,6 +33,7 @@ pub(crate) async fn dump_merkle_stage<N>(
should_run: bool,
evm_config: impl ConfigureEvm<Primitives = N::Primitives>,
consensus: impl FullConsensus<N::Primitives> + 'static,
runtime: reth_tasks::Runtime,
) -> Result<()>
where
N: ProviderNodeTypes<DB = DatabaseEnv>,
@@ -57,7 +59,6 @@ where
unwind_and_copy(db_tool, (from, to), tip_block_number, &output_db, evm_config, consensus)?;
if should_run {
let runtime = reth_tasks::Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
dry_run(
ProviderFactory::<N>::new(
output_db,

View File

@@ -72,30 +72,36 @@ pub struct StageCommand {
}
macro_rules! handle_stage {
($stage_fn:ident, $tool:expr, $command:expr) => {{
($stage_fn:ident, $tool:expr, $command:expr, $runtime:expr) => {{
let StageCommand { output_datadir, from, to, dry_run, .. } = $command;
let output_datadir =
output_datadir.with_chain($tool.chain().chain(), DatadirArgs::default());
$stage_fn($tool, *from, *to, output_datadir, *dry_run).await?
$stage_fn($tool, *from, *to, output_datadir, *dry_run, $runtime).await?
}};
($stage_fn:ident, $tool:expr, $command:expr, $executor:expr, $consensus:expr) => {{
($stage_fn:ident, $tool:expr, $command:expr, $executor:expr, $consensus:expr, $runtime:expr) => {{
let StageCommand { output_datadir, from, to, dry_run, .. } = $command;
let output_datadir =
output_datadir.with_chain($tool.chain().chain(), DatadirArgs::default());
$stage_fn($tool, *from, *to, output_datadir, *dry_run, $executor, $consensus).await?
$stage_fn($tool, *from, *to, output_datadir, *dry_run, $executor, $consensus, $runtime)
.await?
}};
}
impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
/// Execute `dump-stage` command
pub async fn execute<N, Comp, F>(self, components: F) -> eyre::Result<()>
pub async fn execute<N, Comp, F>(
self,
components: F,
runtime: reth_tasks::Runtime,
) -> eyre::Result<()>
where
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
Comp: CliNodeComponents<N>,
F: FnOnce(Arc<C::ChainSpec>) -> Comp,
{
let Environment { provider_factory, .. } = self.env.init::<N>(AccessRights::RO)?;
let Environment { provider_factory, .. } =
self.env.init::<N>(AccessRights::RO, runtime.clone())?;
let tool = DbTool::new(provider_factory)?;
let components = components(tool.chain());
let evm_config = components.evm_config().clone();
@@ -103,12 +109,23 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
match &self.command {
Stages::Execution(cmd) => {
handle_stage!(dump_execution_stage, &tool, cmd, evm_config, consensus)
handle_stage!(
dump_execution_stage,
&tool,
cmd,
evm_config,
consensus,
runtime.clone()
)
}
Stages::StorageHashing(cmd) => {
handle_stage!(dump_hashing_storage_stage, &tool, cmd, runtime.clone())
}
Stages::AccountHashing(cmd) => {
handle_stage!(dump_hashing_account_stage, &tool, cmd, runtime.clone())
}
Stages::StorageHashing(cmd) => handle_stage!(dump_hashing_storage_stage, &tool, cmd),
Stages::AccountHashing(cmd) => handle_stage!(dump_hashing_account_stage, &tool, cmd),
Stages::Merkle(cmd) => {
handle_stage!(dump_merkle_stage, &tool, cmd, evm_config, consensus)
handle_stage!(dump_merkle_stage, &tool, cmd, evm_config, consensus, runtime.clone())
}
}

View File

@@ -49,11 +49,12 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
N: CliNodeTypes<ChainSpec = C::ChainSpec>,
Comp: CliNodeComponents<N>,
{
let executor = ctx.task_executor.clone();
match self.command {
Subcommands::Run(command) => command.execute::<N, _, _>(ctx, components).await,
Subcommands::Drop(command) => command.execute::<N>().await,
Subcommands::Dump(command) => command.execute::<N, _, _>(components).await,
Subcommands::Unwind(command) => command.execute::<N, _, _>(components).await,
Subcommands::Drop(command) => command.execute::<N>(executor).await,
Subcommands::Dump(command) => command.execute::<N, _, _>(components, executor).await,
Subcommands::Unwind(command) => command.execute::<N, _, _>(components, executor).await,
}
}
}

View File

@@ -119,8 +119,9 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
// Does not do anything on windows.
let _ = fdlimit::raise_fd_limit();
let runtime = ctx.task_executor.clone();
let Environment { provider_factory, config, data_dir } =
self.env.init::<N>(AccessRights::RW)?;
self.env.init::<N>(AccessRights::RW, ctx.task_executor.clone())?;
let mut provider_rw = provider_factory.database_provider_rw()?;
let components = components(provider_factory.chain_spec());
@@ -171,6 +172,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
provider_factory.chain_spec(),
p2p_secret_key,
default_peers_path,
runtime.clone(),
)
.build(provider_factory.clone())
.start_network()
@@ -226,6 +228,7 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>
provider_factory.chain_spec(),
p2p_secret_key,
default_peers_path,
runtime.clone(),
)
.build(provider_factory.clone())
.start_network()

View File

@@ -46,12 +46,14 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>, F, Comp>(
self,
components: F,
runtime: reth_tasks::Runtime,
) -> eyre::Result<()>
where
Comp: CliNodeComponents<N>,
F: FnOnce(Arc<C::ChainSpec>) -> Comp,
{
let Environment { provider_factory, config, .. } = self.env.init::<N>(AccessRights::RW)?;
let Environment { provider_factory, config, data_dir: _ } =
self.env.init::<N>(AccessRights::RW, runtime)?;
let target = self.command.unwind_target(provider_factory.clone())?;

View File

@@ -47,6 +47,11 @@ impl CliRunner {
self
}
/// Returns a clone of the underlying [`Runtime`](reth_tasks::Runtime).
pub fn runtime(&self) -> reth_tasks::Runtime {
self.runtime.clone()
}
/// Executes an async block on the runtime and blocks until completion.
pub fn block_on<F, T>(&self, fut: F) -> T
where

View File

@@ -19,6 +19,7 @@ reth-consensus.workspace = true
reth-primitives-traits.workspace = true
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-primitives.workspace = true
[dev-dependencies]
alloy-primitives = { workspace = true, features = ["rand"] }

View File

@@ -2,6 +2,7 @@
use alloy_consensus::{BlockHeader as _, EMPTY_OMMER_ROOT_HASH};
use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip7840::BlobParams};
use alloy_primitives::B256;
use reth_chainspec::{EthChainSpec, EthereumHardfork, EthereumHardforks};
use reth_consensus::ConsensusError;
use reth_primitives_traits::{
@@ -141,6 +142,27 @@ pub fn validate_block_pre_execution<B, ChainSpec>(
block: &SealedBlock<B>,
chain_spec: &ChainSpec,
) -> Result<(), ConsensusError>
where
B: Block,
ChainSpec: EthChainSpec + EthereumHardforks,
{
validate_block_pre_execution_with_tx_root(block, chain_spec, None)
}
/// Validate a block without regard for state using an optional pre-computed transaction root.
///
/// - Compares the ommer hash in the block header to the block body
/// - Compares the transactions root in the block header to the block body
/// - Pre-execution transaction validation
///
/// If `transaction_root` is provided, it is used instead of recomputing the transaction trie
/// root from the block body. The caller must ensure this value was derived from
/// `block.body().calculate_tx_root()`.
pub fn validate_block_pre_execution_with_tx_root<B, ChainSpec>(
block: &SealedBlock<B>,
chain_spec: &ChainSpec,
transaction_root: Option<B256>,
) -> Result<(), ConsensusError>
where
B: Block,
ChainSpec: EthChainSpec + EthereumHardforks,
@@ -148,8 +170,14 @@ where
post_merge_hardfork_fields(block, chain_spec)?;
// Check transaction root
if let Err(error) = block.ensure_transaction_root_valid() {
return Err(ConsensusError::BodyTransactionRootDiff(error.into()))
let expected_transaction_root = block.header().transactions_root();
let calculated_transaction_root =
transaction_root.unwrap_or_else(|| block.body().calculate_tx_root());
if calculated_transaction_root != expected_transaction_root {
return Err(ConsensusError::BodyTransactionRootDiff(
GotExpected { got: calculated_transaction_root, expected: expected_transaction_root }
.into(),
))
}
Ok(())
@@ -426,7 +454,7 @@ pub fn validate_against_parent_4844<H: BlockHeader>(
mod tests {
use super::*;
use alloy_consensus::{BlockBody, Header, TxEip4844};
use alloy_eips::eip4895::Withdrawals;
use alloy_eips::{eip4844::DATA_GAS_PER_BLOB, eip4895::Withdrawals};
use alloy_primitives::{Address, Bytes, Signature, U256};
use rand::Rng;
use reth_chainspec::ChainSpecBuilder;
@@ -507,4 +535,66 @@ mod tests {
// Test with custom larger limit - should pass
assert!(validate_header_extra_data(&header_33, 64).is_ok());
}
#[test]
fn precomputed_tx_root_correct_passes() {
let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
let transaction = mock_blob_tx(1, 1);
let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
let header = Header {
base_fee_per_gas: Some(1337),
withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
transactions_root: tx_root,
blob_gas_used: Some(DATA_GAS_PER_BLOB),
excess_blob_gas: Some(0),
..Default::default()
};
let body = BlockBody {
transactions: vec![transaction],
ommers: vec![],
withdrawals: Some(Withdrawals::default()),
};
let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
// Some(correct_root) should pass just like None
assert!(
validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(tx_root)).is_ok()
);
assert!(validate_block_pre_execution_with_tx_root(&block, &chain_spec, None).is_ok());
}
#[test]
fn precomputed_tx_root_wrong_fails() {
let chain_spec = ChainSpecBuilder::mainnet().cancun_activated().build();
let transaction = mock_blob_tx(1, 1);
let tx_root = proofs::calculate_transaction_root(std::slice::from_ref(&transaction));
let header = Header {
base_fee_per_gas: Some(1337),
withdrawals_root: Some(proofs::calculate_withdrawals_root(&[])),
transactions_root: tx_root,
blob_gas_used: Some(DATA_GAS_PER_BLOB),
excess_blob_gas: Some(0),
..Default::default()
};
let body = BlockBody {
transactions: vec![transaction],
ommers: vec![],
withdrawals: Some(Withdrawals::default()),
};
let block = SealedBlock::seal_slow(alloy_consensus::Block { header, body });
let wrong_root = B256::repeat_byte(0xff);
assert!(matches!(
validate_block_pre_execution_with_tx_root(&block, &chain_spec, Some(wrong_root))
.unwrap_err(),
ConsensusError::BodyTransactionRootDiff(diff)
if diff.0.got == wrong_root && diff.0.expected == tx_root
));
}
}

View File

@@ -21,6 +21,12 @@ use core::error::Error;
/// When provided to [`FullConsensus::validate_block_post_execution`], this allows skipping
/// the receipt root computation and using the pre-computed values instead.
pub type ReceiptRootBloom = (B256, Bloom);
/// Pre-computed transaction root.
///
/// When provided to [`Consensus::validate_block_pre_execution_with_tx_root`], this allows
/// skipping transaction trie reconstruction from the block body.
pub type TransactionRoot = B256;
use reth_execution_types::BlockExecutionResult;
use reth_primitives_traits::{
constants::{GAS_LIMIT_BOUND_DIVISOR, MAXIMUM_GAS_LIMIT_BLOCK, MINIMUM_GAS_LIMIT},
@@ -78,6 +84,22 @@ pub trait Consensus<B: Block>: HeaderValidator<B::Header> {
///
/// Note: validating blocks does not include other validations of the Consensus
fn validate_block_pre_execution(&self, block: &SealedBlock<B>) -> Result<(), ConsensusError>;
/// Validate a block disregarding world state using an optional pre-computed transaction root.
///
/// If `transaction_root` is provided, the implementation should use the pre-computed
/// transaction root instead of recomputing it from the block body. The value must have been
/// derived from `block.body().calculate_tx_root()`.
///
/// By default this falls back to [`Self::validate_block_pre_execution`].
fn validate_block_pre_execution_with_tx_root(
&self,
block: &SealedBlock<B>,
transaction_root: Option<TransactionRoot>,
) -> Result<(), ConsensusError> {
let _ = transaction_root;
self.validate_block_pre_execution(block)
}
}
/// `HeaderValidator` is a protocol that validates headers and their relationships.

View File

@@ -38,7 +38,6 @@ reth-ethereum-primitives.workspace = true
reth-cli-commands.workspace = true
reth-config.workspace = true
reth-consensus.workspace = true
reth-primitives.workspace = true
reth-db-common.workspace = true
reth-primitives-traits.workspace = true

View File

@@ -112,7 +112,7 @@ where
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
Wallet,
)> {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
let runtime = Runtime::test();
let network_config = NetworkArgs {
discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },

View File

@@ -15,7 +15,6 @@ use reth_provider::{
};
use reth_rpc_server_types::RpcModuleSelection;
use reth_stages_types::StageId;
use reth_tasks::Runtime;
use std::{path::Path, sync::Arc};
use tempfile::TempDir;
use tracing::{debug, info, span, Level};
@@ -66,7 +65,7 @@ pub async fn setup_engine_with_chain_import(
+ Copy
+ 'static,
) -> eyre::Result<ChainImportResult> {
let runtime = Runtime::with_existing_handle(tokio::runtime::Handle::current())?;
let runtime = reth_tasks::Runtime::test();
let network_config = NetworkArgs {
discovery: DiscoveryArgs { disable_discovery: true, ..DiscoveryArgs::default() },
@@ -149,6 +148,7 @@ pub async fn setup_engine_with_chain_import(
&config,
evm_config,
consensus,
runtime.clone(),
)
.await?;
@@ -275,8 +275,9 @@ mod tests {
use crate::test_rlp_utils::{create_fcu_json, generate_test_blocks, write_blocks_to_rlp};
use reth_chainspec::{ChainSpecBuilder, MAINNET};
use reth_db::mdbx::DatabaseArguments;
use reth_ethereum_primitives::Block;
use reth_payload_builder::EthPayloadBuilderAttributes;
use reth_primitives::SealedBlock;
use reth_primitives_traits::SealedBlock;
use reth_provider::{
test_utils::MockNodeTypesWithDB, BlockHashReader, BlockNumReader, BlockReaderIdExt,
};
@@ -343,6 +344,7 @@ mod tests {
let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
// Use NoopConsensus to skip gas limit validation for test imports
let consensus = reth_consensus::noop::NoopConsensus::arc();
let runtime = reth_tasks::Runtime::test();
let result = import_blocks_from_file(
&rlp_path,
@@ -351,6 +353,7 @@ mod tests {
&config,
evm_config,
consensus,
runtime,
)
.await
.unwrap();
@@ -446,7 +449,7 @@ mod tests {
chain_spec: &ChainSpec,
block_count: u64,
temp_dir: &Path,
) -> (Vec<SealedBlock>, PathBuf) {
) -> (Vec<SealedBlock<Block>>, PathBuf) {
let test_blocks = generate_test_blocks(chain_spec, block_count);
assert_eq!(
test_blocks.len(),
@@ -509,6 +512,7 @@ mod tests {
let evm_config = reth_node_ethereum::EthEvmConfig::new(chain_spec.clone());
// Use NoopConsensus to skip gas limit validation for test imports
let consensus = reth_consensus::noop::NoopConsensus::arc();
let runtime = reth_tasks::Runtime::test();
let result = import_blocks_from_file(
&rlp_path,
@@ -517,6 +521,7 @@ mod tests {
&config,
evm_config,
consensus,
runtime,
)
.await
.unwrap();

View File

@@ -6,14 +6,13 @@ use alloy_primitives::{Address, B256, B64, U256};
use alloy_rlp::Encodable;
use reth_chainspec::{ChainSpec, EthereumHardforks};
use reth_ethereum_primitives::{Block, BlockBody};
use reth_primitives::SealedBlock;
use reth_primitives_traits::Block as BlockTrait;
use reth_primitives_traits::{Block as BlockTrait, SealedBlock};
use std::{io::Write, path::Path};
use tracing::debug;
/// Generate test blocks for a given chain spec
pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlock> {
let mut blocks: Vec<SealedBlock> = Vec::new();
pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlock<Block>> {
let mut blocks: Vec<SealedBlock<Block>> = Vec::new();
let genesis_header = chain_spec.sealed_genesis_header();
let mut parent_hash = genesis_header.hash();
let mut parent_number = genesis_header.number();
@@ -139,7 +138,7 @@ pub fn generate_test_blocks(chain_spec: &ChainSpec, count: u64) -> Vec<SealedBlo
}
/// Write blocks to RLP file
pub fn write_blocks_to_rlp(blocks: &[SealedBlock], path: &Path) -> std::io::Result<()> {
pub fn write_blocks_to_rlp(blocks: &[SealedBlock<Block>], path: &Path) -> std::io::Result<()> {
let mut file = std::fs::File::create(path)?;
let mut total_bytes = 0;
@@ -173,7 +172,7 @@ pub fn write_blocks_to_rlp(blocks: &[SealedBlock], path: &Path) -> std::io::Resu
}
/// Create FCU JSON for the tip of the chain
pub fn create_fcu_json(tip: &SealedBlock) -> serde_json::Value {
pub fn create_fcu_json(tip: &SealedBlock<Block>) -> serde_json::Value {
serde_json::json!({
"params": [{
"headBlockHash": format!("0x{:x}", tip.hash()),

View File

@@ -9,32 +9,8 @@ pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
/// How close to the canonical head we persist blocks.
pub const DEFAULT_MEMORY_BLOCK_BUFFER_TARGET: u64 = 0;
/// Returns the default number of storage worker threads based on available parallelism.
fn default_storage_worker_count() -> usize {
#[cfg(feature = "std")]
{
std::thread::available_parallelism().map_or(8, |n| n.get() * 2)
}
#[cfg(not(feature = "std"))]
{
8
}
}
/// Returns the default number of account worker threads.
///
/// Account workers coordinate storage proof collection and account trie traversal.
/// They are set to the same count as storage workers for simplicity.
fn default_account_worker_count() -> usize {
default_storage_worker_count()
}
/// The size of proof targets chunk to spawn in one multiproof calculation.
pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 60;
/// The size of proof targets chunk optimized for small blocks (≤20M gas used).
/// Benchmarks: <https://gist.github.com/yongkangc/fda9c24846f0ba891376bcf81b002008>
pub const SMALL_BLOCK_MULTIPROOF_CHUNK_SIZE: usize = 30;
pub const DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE: usize = 5;
/// Gas threshold below which the small block chunk size is used.
pub const SMALL_BLOCK_GAS_THRESHOLD: u64 = 20_000_000;
@@ -127,8 +103,6 @@ pub struct TreeConfig {
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.
multiproof_chunking_enabled: bool,
/// Multiproof task chunk size for proof targets.
multiproof_chunk_size: usize,
/// Number of reserved CPU cores for non-reth processes
@@ -153,10 +127,6 @@ pub struct TreeConfig {
always_process_payload_attributes_on_canonical_head: bool,
/// Whether to unwind canonical header to ancestor during forkchoice updates.
allow_unwind_canonical_header: bool,
/// Number of storage proof worker threads.
storage_worker_count: usize,
/// Number of account proof worker threads.
account_worker_count: usize,
/// Whether to disable cache metrics recording (can be expensive with large cached state).
disable_cache_metrics: bool,
/// Depth for sparse trie pruning after state root computation.
@@ -187,15 +157,12 @@ impl Default for TreeConfig {
state_provider_metrics: false,
cross_block_cache_size: DEFAULT_CROSS_BLOCK_CACHE_SIZE,
has_enough_parallelism: has_enough_parallelism(),
multiproof_chunking_enabled: true,
multiproof_chunk_size: DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE,
reserved_cpu_cores: DEFAULT_RESERVED_CPU_CORES,
precompile_cache_disabled: false,
state_root_fallback: false,
always_process_payload_attributes_on_canonical_head: false,
allow_unwind_canonical_header: false,
storage_worker_count: default_storage_worker_count(),
account_worker_count: default_account_worker_count(),
disable_cache_metrics: false,
sparse_trie_prune_depth: DEFAULT_SPARSE_TRIE_PRUNE_DEPTH,
sparse_trie_max_storage_tries: DEFAULT_SPARSE_TRIE_MAX_STORAGE_TRIES,
@@ -221,15 +188,12 @@ impl TreeConfig {
state_provider_metrics: bool,
cross_block_cache_size: usize,
has_enough_parallelism: bool,
multiproof_chunking_enabled: bool,
multiproof_chunk_size: usize,
reserved_cpu_cores: usize,
precompile_cache_disabled: bool,
state_root_fallback: bool,
always_process_payload_attributes_on_canonical_head: bool,
allow_unwind_canonical_header: bool,
storage_worker_count: usize,
account_worker_count: usize,
disable_cache_metrics: bool,
sparse_trie_prune_depth: usize,
sparse_trie_max_storage_tries: usize,
@@ -248,15 +212,12 @@ impl TreeConfig {
state_provider_metrics,
cross_block_cache_size,
has_enough_parallelism,
multiproof_chunking_enabled,
multiproof_chunk_size,
reserved_cpu_cores,
precompile_cache_disabled,
state_root_fallback,
always_process_payload_attributes_on_canonical_head,
allow_unwind_canonical_header,
storage_worker_count,
account_worker_count,
disable_cache_metrics,
sparse_trie_prune_depth,
sparse_trie_max_storage_tries,
@@ -290,11 +251,6 @@ impl TreeConfig {
self.max_execute_block_batch_size
}
/// Return whether the multiproof task chunking is enabled.
pub const fn multiproof_chunking_enabled(&self) -> bool {
self.multiproof_chunking_enabled
}
/// Return the multiproof task chunk size.
pub const fn multiproof_chunk_size(&self) -> usize {
self.multiproof_chunk_size
@@ -458,15 +414,6 @@ impl TreeConfig {
self
}
/// Setter for whether multiproof task should chunk proof targets.
pub const fn with_multiproof_chunking_enabled(
mut self,
multiproof_chunking_enabled: bool,
) -> Self {
self.multiproof_chunking_enabled = multiproof_chunking_enabled;
self
}
/// Setter for multiproof task chunk size for proof targets.
pub const fn with_multiproof_chunk_size(mut self, multiproof_chunk_size: usize) -> Self {
self.multiproof_chunk_size = multiproof_chunk_size;
@@ -502,42 +449,6 @@ impl TreeConfig {
self.has_enough_parallelism && !self.legacy_state_root
}
/// Return the number of storage proof worker threads.
pub const fn storage_worker_count(&self) -> usize {
self.storage_worker_count
}
/// Setter for the number of storage proof worker threads.
///
/// No-op if it's [`None`].
pub const fn with_storage_worker_count_opt(
mut self,
storage_worker_count: Option<usize>,
) -> Self {
if let Some(count) = storage_worker_count {
self.storage_worker_count = count;
}
self
}
/// Return the number of account proof worker threads.
pub const fn account_worker_count(&self) -> usize {
self.account_worker_count
}
/// Setter for the number of account proof worker threads.
///
/// No-op if it's [`None`].
pub const fn with_account_worker_count_opt(
mut self,
account_worker_count: Option<usize>,
) -> Self {
if let Some(count) = account_worker_count {
self.account_worker_count = count;
}
self
}
/// Returns whether cache metrics recording is disabled.
pub const fn disable_cache_metrics(&self) -> bool {
self.disable_cache_metrics

View File

@@ -12,7 +12,8 @@ use rand::Rng;
use reth_chainspec::ChainSpec;
use reth_db_common::init::init_genesis;
use reth_engine_tree::tree::{
precompile_cache::PrecompileCacheMap, PayloadProcessor, StateProviderBuilder, TreeConfig,
precompile_cache::PrecompileCacheMap, ExecutionEnv, PayloadProcessor, StateProviderBuilder,
TreeConfig,
};
use reth_ethereum_primitives::TransactionSigned;
use reth_evm::OnStateHook;
@@ -230,7 +231,7 @@ fn bench_state_root(c: &mut Criterion) {
|(genesis_hash, mut payload_processor, provider, state_updates)| {
black_box({
let mut handle = payload_processor.spawn(
Default::default(),
ExecutionEnv::test_default(),
(
Vec::<
Result<

View File

@@ -351,14 +351,6 @@ impl<S: StateProvider, const PREWARM: bool> StateProvider for CachedStateProvide
self.state_provider.storage(account, storage_key)
}
}
fn storage_by_hashed_key(
&self,
address: Address,
hashed_storage_key: StorageKey,
) -> ProviderResult<Option<StorageValue>> {
self.state_provider.storage_by_hashed_key(address, hashed_storage_key)
}
}
impl<S: BytecodeReader, const PREWARM: bool> BytecodeReader for CachedStateProvider<S, PREWARM> {

View File

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

View File

@@ -38,7 +38,7 @@ use reth_provider::{
};
use reth_revm::database::StateProviderDatabase;
use reth_stages_api::ControlFlow;
use reth_tasks::spawn_os_thread;
use reth_tasks::{spawn_os_thread, utils::increase_thread_priority};
use reth_trie_db::ChangesetCache;
use revm::interpreter::debug_unreachable;
use state::TreeState;
@@ -420,7 +420,10 @@ where
use_hashed_state,
);
let incoming = task.incoming_tx.clone();
spawn_os_thread("engine", || task.run());
spawn_os_thread("engine", || {
increase_thread_priority();
task.run()
});
(incoming, outgoing)
}
@@ -1413,7 +1416,7 @@ where
// Spawn a background task to trigger computation so it's ready when the next payload
// arrives.
if let Some(overlay) = self.state.tree_state.prepare_canonical_overlay() {
rayon::spawn(move || {
tokio::task::spawn_blocking(move || {
let _ = overlay.get();
});
}
@@ -2580,6 +2583,51 @@ where
Some(TreeEvent::Download(request))
}
/// Handles a downloaded block that was successfully inserted as valid.
///
/// If the block matches the sync target head, returns [`TreeAction::MakeCanonical`].
/// If it matches a non-head sync target (safe or finalized), makes it canonical inline
/// and triggers a download for the remaining blocks towards the actual head.
/// Otherwise, tries to connect buffered blocks.
fn on_valid_downloaded_block(
&mut self,
block_num_hash: BlockNumHash,
) -> Result<Option<TreeEvent>, InsertBlockFatalError> {
// check if we just inserted a block that's part of sync targets,
// i.e. head, safe, or finalized
if let Some(sync_target) = self.state.forkchoice_state_tracker.sync_target_state() &&
sync_target.contains(block_num_hash.hash)
{
debug!(target: "engine::tree", ?sync_target, "appended downloaded sync target block");
if sync_target.head_block_hash == block_num_hash.hash {
// we just inserted the sync target head block, make it canonical
return Ok(Some(TreeEvent::TreeAction(TreeAction::MakeCanonical {
sync_target_head: block_num_hash.hash,
})))
}
// This block is part of the sync target (safe or finalized) but not the
// head. Make it canonical and try to connect any buffered children, then
// continue downloading towards the actual head if needed.
self.make_canonical(block_num_hash.hash)?;
self.try_connect_buffered_blocks(block_num_hash)?;
// Check if we've reached the sync target head after connecting buffered
// blocks (e.g. the head block may have already been buffered).
if self.state.tree_state.canonical_block_hash() != sync_target.head_block_hash {
let target = self.lowest_buffered_ancestor_or(sync_target.head_block_hash);
trace!(target: "engine::tree", %target, "sync target head not yet reached, downloading head block");
return Ok(Some(TreeEvent::Download(DownloadRequest::single_block(target))))
}
return Ok(None)
}
trace!(target: "engine::tree", "appended downloaded block");
self.try_connect_buffered_blocks(block_num_hash)?;
Ok(None)
}
/// Invoked with a block downloaded from the network
///
/// Returns an event with the appropriate action to take, such as:
@@ -2602,22 +2650,11 @@ where
// try to append the block
match self.insert_block(block) {
Ok(InsertPayloadOk::Inserted(BlockStatus::Valid)) => {
// check if we just inserted a block that's part of sync targets,
// i.e. head, safe, or finalized
if let Some(sync_target) = self.state.forkchoice_state_tracker.sync_target_state() &&
sync_target.contains(block_num_hash.hash)
{
debug!(target: "engine::tree", ?sync_target, "appended downloaded sync target block");
// we just inserted a block that we know is part of the canonical chain, so we
// can make it canonical
return Ok(Some(TreeEvent::TreeAction(TreeAction::MakeCanonical {
sync_target_head: block_num_hash.hash,
})))
}
trace!(target: "engine::tree", "appended downloaded block");
self.try_connect_buffered_blocks(block_num_hash)?;
Ok(
InsertPayloadOk::Inserted(BlockStatus::Valid) |
InsertPayloadOk::AlreadySeen(BlockStatus::Valid),
) => {
return self.on_valid_downloaded_block(block_num_hash);
}
Ok(InsertPayloadOk::Inserted(BlockStatus::Disconnected { head, missing_ancestor })) => {
// block is not connected to the canonical head, we need to download

View File

@@ -20,7 +20,6 @@ use multiproof::*;
use parking_lot::RwLock;
use prewarm::PrewarmMetrics;
use rayon::prelude::*;
use reth_engine_primitives::{SMALL_BLOCK_GAS_THRESHOLD, SMALL_BLOCK_MULTIPROOF_CHUNK_SIZE};
use reth_evm::{
block::ExecutableTxParts,
execute::{ExecutableTxFor, WithTxEnv},
@@ -33,7 +32,7 @@ use reth_provider::{
BlockExecutionOutput, BlockReader, DatabaseProviderROFactory, StateProviderFactory, StateReader,
};
use reth_revm::{db::BundleState, state::EvmState};
use reth_tasks::{ForEachOrdered, Runtime};
use reth_tasks::{utils::increase_thread_priority, ForEachOrdered, Runtime};
use reth_trie::{hashed_cursor::HashedCursorFactory, trie_cursor::TrieCursorFactory};
use reth_trie_parallel::{
proof_task::{ProofTaskCtx, ProofWorkerHandle},
@@ -287,7 +286,7 @@ where
let parent_state_root = env.parent_state_root;
let transaction_count = env.transaction_count;
let chunk_size = Self::adaptive_chunk_size(config, env.gas_used);
let chunk_size = config.multiproof_chunk_size();
let prewarm_handle = self.spawn_caching_with(
env,
prewarm_rx,
@@ -352,37 +351,28 @@ where
/// produce fewer state changes and most workers would be idle overhead.
const SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD: usize = 30;
/// Transaction count threshold below which sequential signature recovery is used.
/// Transaction count threshold below which sequential conversion is used.
///
/// For blocks with fewer than this many transactions, the rayon parallel iterator overhead
/// (work-stealing setup, channel-based reorder) exceeds the cost of sequential ECDSA
/// recovery. Inspired by Nethermind's `RecoverSignature` which uses sequential `foreach`
/// for small blocks.
/// (work-stealing setup, channel-based reorder) exceeds the cost of sequential conversion.
/// Inspired by Nethermind's `RecoverSignature` which uses sequential `foreach` for small
/// blocks.
const SMALL_BLOCK_TX_THRESHOLD: usize = 30;
/// Returns the multiproof chunk size adapted to the block's gas usage.
/// Number of leading transactions to convert sequentially before entering the rayon
/// parallel path.
///
/// For blocks with ≤20M gas used, a smaller chunk size (30) yields better throughput.
/// For larger blocks, the configured default chunk size is used.
const fn adaptive_chunk_size(config: &TreeConfig, gas_used: u64) -> Option<usize> {
if !config.multiproof_chunking_enabled() {
return None;
}
let size = if gas_used > 0 && gas_used <= SMALL_BLOCK_GAS_THRESHOLD {
SMALL_BLOCK_MULTIPROOF_CHUNK_SIZE
} else {
config.multiproof_chunk_size()
};
Some(size)
}
/// Rayon's work-stealing does not guarantee that index 0 is processed first, so the
/// ordered consumer can block for up to ~1ms waiting for the first slot. By converting
/// a small head sequentially and sending it immediately, execution can start without
/// waiting for rayon scheduling.
const PARALLEL_PREFETCH_COUNT: usize = 4;
/// Spawns a task advancing transaction env iterator and streaming updates through a channel.
///
/// For blocks with fewer than [`Self::SMALL_BLOCK_TX_THRESHOLD`] transactions, uses
/// sequential iteration to avoid rayon overhead. For larger blocks, uses rayon parallel
/// iteration with [`ForEachOrdered`] to recover signatures in parallel while streaming
/// iteration with [`ForEachOrdered`] to convert transactions in parallel while streaming
/// results to execution in the original transaction order.
#[expect(clippy::type_complexity)]
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
@@ -401,50 +391,50 @@ where
// Empty block — nothing to do.
} else if transaction_count < Self::SMALL_BLOCK_TX_THRESHOLD {
// Sequential path for small blocks — avoids rayon work-stealing setup and
// channel-based reorder overhead when it costs more than the ECDSA recovery itself.
// channel-based reorder overhead when it costs more than sequential conversion.
debug!(
target: "engine::tree::payload_processor",
transaction_count,
"using sequential sig recovery for small block"
);
self.executor.spawn_blocking(move || {
let _enter =
debug_span!(target: "engine::tree::payload_processor", "tx iterator").entered();
self.executor.spawn_blocking_named("tx-iterator", move || {
let (transactions, convert) = transactions.into_parts();
for (idx, tx) in transactions.into_iter().enumerate() {
let tx = convert.convert(tx);
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
if let Ok(tx) = &tx {
let _ = prewarm_tx.send((idx, tx.clone()));
}
let _ = execute_tx.send(tx);
}
convert_serial(transactions.into_iter(), &convert, &prewarm_tx, &execute_tx);
});
} else {
// Parallel path — recover signatures in parallel on rayon, stream results
// to execution in order via `for_each_ordered`.
self.executor.spawn_blocking(move || {
let _enter =
debug_span!(target: "engine::tree::payload_processor", "tx iterator").entered();
//
// To avoid a ~1ms stall waiting for rayon to schedule index 0, the first
// few transactions are recovered sequentially and sent immediately before
// entering the parallel iterator for the remainder.
let prefetch = Self::PARALLEL_PREFETCH_COUNT.min(transaction_count);
self.executor.spawn_blocking_named("tx-iterator", move || {
let (transactions, convert) = transactions.into_parts();
transactions
.into_par_iter()
let mut all: Vec<_> = transactions.into_iter().collect();
let rest = all.split_off(prefetch.min(all.len()));
// Convert the first few transactions sequentially so execution can
// start immediately without waiting for rayon work-stealing.
convert_serial(all.into_iter(), &convert, &prewarm_tx, &execute_tx);
// Convert the remaining transactions in parallel.
rest.into_par_iter()
.enumerate()
.map(|(idx, tx)| {
.map(|(i, tx)| {
let idx = i + prefetch;
let tx = convert.convert(tx);
tx.map(|tx| {
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
let tx = WithTxEnv { tx_env, tx: Arc::new(tx) };
// Send to prewarming out of order with the original index.
let _ = prewarm_tx.send((idx, tx.clone()));
tx
})
});
(idx, tx)
})
.for_each_ordered(|tx| {
.for_each_ordered(|(idx, tx)| {
let _ = execute_tx.send(tx);
debug!(target: "engine::tree::payload_processor", idx, "yielded transaction");
});
});
}
@@ -496,7 +486,7 @@ where
{
let to_prewarm_task = to_prewarm_task.clone();
self.executor.spawn_blocking(move || {
self.executor.spawn_blocking_named("prewarm", move || {
let mode = if skip_prewarm {
PrewarmMode::Skipped
} else if let Some(bal) = bal {
@@ -540,7 +530,7 @@ where
state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, ParallelStateRootError>>,
from_multi_proof: CrossbeamReceiver<MultiProofMessage>,
parent_state_root: B256,
chunk_size: Option<usize>,
chunk_size: usize,
) {
let preserved_sparse_trie = self.sparse_state_trie.clone();
let trie_metrics = self.trie_metrics.clone();
@@ -550,7 +540,9 @@ where
let executor = self.executor.clone();
let parent_span = Span::current();
self.executor.spawn_blocking(move || {
self.executor.spawn_blocking_named("sparse-trie", move || {
increase_thread_priority();
let _enter = debug_span!(target: "engine::tree::payload_processor", parent: parent_span, "sparse_trie_task")
.entered();
@@ -592,8 +584,6 @@ where
);
let result = task.run();
// Capture the computed state_root before sending the result
let computed_state_root = result.as_ref().ok().map(|outcome| outcome.state_root);
// Acquire the guard before sending the result to prevent a race condition:
// Without this, the next block could start after send() but before store(),
@@ -602,6 +592,7 @@ where
// block's take() blocks until we've stored the trie for reuse.
let mut guard = preserved_sparse_trie.lock();
let task_result = result.as_ref().ok().cloned();
// Send state root computation result - next block may start but will block on take()
if state_root_tx.send(result).is_err() {
// Receiver dropped - payload was likely invalid or cancelled.
@@ -625,7 +616,7 @@ where
// A failed computation may have left the trie in a partially updated state.
let _enter =
debug_span!(target: "engine::tree::payload_processor", "preserve").entered();
let deferred = if let Some(state_root) = computed_state_root {
let deferred = if let Some(result) = task_result {
let start = Instant::now();
let (trie, deferred) = task.into_trie_for_reuse(
prune_depth,
@@ -633,11 +624,12 @@ where
SPARSE_TRIE_MAX_NODES_SHRINK_CAPACITY,
SPARSE_TRIE_MAX_VALUES_SHRINK_CAPACITY,
disable_cache_pruning,
&result.trie_updates,
);
trie_metrics
.into_trie_for_reuse_duration_histogram
.record(start.elapsed().as_secs_f64());
guard.store(PreservedSparseTrie::anchored(trie, state_root));
guard.store(PreservedSparseTrie::anchored(trie, result.state_root));
deferred
} else {
debug!(
@@ -708,6 +700,31 @@ where
}
}
/// Converts transactions sequentially and sends them to the prewarm and execute channels.
fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
iter: impl Iterator<Item = RawTx>,
convert: &C,
prewarm_tx: &mpsc::SyncSender<(usize, WithTxEnv<TxEnv, Recovered>)>,
execute_tx: &mpsc::SyncSender<Result<WithTxEnv<TxEnv, Recovered>, Err>>,
) where
Tx: ExecutableTxParts<TxEnv, InnerTx, Recovered = Recovered>,
TxEnv: Clone,
C: ConvertTx<RawTx, Tx = Tx, Error = Err>,
{
for (idx, raw_tx) in iter.enumerate() {
let tx = convert.convert(raw_tx);
let tx = tx.map(|tx| {
let (tx_env, tx) = tx.into_parts();
WithTxEnv { tx_env, tx: Arc::new(tx) }
});
if let Ok(tx) = &tx {
let _ = prewarm_tx.send((idx, tx.clone()));
}
let _ = execute_tx.send(tx);
debug!(target: "engine::tree::payload_processor", idx, "yielded transaction");
}
}
/// Handle to all the spawned tasks.
///
/// Generic over `R` (receipt type) to allow sharing `Arc<ExecutionOutcome<R>>` with the
@@ -805,9 +822,7 @@ impl<Tx, Err, R: Send + Sync + 'static> PayloadHandle<Tx, Err, R> {
/// Returns iterator yielding transactions from the stream.
pub fn iter_transactions(&mut self) -> impl Iterator<Item = Result<Tx, Err>> + '_ {
core::iter::repeat_with(|| self.transactions.recv())
.take_while(|res| res.is_ok())
.map(|res| res.unwrap())
self.transactions.iter()
}
}
@@ -1039,11 +1054,13 @@ pub struct ExecutionEnv<Evm: ConfigureEvm> {
pub withdrawals: Option<Vec<Withdrawal>>,
}
impl<Evm: ConfigureEvm> Default for ExecutionEnv<Evm>
impl<Evm: ConfigureEvm> ExecutionEnv<Evm>
where
EvmEnvFor<Evm>: Default,
{
fn default() -> Self {
/// Creates a new [`ExecutionEnv`] with default values for testing.
#[cfg(any(test, feature = "test-utils"))]
pub fn test_default() -> Self {
Self {
evm_env: Default::default(),
hash: Default::default(),
@@ -1061,7 +1078,7 @@ mod tests {
use super::PayloadExecutionCache;
use crate::tree::{
cached_state::{CachedStateMetrics, ExecutionCache, SavedCache},
payload_processor::{evm_state_to_hashed_post_state, PayloadProcessor},
payload_processor::{evm_state_to_hashed_post_state, ExecutionEnv, PayloadProcessor},
precompile_cache::PrecompileCacheMap,
StateProviderBuilder, TreeConfig,
};
@@ -1336,7 +1353,7 @@ mod tests {
let provider_factory = BlockchainProvider::new(factory).unwrap();
let mut handle = payload_processor.spawn(
Default::default(),
ExecutionEnv::test_default(),
(
Vec::<Result<Recovered<TransactionSigned>, core::convert::Infallible>>::new(),
std::convert::identity,

View File

@@ -162,6 +162,14 @@ pub(crate) struct MultiProofTaskMetrics {
/// Histogram of sparse trie update durations.
pub sparse_trie_update_duration_histogram: Histogram,
/// Histogram of durations spent revealing multiproof results into the sparse trie.
pub sparse_trie_reveal_multiproof_duration_histogram: Histogram,
/// Histogram of durations spent coalescing multiple proof results from the channel.
pub sparse_trie_proof_coalesce_duration_histogram: Histogram,
/// Histogram of durations the event loop spent blocked waiting on channels.
pub sparse_trie_channel_wait_duration_histogram: Histogram,
/// Histogram of durations spent processing trie updates and promoting pending accounts.
pub sparse_trie_process_updates_duration_histogram: Histogram,
/// Histogram of sparse trie final update durations.
pub sparse_trie_final_update_duration_histogram: Histogram,
/// Histogram of sparse trie total durations.
@@ -189,7 +197,7 @@ pub(crate) struct MultiProofTaskMetrics {
pub(crate) fn dispatch_with_chunking<T, I>(
items: T,
chunking_len: usize,
chunk_size: Option<usize>,
chunk_size: usize,
max_targets_for_chunking: usize,
available_account_workers: usize,
available_storage_workers: usize,
@@ -203,10 +211,7 @@ where
available_account_workers > 1 ||
available_storage_workers > 1;
if should_chunk &&
let Some(chunk_size) = chunk_size &&
chunking_len > chunk_size
{
if should_chunk && chunking_len > chunk_size {
let mut num_chunks = 0usize;
for chunk in chunker(items, chunk_size) {
dispatch(chunk);

View File

@@ -20,9 +20,8 @@ use crate::tree::{
use alloy_consensus::transaction::TxHashRef;
use alloy_eip7928::BlockAccessList;
use alloy_eips::eip4895::Withdrawal;
use alloy_evm::Database;
use alloy_primitives::{keccak256, StorageKey, B256};
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
use crossbeam_channel::Sender as CrossbeamSender;
use metrics::{Counter, Gauge, Histogram};
use rayon::prelude::*;
use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Evm, EvmFor, RecoveredTx, SpecFor};
@@ -33,11 +32,11 @@ use reth_provider::{
StateReader,
};
use reth_revm::{database::StateProviderDatabase, state::EvmState};
use reth_tasks::Runtime;
use reth_tasks::{pool::WorkerPool, Runtime};
use reth_trie_parallel::targets_v2::MultiProofTargetsV2;
use std::sync::{
atomic::{AtomicBool, Ordering},
mpsc::{self, channel, Receiver, Sender, SyncSender},
mpsc::{self, channel, Receiver, Sender},
Arc,
};
use tracing::{debug, debug_span, instrument, trace, warn, Span};
@@ -54,15 +53,6 @@ pub enum PrewarmMode<Tx> {
Skipped,
}
/// A wrapper for transactions that includes their index in the block.
#[derive(Clone)]
struct IndexedTransaction<Tx> {
/// The transaction index in the block.
index: usize,
/// The wrapped transaction.
tx: Tx,
}
/// A task that is responsible for caching and prewarming the cache by executing transactions
/// individually in parallel.
///
@@ -122,82 +112,141 @@ where
)
}
/// Spawns all pending transactions as blocking tasks by first chunking them.
/// Streams pending transactions and executes them in parallel on the prewarming pool.
///
/// For Optimism chains, special handling is applied to the first transaction if it's a
/// deposit transaction (type 0x7E/126) which sets critical metadata that affects all
/// subsequent transactions in the block.
fn spawn_all<Tx>(
/// Kicks off EVM init on every pool thread, then uses `in_place_scope` to dispatch
/// transactions as they arrive and wait for all spawned tasks to complete before
/// clearing per-thread state. Workers that start via work-stealing lazily initialise
/// their EVM state on first access via [`get_or_init`](reth_tasks::pool::Worker::get_or_init).
fn spawn_txs_prewarm<Tx>(
&self,
pending: mpsc::Receiver<(usize, Tx)>,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
) where
Tx: ExecutableTxFor<Evm> + Clone + Send + 'static,
Tx: ExecutableTxFor<Evm> + Send + 'static,
{
let executor = self.executor.clone();
let ctx = self.ctx.clone();
let span = Span::current();
self.executor.spawn_blocking(move || {
let _enter = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: span, "spawn_all").entered();
self.executor.spawn_blocking_named("prewarm-txs", move || {
let _enter = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: span,
"prewarm_txs"
)
.entered();
let pool_threads = executor.prewarming_pool().current_num_threads();
// Don't spawn more workers than transactions. When transaction_count is 0
// (unknown), use all pool threads.
let workers_needed = if ctx.env.transaction_count > 0 {
ctx.env.transaction_count.min(pool_threads)
} else {
pool_threads
};
let ctx = &ctx;
let pool = executor.prewarming_pool();
let (done_tx, done_rx) = mpsc::sync_channel(workers_needed);
// Spawn workers
let tx_sender = ctx.clone().spawn_workers(workers_needed, &executor, to_multi_proof.clone(), done_tx.clone());
// Distribute transactions to workers
let mut tx_count = 0usize;
while let Ok((tx_index, tx)) = pending.recv() {
// Stop distributing if termination was requested
if ctx.terminate_execution.load(Ordering::Relaxed) {
trace!(
target: "engine::tree::payload_processor::prewarm",
"Termination requested, stopping transaction distribution"
);
break;
let to_multi_proof = to_multi_proof.as_ref();
pool.in_place_scope(|s| {
s.spawn(|_| {
pool.init::<PrewarmEvmState<Evm>>(|_| ctx.evm_for_ctx());
});
while let Ok((index, tx)) = pending.recv() {
if ctx.terminate_execution.load(Ordering::Relaxed) {
trace!(
target: "engine::tree::payload_processor::prewarm",
"Termination requested, stopping transaction distribution"
);
break;
}
tx_count += 1;
let parent_span = Span::current();
s.spawn(move |_| {
let _enter = debug_span!(
target: "engine::tree::payload_processor::prewarm",
parent: parent_span,
"prewarm_tx",
i = index,
)
.entered();
Self::transact_worker(ctx, index, tx, to_multi_proof);
});
}
let indexed_tx = IndexedTransaction { index: tx_index, tx };
// Send withdrawal prefetch targets after all transactions dispatched
if let Some(to_multi_proof) = to_multi_proof &&
let Some(withdrawals) = &ctx.env.withdrawals &&
!withdrawals.is_empty()
{
let targets = multiproof_targets_from_withdrawals(withdrawals);
let _ = to_multi_proof.send(MultiProofMessage::PrefetchProofs(targets));
}
});
// Send transaction to the workers
// Ignore send errors: workers listen to terminate_execution and may
// exit early when signaled.
let _ = tx_sender.send(indexed_tx);
tx_count += 1;
}
// Send withdrawal prefetch targets after all transactions have been distributed
if let Some(to_multi_proof) = to_multi_proof
&& let Some(withdrawals) = &ctx.env.withdrawals
&& !withdrawals.is_empty()
{
let targets = multiproof_targets_from_withdrawals(withdrawals);
let _ = to_multi_proof
.send(MultiProofMessage::PrefetchProofs(targets));
}
// drop sender and wait for all tasks to finish
drop(done_tx);
drop(tx_sender);
while done_rx.recv().is_ok() {}
// All tasks are done — clear per-thread EVM state for the next block.
pool.clear();
let _ = actions_tx
.send(PrewarmTaskEvent::FinishedTxExecution { executed_transactions: tx_count });
});
}
/// Executes a single prewarm transaction on the current pool thread's EVM.
///
/// Lazily initialises per-thread [`PrewarmEvmState`] via
/// [`get_or_init`](reth_tasks::pool::Worker::get_or_init) on first access.
fn transact_worker<Tx>(
ctx: &PrewarmContext<N, P, Evm>,
index: usize,
tx: Tx,
to_multi_proof: Option<&CrossbeamSender<MultiProofMessage>>,
) where
Tx: ExecutableTxFor<Evm>,
{
WorkerPool::with_worker_mut(|worker| {
let Some((evm, metrics, terminate_execution)) =
worker.get_or_init::<PrewarmEvmState<Evm>>(|| ctx.evm_for_ctx()).as_mut()
else {
return;
};
if terminate_execution.load(Ordering::Relaxed) {
return;
}
let start = Instant::now();
let (tx_env, tx) = tx.into_parts();
let res = match evm.transact(tx_env) {
Ok(res) => res,
Err(err) => {
trace!(
target: "engine::tree::payload_processor::prewarm",
%err,
tx_hash=%tx.tx().tx_hash(),
sender=%tx.signer(),
"Error when executing prewarm transaction",
);
metrics.transaction_errors.increment(1);
return;
}
};
metrics.execution_duration.record(start.elapsed());
if terminate_execution.load(Ordering::Relaxed) {
return;
}
if index > 0 {
let (targets, storage_targets) = multiproof_targets_from_state(res.state);
metrics.prefetch_storage_targets.record(storage_targets as f64);
if let Some(to_multi_proof) = to_multi_proof {
let _ = to_multi_proof.send(MultiProofMessage::PrefetchProofs(targets));
}
}
metrics.total_runtime.record(start.elapsed());
});
}
/// This method calls `ExecutionCache::update_with_guard` which requires exclusive access.
/// It should only be called after ensuring that:
/// 1. All prewarming tasks have completed execution
@@ -370,12 +419,12 @@ where
)]
pub fn run<Tx>(self, mode: PrewarmMode<Tx>, actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>)
where
Tx: ExecutableTxFor<Evm> + Clone + Send + 'static,
Tx: ExecutableTxFor<Evm> + Send + 'static,
{
// Spawn execution tasks based on mode
match mode {
PrewarmMode::Transactions(pending) => {
self.spawn_all(pending, actions_tx, self.to_multi_proof.clone());
self.spawn_txs_prewarm(pending, actions_tx, self.to_multi_proof.clone());
}
PrewarmMode::BlockAccessList(bal) => {
self.run_bal_prewarm(bal, actions_tx);
@@ -454,27 +503,24 @@ where
pub precompile_cache_map: PrecompileCacheMap<SpecFor<Evm>>,
}
/// Per-thread EVM state initialised by [`PrewarmContext::evm_for_ctx`] and stored in
/// [`WorkerPool`] workers via [`Worker::get_or_init`](reth_tasks::pool::Worker::get_or_init).
type PrewarmEvmState<Evm> = Option<(
EvmFor<Evm, StateProviderDatabase<reth_provider::StateProviderBox>>,
PrewarmMetrics,
Arc<AtomicBool>,
)>;
impl<N, P, Evm> PrewarmContext<N, P, Evm>
where
N: NodePrimitives,
P: BlockReader + StateProviderFactory + StateReader + Clone + 'static,
Evm: ConfigureEvm<Primitives = N> + 'static,
{
/// Splits this context into an evm, metrics, and the atomic bool for terminating execution.
/// Creates a per-thread EVM, metrics handle, and termination flag for prewarming.
#[instrument(level = "debug", target = "engine::tree::payload_processor::prewarm", skip_all)]
fn evm_for_ctx(self) -> Option<(EvmFor<Evm, impl Database>, PrewarmMetrics, Arc<AtomicBool>)> {
let Self {
env,
evm_config,
saved_cache,
provider,
metrics,
terminate_execution,
precompile_cache_disabled,
precompile_cache_map,
} = self;
let mut state_provider = match provider.build() {
fn evm_for_ctx(&self) -> PrewarmEvmState<Evm> {
let mut state_provider = match self.provider.build() {
Ok(provider) => provider,
Err(err) => {
trace!(
@@ -487,7 +533,7 @@ where
};
// Use the caches to create a new provider with caching
if let Some(saved_cache) = saved_cache {
if let Some(saved_cache) = &self.saved_cache {
let caches = saved_cache.cache().clone();
let cache_metrics = saved_cache.metrics().clone();
state_provider =
@@ -496,7 +542,7 @@ where
let state_provider = StateProviderDatabase::new(state_provider);
let mut evm_env = env.evm_env;
let mut evm_env = self.env.evm_env.clone();
// we must disable the nonce check so that we can execute the transaction even if the nonce
// doesn't match what's on chain.
@@ -508,130 +554,21 @@ where
// create a new executor and disable nonce checks in the env
let spec_id = *evm_env.spec_id();
let mut evm = evm_config.evm_with_env(state_provider, evm_env);
let mut evm = self.evm_config.evm_with_env(state_provider, evm_env);
if !precompile_cache_disabled {
if !self.precompile_cache_disabled {
// Only cache pure precompiles to avoid issues with stateful precompiles
evm.precompiles_mut().map_pure_precompiles(|address, precompile| {
CachedPrecompile::wrap(
precompile,
precompile_cache_map.cache_for_address(*address),
self.precompile_cache_map.cache_for_address(*address),
spec_id,
None, // No metrics for prewarm
)
});
}
Some((evm, metrics, terminate_execution))
}
/// Accepts a [`CrossbeamReceiver`] of transactions and a handle to prewarm task. Executes
/// transactions and streams [`MultiProofMessage::PrefetchProofs`] messages for each
/// transaction.
///
/// This function processes transactions sequentially from the receiver and emits outcome events
/// via the provided sender. Execution errors are logged and tracked but do not stop the batch
/// processing unless the task is explicitly cancelled.
///
/// Note: There are no ordering guarantees; this does not reflect the state produced by
/// sequential execution.
fn transact_batch<Tx>(
self,
txs: CrossbeamReceiver<IndexedTransaction<Tx>>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
done_tx: SyncSender<()>,
) where
Tx: ExecutableTxFor<Evm>,
{
let Some((mut evm, metrics, terminate_execution)) = self.evm_for_ctx() else { return };
while let Ok(IndexedTransaction { index, tx }) = txs.recv() {
let _enter = debug_span!(
target: "engine::tree::payload_processor::prewarm",
"prewarm tx",
i=index,
)
.entered();
// create the tx env
let start = Instant::now();
// If the task was cancelled, stop execution, and exit.
if terminate_execution.load(Ordering::Relaxed) {
break
}
let (tx_env, tx) = tx.into_parts();
let res = match evm.transact(tx_env) {
Ok(res) => res,
Err(err) => {
trace!(
target: "engine::tree::payload_processor::prewarm",
%err,
tx_hash=%tx.tx().tx_hash(),
sender=%tx.signer(),
"Error when executing prewarm transaction",
);
// Track transaction execution errors
metrics.transaction_errors.increment(1);
// skip error because we can ignore these errors and continue with the next tx
continue
}
};
metrics.execution_duration.record(start.elapsed());
// If the task was cancelled, stop execution, and exit.
if terminate_execution.load(Ordering::Relaxed) {
break
}
// Only send outcome for transactions after the first txn
// as the main execution will be just as fast
if index > 0 {
let (targets, storage_targets) = multiproof_targets_from_state(res.state);
metrics.prefetch_storage_targets.record(storage_targets as f64);
if let Some(to_multi_proof) = &to_multi_proof {
let _ = to_multi_proof.send(MultiProofMessage::PrefetchProofs(targets));
}
}
metrics.total_runtime.record(start.elapsed());
}
// send a message to the main task to flag that we're done
let _ = done_tx.send(());
}
/// Spawns worker tasks that pull transactions from a shared channel.
///
/// Returns the sender for distributing transactions to workers.
fn spawn_workers<Tx>(
self,
workers_needed: usize,
task_executor: &Runtime,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
done_tx: SyncSender<()>,
) -> CrossbeamSender<IndexedTransaction<Tx>>
where
Tx: ExecutableTxFor<Evm> + Send + 'static,
{
let (tx_sender, tx_receiver) = crossbeam_channel::unbounded();
// Spawn workers that all pull from the shared receiver
let span = Span::current();
for idx in 0..workers_needed {
let ctx = self.clone();
let to_multi_proof = to_multi_proof.clone();
let done_tx = done_tx.clone();
let rx = tx_receiver.clone();
let span = debug_span!(target: "engine::tree::payload_processor::prewarm", parent: &span, "prewarm worker", idx);
task_executor.prewarming_pool().spawn(move || {
let _enter = span.entered();
ctx.transact_batch(rx, to_multi_proof, done_tx);
});
}
tx_sender
Some((evm, self.metrics.clone(), self.terminate_execution.clone()))
}
/// Prefetches a single account and all its storage slots from the BAL into the cache.

View File

@@ -12,6 +12,7 @@ use crossbeam_channel::Receiver;
use reth_primitives_traits::Receipt;
use reth_trie_common::ordered_root::OrderedTrieRootEncodedBuilder;
use tokio::sync::oneshot;
use tracing::debug_span;
/// Receipt with index, ready to be sent to the background task for encoding and trie building.
#[derive(Debug, Clone)]
@@ -65,6 +66,13 @@ impl<R: Receipt> ReceiptRootTaskHandle<R> {
/// * `receipts_len` - The total number of receipts expected. This is needed to correctly order
/// the trie keys according to RLP encoding rules.
pub fn run(self, receipts_len: usize) {
let _span = debug_span!(
target: "engine::tree::payload_processor",
"receipt_root",
receipts_len,
)
.entered();
let mut builder = OrderedTrieRootEncodedBuilder::new(receipts_len);
let mut aggregated_bloom = Bloom::ZERO;
let mut encode_buf = Vec::new();

View File

@@ -1,5 +1,7 @@
//! Sparse Trie task related functionality.
use std::sync::Arc;
use crate::tree::{
multiproof::{
dispatch_with_chunking, evm_state_to_hashed_post_state, MultiProofMessage,
@@ -31,7 +33,7 @@ use reth_trie_sparse::{
SparseTrie,
};
use revm_primitives::{hash_map::Entry, B256Map};
use tracing::{debug, debug_span, error, instrument};
use tracing::{debug, debug_span, error, instrument, trace_span};
/// Maximum number of pending/prewarm updates that we accumulate in memory before actually applying.
const MAX_PENDING_UPDATES: usize = 100;
@@ -51,7 +53,7 @@ pub(super) struct SparseTrieCacheTask<A = ParallelSparseTrie, S = ParallelSparse
/// The size of proof targets chunk to spawn in one calculation.
/// If None, chunking is disabled and all targets are processed in a single proof.
chunk_size: Option<usize>,
chunk_size: usize,
/// If this number is exceeded and chunking is enabled, then this will override whether or not
/// there are any active workers and force chunking across workers. This is to prevent tasks
/// which are very long from hitting a single worker.
@@ -90,8 +92,8 @@ pub(super) struct SparseTrieCacheTask<A = ParallelSparseTrie, S = ParallelSparse
account_rlp_buf: Vec<u8>,
/// Whether the last state update has been received.
finished_state_updates: bool,
/// Pending targets to be dispatched to the proof workers.
pending_targets: MultiProofTargetsV2,
/// Pending proof targets queued for dispatch to proof workers.
pending_targets: PendingTargets,
/// Number of pending execution/prewarming updates received but not yet passed to
/// `update_leaves`.
pending_updates: usize,
@@ -112,13 +114,13 @@ where
proof_worker_handle: ProofWorkerHandle,
metrics: MultiProofTaskMetrics,
trie: SparseStateTrie<A, S>,
chunk_size: Option<usize>,
chunk_size: usize,
) -> Self {
let (proof_result_tx, proof_result_rx) = crossbeam_channel::unbounded();
let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
let parent_span = tracing::Span::current();
executor.spawn_blocking(move || {
executor.spawn_blocking_named("trie-hashing", move || {
let _span = debug_span!(parent: parent_span, "run_hashing_task").entered();
Self::run_hashing_task(updates, hashed_state_tx)
});
@@ -158,7 +160,7 @@ where
SparseTrieTaskMessage::PrefetchProofs(targets)
}
MultiProofMessage::StateUpdate(_, state) => {
let _span = debug_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing state update", n = state.len()).entered();
let _span = debug_span!(target: "engine::tree::payload_processor::sparse_trie", "hashing_state_update", n = state.len()).entered();
let hashed = evm_state_to_hashed_post_state(state);
SparseTrieTaskMessage::HashedState(hashed)
}
@@ -192,8 +194,10 @@ where
max_nodes_capacity: usize,
max_values_capacity: usize,
disable_pruning: bool,
updates: &TrieUpdates,
) -> (SparseStateTrie<A, S>, DeferredDrops) {
let Self { mut trie, .. } = self;
trie.commit_updates(updates);
if !disable_pruning {
trie.prune(prune_depth, max_storage_tries);
trie.shrink_to(max_nodes_capacity, max_values_capacity);
@@ -234,8 +238,13 @@ where
let now = Instant::now();
loop {
let mut t = Instant::now();
crossbeam_channel::select_biased! {
recv(self.updates) -> message => {
self.metrics
.sparse_trie_channel_wait_duration_histogram
.record(t.elapsed());
let update = match message {
Ok(m) => m,
Err(_) => {
@@ -249,17 +258,32 @@ where
self.pending_updates += 1;
}
recv(self.proof_result_rx) -> message => {
let phase_end = Instant::now();
self.metrics
.sparse_trie_channel_wait_duration_histogram
.record(phase_end.duration_since(t));
t = phase_end;
let Ok(result) = message else {
unreachable!("we own the sender half")
};
let mut result = result.result?;
let mut result = result.result?;
while let Ok(next) = self.proof_result_rx.try_recv() {
let res = next.result?;
result.extend(res);
}
let phase_end = Instant::now();
self.metrics
.sparse_trie_proof_coalesce_duration_histogram
.record(phase_end.duration_since(t));
t = phase_end;
self.on_proof_result(result)?;
self.metrics
.sparse_trie_reveal_multiproof_duration_histogram
.record(t.elapsed());
},
}
@@ -267,8 +291,10 @@ where
// If we don't have any pending messages, we can spend some time on computing
// storage roots and promoting account updates.
self.dispatch_pending_targets();
t = Instant::now();
self.process_new_updates()?;
self.promote_pending_account_updates()?;
self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
if self.finished_state_updates &&
self.account_updates.is_empty() &&
@@ -281,9 +307,11 @@ where
} else if self.updates.is_empty() || self.pending_updates > MAX_PENDING_UPDATES {
// If we don't have any pending updates OR we've accumulated a lot already, apply
// them to the trie,
t = Instant::now();
self.process_new_updates()?;
self.metrics.sparse_trie_process_updates_duration_histogram.record(t.elapsed());
self.dispatch_pending_targets();
} else if self.pending_targets.chunking_length() > self.chunk_size.unwrap_or_default() {
} else if self.pending_targets.len() > self.chunk_size {
// Make sure to dispatch targets if we've accumulated a lot of them.
self.dispatch_pending_targets();
}
@@ -306,7 +334,7 @@ where
Ok(StateRootComputeOutcome {
state_root,
trie_updates,
trie_updates: Arc::new(trie_updates),
#[cfg(feature = "trie-debug")]
debug_recorders,
})
@@ -404,12 +432,12 @@ where
})
}
#[instrument(
level = "debug",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
fn process_new_updates(&mut self) -> SparseTrieResult<()> {
if self.pending_updates == 0 {
return Ok(());
}
let _span = debug_span!("process_new_updates").entered();
self.pending_updates = 0;
// Firstly apply all new storage and account updates to the tries.
@@ -465,50 +493,38 @@ where
let storage_updates =
if new { &mut self.new_storage_updates } else { &mut self.storage_updates };
// Process all storage updates in parallel, skipping tries with no pending updates.
let span = tracing::Span::current();
let storage_results = storage_updates
.iter_mut()
.filter(|(_, updates)| !updates.is_empty())
.map(|(address, updates)| {
let trie = self.trie.take_or_create_storage_trie(address);
let fetched = self.fetched_storage_targets.remove(address).unwrap_or_default();
// Process all storage updates, skipping tries with no pending updates.
let span = debug_span!("process_storage_leaf_updates").entered();
for (address, updates) in storage_updates {
if updates.is_empty() {
continue;
}
let _enter = trace_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage_trie_leaf_updates", a=%address).entered();
(address, updates, fetched, trie)
})
.par_bridge_buffered()
.map(|(address, updates, mut fetched, mut trie)| {
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage trie leaf updates", a=%address).entered();
let mut targets = Vec::new();
let trie = self.trie.get_or_create_storage_trie_mut(*address);
let fetched = self.fetched_storage_targets.entry(*address).or_default();
let mut targets = Vec::new();
trie.update_leaves(updates, |path, min_len| match fetched.entry(path) {
Entry::Occupied(mut entry) => {
if min_len < *entry.get() {
entry.insert(min_len);
targets.push(Target::new(path).with_min_len(min_len));
}
}
Entry::Vacant(entry) => {
trie.update_leaves(updates, |path, min_len| match fetched.entry(path) {
Entry::Occupied(mut entry) => {
if min_len < *entry.get() {
entry.insert(min_len);
targets.push(Target::new(path).with_min_len(min_len));
}
})?;
SparseTrieResult::Ok((address, targets, fetched, trie))
})
.collect::<Result<Vec<_>, _>>()?;
drop(span);
for (address, targets, fetched, trie) in storage_results {
self.fetched_storage_targets.insert(*address, fetched);
self.trie.insert_storage_trie(*address, trie);
}
Entry::Vacant(entry) => {
entry.insert(min_len);
targets.push(Target::new(path).with_min_len(min_len));
}
})?;
if !targets.is_empty() {
self.pending_targets.storage_targets.entry(*address).or_default().extend(targets);
self.pending_targets.extend_storage_targets(address, targets);
}
}
drop(span);
// Process account trie updates and fill the account targets.
self.process_account_leaf_updates(new)?;
@@ -535,15 +551,13 @@ where
if min_len < *entry.get() {
entry.insert(min_len);
self.pending_targets
.account_targets
.push(Target::new(target).with_min_len(min_len));
.push_account_target(Target::new(target).with_min_len(min_len));
}
}
Entry::Vacant(entry) => {
entry.insert(min_len);
self.pending_targets
.account_targets
.push(Target::new(target).with_min_len(min_len));
.push_account_target(Target::new(target).with_min_len(min_len));
}
}
})?;
@@ -577,7 +591,7 @@ where
})
.par_bridge_buffered()
.for_each(|(address, trie)| {
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage root", ?address).entered();
let _enter = debug_span!(target: "engine::tree::payload_processor::sparse_trie", parent: &span, "storage_root", ?address).entered();
trie.root().expect("updates are drained, trie should be revealed by now");
});
drop(span);
@@ -594,18 +608,7 @@ where
return true;
} else if let Some(account) = account.take() {
let storage_root = self.trie.storage_root(addr).expect("updates are drained, storage trie should be revealed by now");
let encoded = if account.is_none_or(|account| account.is_empty()) &&
storage_root == EMPTY_ROOT_HASH
{
Vec::new()
} else {
account_rlp_buf.clear();
account
.unwrap_or_default()
.into_trie_account(storage_root)
.encode(account_rlp_buf);
account_rlp_buf.clone()
};
let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
num_promoted += 1;
return false;
@@ -613,13 +616,13 @@ where
}
// Get the current account state either from the trie or from latest account update.
let trie_account = if let Some(LeafUpdate::Changed(encoded)) = self.account_updates.get(addr) {
Some(encoded).filter(|encoded| !encoded.is_empty())
} else if !self.account_updates.contains_key(addr) {
self.trie.get_account_value(addr)
} else {
let trie_account = match self.account_updates.get(addr) {
Some(LeafUpdate::Changed(encoded)) => {
Some(encoded).filter(|encoded| !encoded.is_empty())
}
// Needs to be revealed first
return true;
Some(LeafUpdate::Touched) => return true,
None => self.trie.get_account_value(addr),
};
let trie_account = trie_account.map(|value| TrieAccount::decode(&mut &value[..]).expect("invalid account RLP"));
@@ -636,13 +639,7 @@ where
(trie_account.map(Into::into), self.trie.storage_root(addr).expect("account had storage updates that were applied to its trie, storage root must be revealed by now"))
};
let encoded = if account.is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH {
Vec::new()
} else {
account_rlp_buf.clear();
account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
account_rlp_buf.clone()
};
let encoded = encode_account_leaf_value(account, storage_root, account_rlp_buf);
self.account_updates.insert(*addr, LeafUpdate::Changed(encoded));
num_promoted += 1;
@@ -663,39 +660,89 @@ where
Ok(())
}
#[instrument(
level = "debug",
target = "engine::tree::payload_processor::sparse_trie",
skip_all
)]
fn dispatch_pending_targets(&mut self) {
if !self.pending_targets.is_empty() {
let chunking_length = self.pending_targets.chunking_length();
dispatch_with_chunking(
std::mem::take(&mut self.pending_targets),
chunking_length,
self.chunk_size,
self.max_targets_for_chunking,
self.proof_worker_handle.available_account_workers(),
self.proof_worker_handle.available_storage_workers(),
MultiProofTargetsV2::chunks,
|proof_targets| {
if let Err(e) = self.proof_worker_handle.dispatch_account_multiproof(
AccountMultiproofInput {
targets: proof_targets,
proof_result_sender: ProofResultContext::new(
self.proof_result_tx.clone(),
0,
HashedPostState::default(),
Instant::now(),
),
},
) {
error!("failed to dispatch account multiproof: {e:?}");
}
},
);
if self.pending_targets.is_empty() {
return;
}
let _span = debug_span!("dispatch_pending_targets").entered();
let (targets, chunking_length) = self.pending_targets.take();
dispatch_with_chunking(
targets,
chunking_length,
self.chunk_size,
self.max_targets_for_chunking,
self.proof_worker_handle.available_account_workers(),
self.proof_worker_handle.available_storage_workers(),
MultiProofTargetsV2::chunks,
|proof_targets| {
if let Err(e) =
self.proof_worker_handle.dispatch_account_multiproof(AccountMultiproofInput {
targets: proof_targets,
proof_result_sender: ProofResultContext::new(
self.proof_result_tx.clone(),
HashedPostState::default(),
Instant::now(),
),
})
{
error!("failed to dispatch account multiproof: {e:?}");
}
},
);
}
}
/// RLP-encodes the account as a [`TrieAccount`] leaf value, or returns empty for deletions.
fn encode_account_leaf_value(
account: Option<Account>,
storage_root: B256,
account_rlp_buf: &mut Vec<u8>,
) -> Vec<u8> {
if account.is_none_or(|account| account.is_empty()) && storage_root == EMPTY_ROOT_HASH {
return Vec::new();
}
account_rlp_buf.clear();
account.unwrap_or_default().into_trie_account(storage_root).encode(account_rlp_buf);
account_rlp_buf.clone()
}
/// Pending proof targets queued for dispatch to proof workers, along with their count.
#[derive(Default)]
struct PendingTargets {
/// The proof targets.
targets: MultiProofTargetsV2,
/// Number of account + storage proof targets currently queued.
len: usize,
}
impl PendingTargets {
/// Returns the number of pending targets.
const fn len(&self) -> usize {
self.len
}
/// Returns `true` if there are no pending targets.
const fn is_empty(&self) -> bool {
self.len == 0
}
/// Takes the pending targets, replacing with empty defaults.
fn take(&mut self) -> (MultiProofTargetsV2, usize) {
(std::mem::take(&mut self.targets), std::mem::take(&mut self.len))
}
/// Adds a target to the account targets.
fn push_account_target(&mut self, target: Target) {
self.targets.account_targets.push(target);
self.len += 1;
}
/// Extends storage targets for the given address.
fn extend_storage_targets(&mut self, address: &B256, targets: Vec<Target>) {
self.len += targets.len();
self.targets.storage_targets.entry(*address).or_default().extend(targets);
}
}
@@ -711,12 +758,12 @@ enum SparseTrieTaskMessage {
/// Outcome of the state root computation, including the state root itself with
/// the trie updates.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct StateRootComputeOutcome {
/// The state root.
pub state_root: B256,
/// The trie updates.
pub trie_updates: TrieUpdates,
pub trie_updates: Arc<TrieUpdates>,
/// Debug recorders taken from the sparse tries, keyed by `None` for account trie
/// and `Some(address)` for storage tries.
#[cfg(feature = "trie-debug")]
@@ -726,7 +773,7 @@ pub struct StateRootComputeOutcome {
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::{keccak256, Address, U256};
use alloy_primitives::{keccak256, Address, B256, U256};
use reth_trie_sparse::ParallelSparseTrie;
#[test]
@@ -777,4 +824,33 @@ mod tests {
assert!(hashed_state_rx.recv().is_err());
handle.join().unwrap();
}
#[test]
fn test_encode_account_leaf_value_empty_account_and_empty_root_is_empty() {
let mut account_rlp_buf = vec![0xAB];
let encoded = encode_account_leaf_value(None, EMPTY_ROOT_HASH, &mut account_rlp_buf);
assert!(encoded.is_empty());
// Early return should not touch the caller's buffer.
assert_eq!(account_rlp_buf, vec![0xAB]);
}
#[test]
fn test_encode_account_leaf_value_non_empty_account_is_rlp() {
let storage_root = B256::from([0x99; 32]);
let account = Some(Account {
nonce: 7,
balance: U256::from(42),
bytecode_hash: Some(B256::from([0xAA; 32])),
});
let mut account_rlp_buf = vec![0x00, 0x01];
let encoded = encode_account_leaf_value(account, storage_root, &mut account_rlp_buf);
let decoded = TrieAccount::decode(&mut &encoded[..]).expect("valid account RLP");
assert_eq!(decoded.nonce, 7);
assert_eq!(decoded.balance, U256::from(42));
assert_eq!(decoded.storage_root, storage_root);
assert_eq!(account_rlp_buf, encoded);
}
}

View File

@@ -52,7 +52,10 @@ use std::{
panic::{self, AssertUnwindSafe},
sync::{mpsc::RecvTimeoutError, Arc},
};
use tracing::{debug, debug_span, error, info, instrument, trace, warn};
use tracing::{debug, debug_span, error, info, instrument, trace, warn, Span};
/// Handle to a [`HashedPostState`] computed on a background thread.
type LazyHashedPostState = reth_tasks::LazyHandle<HashedPostState>;
/// Context providing access to tree state during validation.
///
@@ -289,7 +292,7 @@ where
let block = self.convert_to_block(input)?;
// Validate block consensus rules which includes header validation
if let Err(consensus_err) = self.validate_block_inner(&block) {
if let Err(consensus_err) = self.validate_block_inner(&block, None) {
// Header validation error takes precedence over execution error
return Err(InsertBlockError::new(block, consensus_err.into()).into())
}
@@ -331,19 +334,24 @@ where
V: PayloadValidator<T, Block = N::Block> + Clone,
Evm: ConfigureEngineEvm<T::ExecutionData, Primitives = N>,
{
// Spawn block conversion on a background thread so it runs concurrently with the
// Spawn payload conversion on a background thread so it runs concurrently with the
// rest of the function (setup + execution). For payloads this overlaps the cost of
// RLP decoding + header hashing; for already-converted blocks this is a no-op.
// RLP decoding + header hashing.
let is_payload = matches!(&input, BlockOrPayload::Payload(_));
let convert_to_block = match &input {
BlockOrPayload::Payload(_) => {
let payload_clone = input.clone();
let validator = self.validator.clone();
let (tx, rx) = tokio::sync::oneshot::channel();
self.payload_processor.executor().spawn_blocking(move || {
let BlockOrPayload::Payload(payload) = payload_clone else { unreachable!() };
let _ = tx.send(validator.convert_payload_to_block(payload));
});
Either::Left(rx)
let handle = self.payload_processor.executor().spawn_blocking_named(
"payload-convert",
move || {
let BlockOrPayload::Payload(payload) = payload_clone else {
unreachable!()
};
validator.convert_payload_to_block(payload)
},
);
Either::Left(handle)
}
BlockOrPayload::Block(_) => Either::Right(()),
};
@@ -353,9 +361,7 @@ where
let convert_to_block =
move |input: BlockOrPayload<T>| -> Result<SealedBlock<N::Block>, NewPayloadError> {
match convert_to_block {
Either::Left(rx) => rx.blocking_recv().map_err(|_| {
NewPayloadError::Other("block conversion task panicked".into())
})?,
Either::Left(handle) => handle.try_into_inner().expect("sole handle"),
Either::Right(()) => {
let BlockOrPayload::Block(block) = input else { unreachable!() };
Ok(block)
@@ -395,7 +401,7 @@ where
trace!(target: "engine::tree::payload_validator", "Fetching block state provider");
let _enter =
debug_span!(target: "engine::tree::payload_validator", "state provider").entered();
debug_span!(target: "engine::tree::payload_validator", "state_provider").entered();
let Some(provider_builder) =
ensure_ok!(self.state_provider_builder(parent_hash, ctx.state()))
else {
@@ -420,7 +426,7 @@ where
.into())
};
let evm_env = debug_span!(target: "engine::tree::payload_validator", "evm env")
let evm_env = debug_span!(target: "engine::tree::payload_validator", "evm_env")
.in_scope(|| self.evm_env_for(&input))
.map_err(NewPayloadError::other)?;
@@ -507,18 +513,59 @@ where
// needed. This frees up resources while state root computation continues.
let valid_block_tx = handle.terminate_caching(Some(output.clone()));
let block = convert_to_block(input)?.with_senders(senders);
// Spawn hashed post state computation in background so it runs concurrently with
// block conversion and receipt root computation. This is a pure CPU-bound task
// (keccak256 hashing of all changed addresses and storage slots).
let hashed_state_output = output.clone();
let hashed_state_provider = self.provider.clone();
let hashed_state: LazyHashedPostState =
self.payload_processor.executor().spawn_blocking_named("hash-post-state", move || {
let _span = debug_span!(
target: "engine::tree::payload_validator",
"hashed_post_state",
)
.entered();
hashed_state_provider.hashed_post_state(&hashed_state_output.state)
});
let block = convert_to_block(input)?;
let transaction_root = is_payload.then(|| {
let block = block.clone();
let parent_span = Span::current();
let num_hash = block.num_hash();
self.payload_processor.executor().spawn_blocking_named("payload-tx-root", move || {
let _span =
debug_span!(target: "engine::tree::payload_validator", parent: parent_span, "payload_tx_root", block = ?num_hash)
.entered();
block.body().calculate_tx_root()
})
});
let block = block.with_senders(senders);
// Wait for the receipt root computation to complete.
let receipt_root_bloom = receipt_root_rx
.blocking_recv()
.inspect_err(|_| {
tracing::error!(
target: "engine::tree::payload_validator",
"Receipt root task dropped sender without result, receipt root calculation likely aborted"
);
})
.ok();
let receipt_root_bloom = {
let _enter = debug_span!(
target: "engine::tree::payload_validator",
"wait_receipt_root",
)
.entered();
receipt_root_rx
.blocking_recv()
.inspect_err(|_| {
tracing::error!(
target: "engine::tree::payload_validator",
"Receipt root task dropped sender without result, receipt root calculation likely aborted"
);
})
.ok()
};
let transaction_root = transaction_root.map(|handle| {
let _span =
debug_span!(target: "engine::tree::payload_validator", "wait_payload_tx_root")
.entered();
handle.try_into_inner().expect("sole handle")
});
let hashed_state = ensure_ok_post_block!(
self.validate_post_execution(
@@ -526,7 +573,9 @@ where
&parent_block,
&output,
&mut ctx,
receipt_root_bloom
transaction_root,
receipt_root_bloom,
hashed_state,
),
block
);
@@ -570,7 +619,7 @@ where
let _has_diff = self.compare_trie_updates_with_serial(
overlay_factory.clone(),
&hashed_state,
trie_updates.clone(),
trie_updates.as_ref().clone(),
);
#[cfg(feature = "trie-debug")]
if _has_diff {
@@ -616,7 +665,7 @@ where
?elapsed,
"Regular root task finished"
);
maybe_state_root = Some((result.0, result.1, elapsed));
maybe_state_root = Some((result.0, Arc::new(result.1), elapsed));
}
Err(error) => {
debug!(target: "engine::tree::payload_validator", %error, "Parallel state root computation failed");
@@ -651,7 +700,7 @@ where
self.metrics.block_validation.state_root_task_fallback_success_total.increment(1);
}
(root, updates, root_time.elapsed())
(root, Arc::new(updates), root_time.elapsed())
};
self.metrics.block_validation.record_state_root(&trie_output, root_elapsed.as_secs_f64());
@@ -716,13 +765,19 @@ where
/// Validate if block is correct and satisfies all the consensus rules that concern the header
/// and block body itself.
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
fn validate_block_inner(&self, block: &SealedBlock<N::Block>) -> Result<(), ConsensusError> {
fn validate_block_inner(
&self,
block: &SealedBlock<N::Block>,
transaction_root: Option<B256>,
) -> Result<(), ConsensusError> {
if let Err(e) = self.consensus.validate_header(block.sealed_header()) {
error!(target: "engine::tree::payload_validator", ?block, "Failed to validate header {}: {e}", block.hash());
return Err(e)
}
if let Err(e) = self.consensus.validate_block_pre_execution(block) {
if let Err(e) =
self.consensus.validate_block_pre_execution_with_tx_root(block, transaction_root)
{
error!(target: "engine::tree::payload_validator", ?block, "Failed to validate block {}: {e}", block.hash());
return Err(e)
}
@@ -762,20 +817,27 @@ where
{
debug!(target: "engine::tree::payload_validator", "Executing block");
let mut db = State::builder()
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.without_state_clear()
.build();
let mut db = debug_span!(target: "engine::tree", "build_state_db").in_scope(|| {
State::builder()
.with_database(StateProviderDatabase::new(state_provider))
.with_bundle_update()
.without_state_clear()
.build()
});
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);
let (spec_id, mut executor) = {
let _span = debug_span!(target: "engine::tree", "create_evm").entered();
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 executor = self.evm_config.create_executor(evm, ctx);
(spec_id, executor)
};
if !self.config.precompile_cache_disabled() {
// Only cache pure precompiles to avoid issues with stateful precompiles
let _span = debug_span!(target: "engine::tree", "setup_precompile_cache").entered();
executor.evm_mut().precompiles_mut().map_pure_precompiles(|address, precompile| {
let metrics = self
.precompile_cache_metrics
@@ -797,7 +859,9 @@ where
let (receipt_tx, receipt_rx) = crossbeam_channel::unbounded();
let (result_tx, result_rx) = tokio::sync::oneshot::channel();
let task_handle = ReceiptRootTaskHandle::new(receipt_rx, result_tx);
self.payload_processor.executor().spawn_blocking(move || task_handle.run(receipts_len));
self.payload_processor
.executor()
.spawn_blocking_named("receipt-root", move || task_handle.run(receipts_len));
let transaction_count = input.transaction_count();
let executor = executor.with_state_hook(Some(Box::new(handle.state_hook())));
@@ -815,13 +879,13 @@ where
// Finish execution and get the result
let post_exec_start = Instant::now();
let (_evm, result) = debug_span!(target: "engine::tree", "finish")
let (_evm, result) = debug_span!(target: "engine::tree", "BlockExecutor::finish")
.in_scope(|| executor.finish())
.map(|(evm, result)| (evm.into_db(), result))?;
self.metrics.record_post_execution(post_exec_start.elapsed());
// Merge transitions into bundle state
debug_span!(target: "engine::tree", "merge transitions")
debug_span!(target: "engine::tree", "merge_transitions")
.in_scope(|| db.merge_transitions(BundleRetention::Reverts));
let output = BlockExecutionOutput { result, state: db.take_bundle() };
@@ -860,7 +924,7 @@ where
// Apply pre-execution changes (e.g., beacon root update)
let pre_exec_start = Instant::now();
debug_span!(target: "engine::tree", "pre execution")
debug_span!(target: "engine::tree", "pre_execution")
.in_scope(|| executor.apply_pre_execution_changes())?;
self.metrics.record_pre_execution(pre_exec_start.elapsed());
@@ -924,8 +988,9 @@ where
fn compute_state_root_parallel(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
) -> Result<(B256, TrieUpdates), ParallelStateRootError> {
let hashed_state = hashed_state.get();
// The `hashed_state` argument will be taken into account as part of the overlay, but we
// need to use the prefix sets which were generated from it to indicate to the
// ParallelStateRoot which parts of the trie need to be recomputed.
@@ -943,8 +1008,9 @@ where
/// trie updates for this block.
fn compute_state_root_serial(
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<(B256, TrieUpdates)> {
let hashed_state = hashed_state.get();
// The `hashed_state` argument will be taken into account as part of the overlay, but we
// need to use the prefix sets which were generated from it to indicate to the
// StateRoot which parts of the trie need to be recomputed.
@@ -982,7 +1048,7 @@ where
&self,
handle: &mut PayloadHandle<Tx, Err, R>,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
) -> ProviderResult<Result<StateRootComputeOutcome, ParallelStateRootError>> {
let Some(timeout) = self.config.state_root_task_timeout() else {
return Ok(handle.state_root());
@@ -1008,7 +1074,7 @@ where
let seq_overlay = overlay_factory;
let seq_hashed_state = hashed_state.clone();
self.payload_processor.executor().spawn_blocking(move || {
self.payload_processor.executor().spawn_blocking_named("serial-root", move || {
let result = Self::compute_state_root_serial(seq_overlay, &seq_hashed_state);
let _ = seq_tx.send(result);
});
@@ -1038,7 +1104,7 @@ where
let (state_root, trie_updates) = result?;
return Ok(Ok(StateRootComputeOutcome {
state_root,
trie_updates,
trie_updates: Arc::new(trie_updates),
#[cfg(feature = "trie-debug")]
debug_recorders: Vec::new(),
}));
@@ -1055,7 +1121,7 @@ where
let (state_root, trie_updates) = result?;
return Ok(Ok(StateRootComputeOutcome {
state_root,
trie_updates,
trie_updates: Arc::new(trie_updates),
#[cfg(feature = "trie-debug")]
debug_recorders: Vec::new(),
}));
@@ -1074,7 +1140,7 @@ where
fn compare_trie_updates_with_serial(
&self,
overlay_factory: OverlayStateProviderFactory<P>,
hashed_state: &HashedPostState,
hashed_state: &LazyHashedPostState,
task_trie_updates: TrieUpdates,
) -> bool {
debug!(target: "engine::tree::payload_validator", "Comparing trie updates with serial computation");
@@ -1090,17 +1156,20 @@ where
// Get a database provider to use as trie cursor factory
match overlay_factory.database_provider_ro() {
Ok(provider) => {
if let Err(err) = super::trie_updates::compare_trie_updates(
match super::trie_updates::compare_trie_updates(
&provider,
task_trie_updates,
serial_trie_updates,
) {
warn!(
target: "engine::tree::payload_validator",
%err,
"Error comparing trie updates"
);
return true;
Ok(has_diff) => return has_diff,
Err(err) => {
warn!(
target: "engine::tree::payload_validator",
%err,
"Error comparing trie updates"
);
return true;
}
}
}
Err(err) => {
@@ -1170,15 +1239,20 @@ where
///
/// If `receipt_root_bloom` is provided, it will be used instead of computing the receipt root
/// and logs bloom from the receipts.
///
/// The `hashed_state` handle wraps the background hashed post state computation.
#[instrument(level = "debug", target = "engine::tree::payload_validator", skip_all)]
#[expect(clippy::too_many_arguments)]
fn validate_post_execution<T: PayloadTypes<BuiltPayload: BuiltPayload<Primitives = N>>>(
&self,
block: &RecoveredBlock<N::Block>,
parent_block: &SealedHeader<N::BlockHeader>,
output: &BlockExecutionOutput<N::Receipt>,
ctx: &mut TreeCtx<'_, N>,
transaction_root: Option<B256>,
receipt_root_bloom: Option<ReceiptRootBloom>,
) -> Result<HashedPostState, InsertBlockErrorKind>
hashed_state: LazyHashedPostState,
) -> Result<LazyHashedPostState, InsertBlockErrorKind>
where
V: PayloadValidator<T, Block = N::Block>,
{
@@ -1186,7 +1260,7 @@ where
trace!(target: "engine::tree::payload_validator", block=?block.num_hash(), "Validating block consensus");
// validate block consensus rules
if let Err(e) = self.validate_block_inner(block) {
if let Err(e) = self.validate_block_inner(block, transaction_root) {
return Err(e.into())
}
@@ -1213,14 +1287,15 @@ where
}
drop(_enter);
let _enter =
debug_span!(target: "engine::tree::payload_validator", "hashed_post_state").entered();
let hashed_state = self.provider.hashed_post_state(&output.state);
drop(_enter);
// Wait for the background keccak256 hashing task to complete. This blocks until
// all changed addresses and storage slots have been hashed.
let hashed_state_ref =
debug_span!(target: "engine::tree::payload_validator", "wait_hashed_post_state")
.in_scope(|| hashed_state.get());
let _enter = debug_span!(target: "engine::tree::payload_validator", "validate_block_post_execution_with_hashed_state").entered();
if let Err(err) =
self.validator.validate_block_post_execution_with_hashed_state(&hashed_state, block)
self.validator.validate_block_post_execution_with_hashed_state(hashed_state_ref, block)
{
// call post-block hook
self.on_invalid_block(parent_block, block, output, None, ctx.state_mut());
@@ -1444,8 +1519,8 @@ where
block: RecoveredBlock<N::Block>,
execution_outcome: Arc<BlockExecutionOutput<N::Receipt>>,
ctx: &TreeCtx<'_, N>,
hashed_state: HashedPostState,
trie_output: TrieUpdates,
hashed_state: LazyHashedPostState,
trie_output: Arc<TrieUpdates>,
overlay_factory: OverlayStateProviderFactory<P>,
) -> ExecutedBlock<N> {
// Capture parent hash and ancestor overlays for deferred trie input construction.
@@ -1461,12 +1536,14 @@ where
overlay_blocks.iter().rev().map(|b| b.trie_data_handle()).collect();
// Create deferred handle with fallback inputs in case the background task hasn't completed.
let deferred_trie_data = DeferredTrieData::pending(
Arc::new(hashed_state),
Arc::new(trie_output),
anchor_hash,
ancestors,
);
// Resolve the lazy handle into Arc<HashedPostState>. By this point the hashed state has
// already been computed and used for state root verification, so .get() returns instantly.
let hashed_state = match hashed_state.try_into_inner() {
Ok(state) => Arc::new(state),
Err(handle) => Arc::new(handle.get().clone()),
};
let deferred_trie_data =
DeferredTrieData::pending(hashed_state, trie_output, anchor_hash, ancestors);
let deferred_handle_task = deferred_trie_data.clone();
let block_validation_metrics = self.metrics.block_validation.clone();
@@ -1552,7 +1629,9 @@ where
};
// Spawn task that computes trie data asynchronously.
self.payload_processor.executor().spawn_blocking(compute_trie_input_task);
self.payload_processor
.executor()
.spawn_blocking_named("trie-input", compute_trie_input_task);
ExecutedBlock::with_deferred_trie_data(
Arc::new(block),

View File

@@ -1,6 +1,9 @@
//! Contains a precompile cache backed by `schnellru::LruMap` (LRU by length).
use alloy_primitives::Bytes;
use alloy_primitives::{
map::{DefaultHashBuilder, FbBuildHasher},
Bytes,
};
use moka::policy::EvictionPolicy;
use reth_evm::precompiles::{DynPrecompile, Precompile, PrecompileInput};
use reth_primitives_traits::dashmap::DashMap;
@@ -13,7 +16,7 @@ const MAX_CACHE_SIZE: u32 = 10_000;
/// Stores caches for each precompile.
#[derive(Debug, Clone, Default)]
pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>>>)
pub struct PrecompileCacheMap<S>(Arc<DashMap<Address, PrecompileCache<S>, FbBuildHasher<20>>>)
where
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;
@@ -37,9 +40,7 @@ where
/// Cache for precompiles, for each input stores the result.
#[derive(Debug, Clone)]
pub struct PrecompileCache<S>(
moka::sync::Cache<Bytes, CacheEntry<S>, alloy_primitives::map::DefaultHashBuilder>,
)
pub struct PrecompileCache<S>(moka::sync::Cache<Bytes, CacheEntry<S>, DefaultHashBuilder>)
where
S: Eq + Hash + std::fmt::Debug + Send + Sync + Clone + 'static;

View File

@@ -139,7 +139,7 @@ impl<N: NodePrimitives> TreeState<N> {
///
/// Both parent hash and anchor hash must match to ensure the overlay is valid.
/// This prevents using a stale overlay after persistence has advanced the anchor.
pub(crate) fn get_cached_overlay(
pub fn get_cached_overlay(
&self,
parent_hash: B256,
expected_anchor: B256,

View File

@@ -2041,3 +2041,126 @@ mod forkchoice_updated_tests {
assert_eq!(last_persisted_number, canonical_tip);
}
}
/// Tests that `on_valid_downloaded_block` triggers a download for the actual head block when
/// the block matches a non-head sync target (safe or finalized).
///
/// This exercises the exact code path fixed in `on_downloaded_block`: after `insert_block`
/// returns `Inserted(Valid)`, `on_valid_downloaded_block` checks `sync_target.contains()`.
/// If the block is NOT the head, it should make canonical inline and emit a `Download`
/// event for the head — rather than returning `MakeCanonical` which would stop the download
/// pipeline.
///
/// Reproduces the hive test failure:
/// "Sync after 2 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts -
/// No Transactions: Timeout while waiting for secondary client to sync"
#[test]
fn test_on_valid_downloaded_non_head_sync_target_continues_to_head() {
reth_tracing::init_test_tracing();
let chain_spec = MAINNET.clone();
let mut test_harness = TestHarness::new(chain_spec);
// Build blocks: genesis (0) and safe block (1).
let blocks: Vec<_> = test_harness.block_builder.get_executed_blocks(0..2).collect();
let genesis = &blocks[0];
let safe_block = &blocks[1];
// Insert genesis and safe block into the tree. The safe block must be in the tree
// for `make_canonical` to succeed inside `on_valid_downloaded_block`.
test_harness = test_harness.with_blocks(vec![genesis.clone(), safe_block.clone()]);
let genesis_hash = genesis.recovered_block().hash();
let safe_hash = safe_block.recovered_block().hash();
let head_hash = B256::random(); // head block is unknown — hasn't been downloaded yet
// Reset canonical head to genesis so the safe block is in tree but not yet canonical.
test_harness.tree.state.tree_state.set_canonical_head(genesis.recovered_block().num_hash());
// Set the forkchoice tracker to SYNCING with head != safe.
let fcu_state = ForkchoiceState {
head_block_hash: head_hash,
safe_block_hash: safe_hash,
finalized_block_hash: genesis_hash,
};
test_harness
.tree
.state
.forkchoice_state_tracker
.set_latest(fcu_state, ForkchoiceStatus::Syncing);
// Call on_valid_downloaded_block — this is called by on_downloaded_block after
// insert_block returns Inserted(Valid).
let safe_num_hash = safe_block.recovered_block().num_hash();
let result = test_harness.tree.on_valid_downloaded_block(safe_num_hash).unwrap();
// With the fix: the engine makes safe canonical inline, then emits Download for head.
// Without the fix: it would return MakeCanonical{safe_hash} and never download head.
match result {
Some(TreeEvent::Download(DownloadRequest::BlockSet(hashes))) => {
assert!(
hashes.contains(&head_hash),
"Expected download for head block {head_hash}, got {hashes:?}"
);
}
Some(TreeEvent::TreeAction(TreeAction::MakeCanonical { sync_target_head })) => {
panic!(
"BUG: returned MakeCanonical for non-head block {sync_target_head} \
instead of downloading the actual head {head_hash}"
);
}
other => panic!("Expected Download event for head block, got: {other:?}"),
}
// Verify the safe block was made canonical.
assert_eq!(
test_harness.tree.state.tree_state.canonical_block_hash(),
safe_hash,
"Safe block should be canonical after on_valid_downloaded_block"
);
}
/// Tests that `on_valid_downloaded_block` returns `MakeCanonical` when the downloaded block
/// IS the sync target head (the normal non-buggy path).
#[test]
fn test_on_valid_downloaded_head_sync_target_returns_make_canonical() {
reth_tracing::init_test_tracing();
let chain_spec = MAINNET.clone();
let mut test_harness = TestHarness::new(chain_spec);
let blocks: Vec<_> = test_harness.block_builder.get_executed_blocks(0..2).collect();
let genesis = &blocks[0];
let head_block = &blocks[1];
test_harness = test_harness.with_blocks(vec![genesis.clone(), head_block.clone()]);
let genesis_hash = genesis.recovered_block().hash();
let head_hash = head_block.recovered_block().hash();
// Reset canonical head to genesis.
test_harness.tree.state.tree_state.set_canonical_head(genesis.recovered_block().num_hash());
// Set the forkchoice tracker: head == the downloaded block.
let fcu_state = ForkchoiceState {
head_block_hash: head_hash,
safe_block_hash: head_hash,
finalized_block_hash: genesis_hash,
};
test_harness
.tree
.state
.forkchoice_state_tracker
.set_latest(fcu_state, ForkchoiceStatus::Syncing);
let head_num_hash = head_block.recovered_block().num_hash();
let result = test_harness.tree.on_valid_downloaded_block(head_num_hash).unwrap();
// When the downloaded block IS the head, should return MakeCanonical.
match result {
Some(TreeEvent::TreeAction(TreeAction::MakeCanonical { sync_target_head })) => {
assert_eq!(sync_target_head, head_hash);
}
other => panic!("Expected MakeCanonical for head block, got: {other:?}"),
}
}

View File

@@ -101,11 +101,13 @@ impl StorageTrieUpdatesDiff {
/// Compares the trie updates from state root task, regular state root calculation and database,
/// and logs the differences if there's any.
///
/// Returns `true` if there are differences.
pub(crate) fn compare_trie_updates(
trie_cursor_factory: impl TrieCursorFactory,
task: TrieUpdates,
regular: TrieUpdates,
) -> Result<(), DatabaseError> {
) -> Result<bool, DatabaseError> {
let mut task = adjust_trie_updates(task);
let mut regular = adjust_trie_updates(regular);
@@ -179,9 +181,10 @@ pub(crate) fn compare_trie_updates(
}
// log differences
let has_differences = diff.has_differences();
diff.log_differences();
Ok(())
Ok(has_differences)
}
fn compare_storage_trie_updates<C: TrieCursor>(

View File

@@ -108,7 +108,7 @@ impl EngineMessageStore {
tracing::warn!(target: "engine::store", ?filename, "Skipping non json file");
}
}
Ok(filenames_by_ts.into_iter().flat_map(|(_, paths)| paths))
Ok(filenames_by_ts.into_values().flatten())
}
}

View File

@@ -97,7 +97,21 @@ where
{
let runner = match self.runner.take() {
Some(runner) => runner,
None => CliRunner::try_default_runtime()?,
None => {
let runtime_config = match &self.cli.command {
Commands::Node(command) => {
reth_tasks::RuntimeConfig::default().with_rayon(RayonConfig {
reserved_cpu_cores: command.engine.reserved_cpu_cores,
proof_storage_worker_threads: command.engine.storage_worker_count,
proof_account_worker_threads: command.engine.account_worker_count,
prewarming_threads: command.engine.prewarming_threads,
..Default::default()
})
}
_ => reth_tasks::RuntimeConfig::default(),
};
CliRunner::try_with_runtime_config(runtime_config)?
}
};
// Add network name if available to the logs dir
@@ -106,6 +120,11 @@ where
self.cli.logs.log_file_directory.join(chain_spec.chain().to_string());
}
// Apply node-specific log defaults before initializing tracing
if matches!(self.cli.command, Commands::Node(_)) {
self.cli.logs.apply_node_defaults();
}
self.init_tracing(&runner)?;
// Install the prometheus recorder to be sure to record all metrics
@@ -118,8 +137,8 @@ where
///
/// See [`Cli::init_tracing`] for more information.
pub fn init_tracing(&mut self, runner: &CliRunner) -> Result<()> {
if self.guard.is_none() {
self.guard = self.cli.init_tracing(runner, self.layers.take().unwrap_or_default())?;
if let Some(layers) = self.layers.take() {
self.guard = self.cli.init_tracing(runner, layers)?;
}
Ok(())
@@ -144,6 +163,8 @@ where
N: CliNodeTypes<Primitives: NodePrimitives<BlockHeader: HeaderMut>, ChainSpec: Hardforks>,
SubCmd: ExtendedCommand + Subcommand + fmt::Debug,
{
let rt = runner.runtime();
match cli.command {
Commands::Node(command) => {
// Validate RPC modules using the configured validator
@@ -154,28 +175,17 @@ where
Rpc::validate_selection(ws_api, "ws.api").map_err(|e| eyre!("{e}"))?;
}
let rayon_config = RayonConfig {
reserved_cpu_cores: command.engine.reserved_cpu_cores,
proof_storage_worker_threads: command.engine.storage_worker_count,
proof_account_worker_threads: command.engine.account_worker_count,
prewarming_threads: command.engine.prewarming_threads,
..Default::default()
};
let runner = CliRunner::try_with_runtime_config(
reth_tasks::RuntimeConfig::default().with_rayon(rayon_config),
)?;
runner.run_command_until_exit(|ctx| {
command.execute(ctx, FnLauncher::new::<C, Ext>(launcher))
})
}
Commands::Init(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::InitState(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::Init(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::InitState(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::Import(command) => {
runner.run_blocking_until_ctrl_c(command.execute::<N, _>(components))
runner.run_blocking_until_ctrl_c(command.execute::<N, _>(components, rt))
}
Commands::ImportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::ExportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>()),
Commands::ImportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::ExportEra(command) => runner.run_blocking_until_ctrl_c(command.execute::<N>(rt)),
Commands::DumpGenesis(command) => runner.run_blocking_until_ctrl_c(command.execute()),
Commands::Db(command) => {
runner.run_blocking_command_until_exit(|ctx| command.execute::<N>(ctx))
@@ -189,7 +199,9 @@ where
Commands::Prune(command) => runner.run_command_until_exit(|ctx| command.execute::<N>(ctx)),
#[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::ReExecute(command) => {
runner.run_until_ctrl_c(command.execute::<N>(components, rt))
}
Commands::Ext(command) => command.execute(runner),
}
}

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