Compare commits

...

94 Commits

Author SHA1 Message Date
Dan Cline
956a31c149 Merge branch 'main' into dan/static-file-split 2026-03-31 18:32:18 -04:00
joshieDo
f8efc76880 refactor(storage): remove changeset count APIs (#23310)
Co-authored-by: Amp <amp@ampcode.com>
2026-03-31 18:45:30 +00:00
stevencartavia
ef3cda7b66 feat: integrate reth-rpc-traits and remove IntoRpcTx (#23288) 2026-03-31 15:42:48 +00:00
Debjit Bhowal
23b68fcc38 feat(client): add era type override functionality to EraClient (#23307) 2026-03-31 15:33:46 +00:00
onbjerg
3802a31991 feat(download): make snapshot API URL overridable (#23303) 2026-03-31 15:12:15 +00:00
Brian Picciano
eab36bd18f chore(grafana): add sparse trie idle metrics to grafana overview (#23302)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
2026-03-31 13:03:39 +00:00
Brian Picciano
afbb3986d7 feat: add reth-bb binary with multi-segment big block execution support (#23140)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: amp[bot] <noreply@ampcode.com>
2026-03-31 12:45:12 +00:00
Matthias Seitz
0f7cd0fd98 chore: check trie-debug in zepter (#23304) 2026-03-31 12:36:19 +00:00
Arsenii Kulikov
f0d07c38be chore: bump alloy-evm (#23289) 2026-03-30 17:51:09 +00:00
Sergei Shulepov
930f2a6eb2 feat(engine): backpressure, take 2. (#23280) 2026-03-30 15:19:55 +00:00
figtracer
69bde3a5cc feat(trie): add SparseStateTrie::update_account_stateless for stateless validation (#23272) 2026-03-30 12:10:03 +00:00
figtracer
949fe33066 feat(db): add create_test_provider_factory_with_chain_spec_and_db_args (#23270) 2026-03-30 10:57:32 +00:00
John Chase
fc6462b5ba fix(nat): resolve DNS for ExternalAddr in external_addr_with (#23269) 2026-03-30 10:49:45 +00:00
0xWeakSheep
dae2485b04 test(txpool): add regression for parked basefee ancestor handling (#23277) 2026-03-30 10:28:56 +00:00
Crypto Nomad
dc22ece4d2 fix(cli): use HeaderTy for stage dump headers (#23274) 2026-03-30 10:27:21 +00:00
MagicJoshh
540f513a88 fix(net): prefer peer-reported block number in session activation (#23275) 2026-03-30 10:26:45 +00:00
Brian Picciano
43dfe6ed84 feat(trie): Record trie cursor metrics (#23252)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
2026-03-30 07:45:23 +00:00
github-actions[bot]
0f89525111 chore(deps): weekly cargo update (#23267)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-03-29 06:19:38 +00:00
DaniPopes
49339780c0 perf: use FastInstant for remaining metrics timing (#23265) 2026-03-29 06:01:22 +00:00
Dan Cline
27781443a6 chore(cli): add more WARN logging before we retry a download (#23258) 2026-03-28 04:59:16 +00:00
Arsenii Kulikov
afdf905295 feat: add a method to get payload resolve future (#23256) 2026-03-28 04:58:07 +00:00
Derek Cofausper
d2d2f34409 refactor(engine): remove op PayloadAttributesBuilder impl and op feature from engine-local (#23255)
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-03-27 18:25:28 +00:00
Matt Stam
4f34ac7e10 fix(consensus): retry block subscription on initial connection failure (#23233) 2026-03-27 16:55:59 +00:00
joshieDo
29bab063b7 feat(engine): share sparse trie pipeline with payload builder (#23246)
Co-authored-by: Amp <amp@ampcode.com>
2026-03-27 16:35:03 +00:00
Derek Cofausper
9d360728f3 refactor(payload): remove op ExecutionPayload impl and op feature from payload-primitives (#23253)
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-03-27 16:03:58 +00:00
Arsenii Kulikov
9db411efce chore: relax rpc converter impls (#23254) 2026-03-27 17:19:24 +01:00
Marco Lombardi
3208a4a615 fix(engine): avoid double decrement in account cache size (#23249) 2026-03-27 07:42:32 +00:00
AKABABA-ETH
7096d6ce1a fix(trie): use Entry API in MultiProofTargets::extend_inner (#23247) 2026-03-27 06:31:12 +00:00
Arsenii Kulikov
e3dbdbb115 feat: share execution cache with payload builder (#23242) 2026-03-26 16:03:07 +00:00
Muzry
0cbd0aa4cf chore(engine): return -38003 for FCUv2 payloadAttributes mismatch (#22924) 2026-03-26 14:07:18 +00:00
Sergei Shulepov
dba8b21aa7 fix(trie): before prune call root (#23243) 2026-03-26 12:31:13 +00:00
Matthias Seitz
ef0095b565 chore: bump alloy 1.8.2 (#23241) 2026-03-26 11:20:59 +01:00
Tim
b3dd2e246d feat: add hourly main regression bench (#23219) 2026-03-26 09:51:00 +00:00
stevencartavia
eb663aeaac chore(docker): bump lighthouse v8.1.3 (#23239) 2026-03-26 04:47:27 +00:00
Alexey Shekhirin
7f4a9a05ef fix(cli): use storage.v2 flag for storage settings (#23236) 2026-03-25 21:57:42 +00:00
DaniPopes
d3c3466c44 chore: make EvmConfig generic in examples (#23229) 2026-03-25 19:28:26 +00:00
Matthias Seitz
fb62487148 chore: bump alloy 1.8.1 (#23228) 2026-03-25 15:36:27 +00:00
Brian Picciano
401e751088 bench(ci): reuse cached big-block fixtures and select snapshot from manifest (#23193)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-03-25 15:16:43 +00:00
Derek Cofausper
7d31bb176c chore: remove deprecated reth-primitives crate (#23220)
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
2026-03-25 14:51:16 +00:00
Sergei Shulepov
50ce26f719 fix(trie): preserve prune invariants across sparse trie impls (#23226) 2026-03-25 14:41:34 +00:00
Derek Cofausper
4094d677e4 feat: enable jemalloc override_allocator_on_supported_platforms (#23214)
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-03-25 14:38:31 +00:00
Arsenii Kulikov
a37f91e6c0 refactor(tests): use FCU for requesting new payloads (#23222) 2026-03-25 14:22:59 +00:00
Alexey Shekhirin
78b97e81b7 fix(engine): do not report metrics for already seen payloads (#23227) 2026-03-25 14:17:50 +00:00
Emma Jamieson-Hoare
aedda7f6ad fix(engine): emit slow block log immediately after execution (#23225)
Co-authored-by: Amp <amp@ampcode.com>
2026-03-25 14:07:31 +00:00
stevencartavia
acc7b56e31 perf(payload): avoid tx clone in block building loop (#23180)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-03-25 13:18:33 +00:00
dependabot[bot]
87077ddcde chore(deps): bump the cargo-weekly group across 1 directory with 2 updates (#23211)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-03-25 11:40:59 +00:00
Derek Cofausper
5a66d0064c refactor(engine): extract PayloadExecutionCache into reth-execution-cache crate (#23209)
Co-authored-by: Arsenii Kulikov <62447812+klkvr@users.noreply.github.com>
2026-03-25 11:08:01 +00:00
Derek Cofausper
6183361f83 refactor: replace reth-primitives-traits with git dep to reth-core (#23210)
Co-authored-by: Arsenii Kulikov <62447812+klkvr@users.noreply.github.com>
2026-03-25 10:33:06 +00:00
Derek Cofausper
e91a900dd7 feat(engine): log in-flight persistence action in persist_until_complete (#23204)
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-03-25 09:30:00 +00:00
Matthias Seitz
33ec89994e feat(txpool): add TransactionValidationTaskExecutor::spawn (#23196) 2026-03-25 08:28:56 +00:00
Sergei Shulepov
80094e1bda fix(trie): avoid boundary parent unwrap panic in parallel sparse reveal (#23171) 2026-03-25 07:12:19 +00:00
Derek Cofausper
2e5730b6b5 chore(cli): suppress unused tracy_client dependency warning (#23212)
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-03-25 01:05:26 +00:00
Arsenii Kulikov
677d07041e refactor: use reth-core deps (#23186) 2026-03-24 21:56:25 +00:00
AKABABA-ETH
8606df3075 fix: remove apt-get upgrade from hive Dockerfile (#23206) 2026-03-24 18:12:36 +00:00
Arsenii Kulikov
cf83b198d3 refactor: remove PayloadBuilderAttributes (#23202) 2026-03-24 17:57:05 +00:00
DaniPopes
52ab4223a0 chore(meta): rename CLAUDE.md to AGENTS.md, symlink CLAUDE.md to it (#23203) 2026-03-24 17:51:51 +00:00
Matthias Seitz
15338b8113 chore: remove unused Extended type and op feature from primitives-traits (#23198) 2026-03-24 14:06:33 +00:00
Matthias Seitz
b3f5e62494 fix(init): track actual byte size instead of account count in dump_state (#23190) 2026-03-24 09:13:32 +00:00
Arsenii Kulikov
7b4c07338e refactor: simplify compact impls for scale types (#23185) 2026-03-23 23:20:10 +00:00
Arsenii Kulikov
bbed2e9ebf chore: unify InMemorySize (#23184)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-03-23 21:02:33 +00:00
Derek Cofausper
8c6e67bbaa fix(download): retry on extraction failure in resumable modular downloads (#23054)
Co-authored-by: Dan Cline <6798349+Rjected@users.noreply.github.com>
2026-03-23 19:51:13 +00:00
Derek Cofausper
8bb96ace64 refactor: remove SerdeBincodeCompat trait, use RLP for block serialization (#23158)
Co-authored-by: Arsenii Kulikov <62447812+klkvr@users.noreply.github.com>
Co-authored-by: Arsenii Kulikov <klkvrr@gmail.com>
2026-03-23 18:45:30 +00:00
Derek Cofausper
81dc5e2136 perf: disable readahead on slot-preimage MDBX environment (#23183)
Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com>
2026-03-23 18:26:42 +00:00
Derek Cofausper
ad00546081 feat(bench): add --wait-for-persistence flag (#23176)
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
2026-03-23 16:11:13 +00:00
Arsenii Kulikov
cc0c29e449 fix: always reinsert reorged blocks (#23175) 2026-03-23 14:15:36 +00:00
Derek Cofausper
bdcc262bbb chore(bench): tag tim and alexey on nightly bench failures (#23174)
Co-authored-by: Emma Jamieson-Hoare <21029500+emmajam@users.noreply.github.com>
2026-03-23 14:04:45 +00:00
Emma Jamieson-Hoare
0c90359be6 chore: fix build hive jobs (#23169)
Co-authored-by: Amp <amp@ampcode.com>
2026-03-23 14:04:12 +00:00
Alexey Shekhirin
b8aca9586a fix(cli): --storage.v2 without explicit true/false (#23173) 2026-03-23 13:42:21 +00:00
figtracer
fbbadab3be feat(net): include discv5 ENR data in admin_nodeInfo response (#23170) 2026-03-23 13:16:20 +00:00
figtracer
e0d40df3df perf(net): size-based backpressure for session broadcast messages (#22849) 2026-03-23 12:15:34 +00:00
Derek Cofausper
ff217592bc feat(tree): add idle time metrics to SparseTrieCacheTask and hashing task (#23136)
Co-authored-by: Brian Picciano <933154+mediocregopher@users.noreply.github.com>
Co-authored-by: Brian Picciano <me@mediocregopher.com>
Co-authored-by: Alexey Shekhirin <5773434+shekhirin@users.noreply.github.com>
Co-authored-by: Dan Cline <6798349+Rjected@users.noreply.github.com>
Co-authored-by: Alexey Shekhirin <github@shekhirin.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: YK <46377366+yongkangc@users.noreply.github.com>
Co-authored-by: YK <chiayongkang@hotmail.com>
Co-authored-by: amp[bot] <noreply@ampcode.com>
2026-03-23 11:52:33 +00:00
Derek Cofausper
a9b6969e77 fix: avoid OOM during init-state by dropping prefix sets (#23166)
Co-authored-by: Matthias Seitz <19890894+mattsse@users.noreply.github.com>
2026-03-23 11:37:48 +00:00
Emma Jamieson-Hoare
4338fb2631 chore(ci): ping AI agent on nightly Docker build failure (#23168)
Co-authored-by: Amp <amp@ampcode.com>
2026-03-23 11:14:42 +00:00
Crypto Nomad
cc6d14a2ca perf(rpc): avoid cloning InvalidBlock sealed block (#23162) 2026-03-23 10:54:11 +00:00
Derek Cofausper
cfab0c6371 chore(engine): downgrade yielded transaction log to trace (#22597)
Co-authored-by: Amp <amp@ampcode.com>
2026-03-23 10:26:21 +00:00
Matthias Seitz
bc7d585506 docs(consensus): document the validation pipeline and trait hierarchy (#22869) 2026-03-23 09:54:55 +00:00
Derek Cofausper
4bfc0083c9 docs: clarify transaction pool link wording (#23160)
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-03-22 21:41:38 +00:00
MagicJoshh
3d1dc4d9e2 fix(rpc): return error instead of empty response for missing blocks in debug_getRaw (#22675)
Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com>
2026-03-22 21:31:38 +00:00
Arsenii Kulikov
c9e9db184e fix: gracefully shut down engine (#23159) 2026-03-22 19:52:53 +00:00
Matthias Seitz
2d2778fa24 revert: "fix(engine/tree): continue sync-target progression for already-seen downloaded blocks" (#23157) 2026-03-22 17:17:35 +00:00
Arsenii Kulikov
7551d9c5dd refactor: remove bincode usage from HeaderStage (#23156) 2026-03-22 17:04:18 +00:00
stevencartavia
182f39db67 perf(engine): clone block body instead of full block for tx root task (#23147) 2026-03-22 04:08:55 +00:00
github-actions[bot]
e738bd34b3 chore(deps): weekly cargo update (#23148)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
2026-03-22 01:14:35 +00:00
stevencartavia
6fb5337786 perf(rpc): avoid cloning block env in pending block builder (#23144)
Co-authored-by: Matthias Seitz <matthias.seitz@outlook.de>
2026-03-21 04:51:47 +00:00
stevencartavia
f1c71d0c2e perf(rpc): remove redundant block id resolution in debug_trace_block (#23128) 2026-03-21 04:17:59 +00:00
Matthias Seitz
76e45117da chore(deps): allow lru advisory and bump rustls-webpki (#23145) 2026-03-21 05:04:27 +01:00
stevencartavia
40eb2d63e8 refactor(rpc): simplify block_transaction_count (#23139) 2026-03-20 14:44:01 +00:00
Dan Cline
832ac79b8e perf: reuse jar cursor in account/storage changeset split
Same optimization as transactions/receipts — reuse one cursor per jar
file instead of creating a new jar + cursor per block. Reduces cursor
creations from ~24.5M to ~50 for changeset segments.

Amp-Thread-ID: https://ampcode.com/threads/T-019c9c40-05f9-72c0-b6a0-7f8c32db6c6f
Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 19:01:45 -05:00
Dan Cline
47681f785d perf: reuse jar cursor across blocks in static file split
Instead of creating a new StaticFileJarProvider + StaticFileCursor for
every single transaction (billions on mainnet), reuse one cursor per
jar file. The cursor is only replaced when the next block's transactions
fall outside the current jar's tx range.

This eliminates billions of Vec<u8>::with_capacity allocations and
BTreeMap lookups, reducing cursor creations from ~2.8B to ~50 (one
per jar file).

Amp-Thread-ID: https://ampcode.com/threads/T-019c9c40-05f9-72c0-b6a0-7f8c32db6c6f
Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 18:58:23 -05:00
Dan Cline
cb47b195fd perf: bulk-load block body indices for static file split
Instead of holding a DB read transaction open and seeking per-block
throughout the entire split operation, load all BlockBodyIndices into
memory upfront with a single sequential walk_range scan. The table is
<1G so this is safe, and the loaded map is shared across the
transactions, receipts, and transaction senders segments.

Amp-Thread-ID: https://ampcode.com/threads/T-019c9c40-05f9-72c0-b6a0-7f8c32db6c6f
Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 18:42:14 -05:00
Dan Cline
5a9bbbe72b fix: disable long read tx safety in static file split command
The split_transactions, split_receipts, and split_transaction_senders
methods hold a read transaction open for the entire block range iteration,
which can trigger the long-lived read transaction timeout warning.

Since the split command runs offline against a non-progressing database,
disable the safety check to match the pattern used in other CLI commands
(db checksum, db list, db diff, etc.).

Amp-Thread-ID: https://ampcode.com/threads/T-019c9c40-05f9-72c0-b6a0-7f8c32db6c6f
Co-authored-by: Amp <amp@ampcode.com>
2026-02-26 18:25:57 -05:00
Dan Cline
749719c053 chore: regenerate CLI docs after rebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 15:16:48 -05:00
Georgios Konstantopoulos
cf12b2cb7f fix: update changeset offset API and generate CLI docs
- Update split_account_changesets and split_storage_changesets to use
  the new read_changeset_offset() method instead of the removed
  changeset_offset() on SegmentHeader
- Generate CLI docs for the new static-files split command

Amp-Thread-ID: https://ampcode.com/threads/T-019c6eb5-dde6-75ed-bf8c-b520a95d9fd4
Co-authored-by: Amp <amp@ampcode.com>
2026-02-25 15:16:48 -05:00
Dan Cline
8122fdf0af chore: static file split cli command 2026-02-25 15:16:48 -05:00
337 changed files with 8619 additions and 16120 deletions

View File

@@ -0,0 +1,5 @@
---
reth-cli-commands: patch
---
Added `snapshot_api_url` field to `DownloadDefaults` so downstream projects can override the snapshot discovery API endpoint. Previously, `discover_manifest_url`, `fetch_snapshot_api_entries`, and `print_snapshot_listing` used a hardcoded `snapshots.reth.rs` URL that bypassed the `DownloadDefaults` override mechanism.

View File

@@ -0,0 +1,6 @@
---
reth-primitives-traits: major
reth-downloaders: patch
---
Removed the local `size` module from `reth-primitives-traits` and replaced it with `alloy_consensus::InMemorySize`. Simplified `SignedTransaction` to a blanket impl covering all types satisfying the required bounds, removing `is_system_tx`, `auto_impl` attributes, and explicit impls for `EthereumTxEnvelope` and OP types. Updated import paths in `reth-downloaders` accordingly.

View File

@@ -0,0 +1,13 @@
---
reth-primitives-traits: minor
reth-engine-local: patch
reth-evm: patch
reth-node-builder: patch
reth-payload-primitives: patch
reth-rpc-convert: patch
reth-rpc-eth-api: patch
reth-db-api: patch
reth-db: patch
---
Removed the unused `Extended` type and `op` feature (including `op-alloy-consensus` dependency) from `reth-primitives-traits`. Updated all dependent crates to remove the now-unnecessary `reth-primitives-traits/op` feature flag propagation.

View File

@@ -0,0 +1,6 @@
---
reth-engine-local: patch
reth-node-builder: patch
---
Removed the `op` feature flag and `OpPayloadAttributes` `PayloadAttributesBuilder` implementation from `reth-engine-local`, along with the `op-alloy-rpc-types-engine` dependency. Updated `reth-node-builder` to no longer enable the removed `op` feature on `reth-engine-local`.

View File

@@ -0,0 +1,6 @@
---
reth-payload-primitives: patch
reth-engine-local: patch
---
Removed the `op` feature and `op-alloy-rpc-types-engine` dependency from `reth-payload-primitives`, along with the `ExecutionPayload` impl for `OpExecutionData`. Updated `reth-engine-local` to drop the corresponding feature flag dependency.

View File

@@ -0,0 +1,5 @@
---
reth-trie-sparse: patch
---
Fixed a panic in `ParallelSparseTrie::reveal_nodes` when a boundary node's upper parent is absent or non-branch (e.g. when an upper extension crosses the boundary). The code now skips gracefully instead of unwrapping. Added a regression test covering this case.

View File

@@ -0,0 +1,5 @@
---
reth-transaction-pool: minor
---
Added `TransactionValidationTaskExecutor::spawn` as a dedicated constructor that encapsulates spawning validation tasks on a runtime, and refactored `EthTransactionValidatorBuilder::build_with_tasks` to use it.

View File

@@ -0,0 +1,8 @@
---
reth-engine-primitives: minor
reth-engine-tree: major
reth-node-core: minor
reth-cli-commands: minor
---
Added persistence backpressure to the engine tree: when the canonical-minus-persisted block gap exceeds a configurable threshold (`--engine.persistence-backpressure-threshold`, default 16), the engine loop stalls on the persistence receiver instead of processing new incoming messages. Added CLI argument, cross-field validation, metrics (`backpressure_active`, `backpressure_stall_duration`), and tests.

View File

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

View File

@@ -11,10 +11,11 @@
# optional branch-sha is the PR head commit for cache key
#
# Outputs:
# baseline: <source-dir>/target/profiling/reth
# feature: <source-dir>/target/profiling/reth, reth-bench installed to cargo bin
# baseline: <source-dir>/target/profiling/reth (or reth-bb if BENCH_BIG_BLOCKS=true)
# feature: <source-dir>/target/profiling/reth (or reth-bb), reth-bench installed to cargo bin
#
# Required: mc (MinIO client) with a configured alias
# Optional env: BENCH_BIG_BLOCKS (true/false) — build reth-bb instead of reth
set -euo pipefail
MC="mc"
@@ -22,6 +23,16 @@ MODE="$1"
SOURCE_DIR="$2"
COMMIT="$3"
BIG_BLOCKS="${BENCH_BIG_BLOCKS:-false}"
# The node binary to build: reth-bb for big blocks, reth otherwise
if [ "$BIG_BLOCKS" = "true" ]; then
NODE_BIN="reth-bb"
NODE_PKG="-p reth-bb"
else
NODE_BIN="reth"
NODE_PKG="--bin reth"
fi
# Tracy support: when BENCH_TRACY is "on" or "full", add Tracy cargo features
# and frame pointers for accurate stack traces.
EXTRA_FEATURES=""
@@ -62,18 +73,18 @@ case "$MODE" in
mkdir -p "${SOURCE_DIR}/target/profiling"
CACHE_VALID=false
if $MC stat "${BUCKET}/reth" &>/dev/null; then
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
if $MC stat "${BUCKET}/${NODE_BIN}" &>/dev/null; then
echo "Cache hit for baseline (${COMMIT}), downloading ${NODE_BIN}..."
$MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
chmod +x "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
if verify_binary "${SOURCE_DIR}/target/profiling/${NODE_BIN}" "${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..."
echo "Building baseline ${NODE_BIN} (${COMMIT}) from source..."
cd "${SOURCE_DIR}"
FEATURES_ARG=""
WORKSPACE_ARG=""
@@ -84,8 +95,8 @@ case "$MODE" in
fi
# shellcheck disable=SC2086
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
cargo build --profile profiling --bin reth $WORKSPACE_ARG $FEATURES_ARG
$MC cp target/profiling/reth "${BUCKET}/reth"
cargo build --profile profiling $NODE_PKG $WORKSPACE_ARG $FEATURES_ARG
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
fi
;;
@@ -94,32 +105,34 @@ case "$MODE" in
BUCKET="minio/reth-binaries/${BRANCH_SHA}${BUILD_SUFFIX}"
CACHE_VALID=false
if $MC stat "${BUCKET}/reth" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
if $MC stat "${BUCKET}/${NODE_BIN}" &>/dev/null && $MC stat "${BUCKET}/reth-bench" &>/dev/null; then
echo "Cache hit for ${BRANCH_SHA}, downloading binaries..."
mkdir -p "${SOURCE_DIR}/target/profiling"
$MC cp "${BUCKET}/reth" "${SOURCE_DIR}/target/profiling/reth"
$MC cp "${BUCKET}/${NODE_BIN}" "${SOURCE_DIR}/target/profiling/${NODE_BIN}"
$MC cp "${BUCKET}/reth-bench" /home/ubuntu/.cargo/bin/reth-bench
chmod +x "${SOURCE_DIR}/target/profiling/reth" /home/ubuntu/.cargo/bin/reth-bench
if verify_binary "${SOURCE_DIR}/target/profiling/reth" "${COMMIT}"; then
chmod +x "${SOURCE_DIR}/target/profiling/${NODE_BIN}" /home/ubuntu/.cargo/bin/reth-bench
if verify_binary "${SOURCE_DIR}/target/profiling/${NODE_BIN}" "${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..."
echo "Building feature ${NODE_BIN} (${COMMIT}) from source..."
cd "${SOURCE_DIR}"
rustup show active-toolchain || rustup default stable
if [ -n "$EXTRA_FEATURES" ]; then
# Can't use `make profiling` when adding features; build explicitly
# --workspace is needed for cross-package feature syntax (tracy-client/ondemand)
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
cargo build --profile profiling --workspace --bin reth --features "${EXTRA_FEATURES}"
cargo build --profile profiling --workspace $NODE_PKG --features "${EXTRA_FEATURES}"
else
make profiling
# shellcheck disable=SC2086
RUSTFLAGS="-C target-cpu=native${EXTRA_RUSTFLAGS}" \
cargo build --profile profiling $NODE_PKG
fi
make install-reth-bench
$MC cp target/profiling/reth "${BUCKET}/reth"
$MC cp "target/profiling/${NODE_BIN}" "${BUCKET}/${NODE_BIN}"
$MC cp "$(which reth-bench)" "${BUCKET}/reth-bench"
fi
;;

View File

@@ -18,7 +18,11 @@ set -euo pipefail
LABEL="$1"
BINARY="$2"
OUTPUT_DIR="$3"
DATADIR="$SCHELK_MOUNT/datadir"
DATADIR_NAME="datadir"
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
DATADIR_NAME="datadir-big-blocks"
fi
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
mkdir -p "$OUTPUT_DIR"
LOG="${OUTPUT_DIR}/node.log"
@@ -128,12 +132,6 @@ if "$BINARY" node --help 2>/dev/null | grep -qF -- '--debug.startup-sync-state-i
SYNC_STATE_IDLE=true
fi
# Big blocks mode requires the testing API, skip-invalid-transactions, and
# skip-gas-limit-ramp-check + gas-limit override to avoid the 6800-block ramp.
if [ "$BIG_BLOCKS" = "true" ]; then
RETH_ARGS+=(--http.api eth,net,web3,reth,testing --rpc.max-request-size max --testing.skip-invalid-transactions --testing.skip-gas-limit-ramp-check --testing.gas-limit 1000000000)
fi
# Append per-label extra node args (baseline or feature)
EXTRA_NODE_ARGS=""
case "$LABEL" in
@@ -244,7 +242,7 @@ BENCH_NICE="sudo nice -n -20 sudo -u $(id -un)"
# Build optional flags
EXTRA_BENCH_ARGS=()
if [ "${BENCH_RETH_NEW_PAYLOAD:-true}" != "false" ]; then
EXTRA_BENCH_ARGS+=(--reth-new-payload)
EXTRA_BENCH_ARGS+=(--reth-new-payload --wait-for-persistence)
fi
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
EXTRA_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
@@ -252,7 +250,7 @@ fi
if [ "$BIG_BLOCKS" = "true" ]; then
# Big blocks mode: replay pre-generated payloads
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
BIG_BLOCKS_DIR="${BENCH_BIG_BLOCKS_DIR:-${BENCH_WORK_DIR}/big-blocks}"
# Start tracy-capture so profile only covers the benchmark
if [ "${BENCH_TRACY:-off}" != "off" ]; then
@@ -262,9 +260,18 @@ if [ "$BIG_BLOCKS" = "true" ]; then
sleep 0.5 # give tracy-capture time to connect
fi
BB_BENCH_ARGS=(--reth-new-payload --wait-for-persistence)
if [ -n "${BENCH_WAIT_TIME:-}" ]; then
BB_BENCH_ARGS+=(--wait-time "$BENCH_WAIT_TIME")
fi
# Limit number of payloads if blocks count is specified
if [ "${BENCH_BLOCKS:-0}" -gt 0 ] 2>/dev/null; then
BB_BENCH_ARGS+=(--count "$BENCH_BLOCKS")
fi
echo "Running big blocks benchmark (replay-payloads)..."
$BENCH_NICE "$RETH_BENCH" replay-payloads \
"${EXTRA_BENCH_ARGS[@]}" \
"${BB_BENCH_ARGS[@]}" \
--payload-dir "$BIG_BLOCKS_DIR/payloads" \
--engine-rpc-url http://127.0.0.1:8551 \
--jwt-secret "$DATADIR/jwt.hex" \

View File

@@ -22,9 +22,18 @@ set -euo pipefail
MC="mc"
BUCKET="minio/reth-snapshots"
MANIFEST_PATH="reth-1-minimal-stable/manifest.json"
DATADIR="$SCHELK_MOUNT/datadir"
HASH_FILE="$HOME/.reth-bench-snapshot-hash"
# Allow overriding the snapshot name (e.g. for big-blocks mode where the
# big-blocks manifest specifies which base snapshot to use).
SNAPSHOT_NAME="${BENCH_SNAPSHOT_NAME:-reth-1-minimal-stable}"
MANIFEST_PATH="${SNAPSHOT_NAME}/manifest.json"
DATADIR_NAME="datadir"
HASH_MODE_SUFFIX=""
if [ "${BENCH_BIG_BLOCKS:-false}" = "true" ]; then
DATADIR_NAME="datadir-big-blocks"
HASH_MODE_SUFFIX="-big-blocks"
fi
DATADIR="$SCHELK_MOUNT/$DATADIR_NAME"
HASH_FILE="$HOME/.reth-bench-snapshot-hash${HASH_MODE_SUFFIX}"
# Fetch manifest and compute content hash for reliable freshness check
MANIFEST_CONTENT=$($MC cat "${BUCKET}/${MANIFEST_PATH}" 2>/dev/null) || {
@@ -60,7 +69,7 @@ if [ -z "$MINIO_ENDPOINT" ]; then
echo "::error::Failed to resolve MinIO endpoint from mc alias 'minio'"
exit 1
fi
BASE_URL="${MINIO_ENDPOINT}/reth-snapshots/reth-1-minimal-stable"
BASE_URL="${MINIO_ENDPOINT}/reth-snapshots/${SNAPSHOT_NAME}"
# Rewrite manifest's base_url with the runner-reachable endpoint
MANIFEST_TMP=$(mktemp --suffix=.json)

View File

@@ -1,31 +1,130 @@
#!/usr/bin/env bash
#
# Resolves baseline and feature refs for nightly regression benchmark runs.
# Resolves baseline and feature refs for scheduled benchmark runs.
#
# Queries the latest successful scheduled docker.yml run via GitHub API
# to find the commit that built the nightly Docker image. Compares with
# the last successful feature ref (from GH Actions cache) to determine
# baseline, detect staleness, and decide whether to skip.
# Supports two modes:
# nightly — Queries the latest successful scheduled docker.yml run via
# GitHub API to find the nightly Docker image commit. Compares
# with the last successful feature ref to detect staleness.
# hourly — Compares origin/main HEAD against the last successfully
# benchmarked commit (falls back to HEAD~1 on first run).
# Checks for in-progress sibling runs to avoid overlap.
#
# Usage: bench-nightly-refs.sh [--force]
# Usage: bench-scheduled-refs.sh <force> <mode>
# force — "true" to run even if no new commit (bypass skip logic)
# mode — "nightly" or "hourly"
#
# Outputs (via GITHUB_OUTPUT):
# baseline-ref — commit SHA for baseline
# feature-ref — commit SHA for feature (current nightly)
# should-skip — "true" if no new nightly since last run
# is-stale — "true" if latest nightly build is >24h old
# stale-age-hours — age of the nightly build in hours (only if stale)
# nightly-created — ISO timestamp of the nightly build
# feature-ref — commit SHA for feature
# should-skip — "true" if no new commit since last run or sibling in progress
# is-stale — "true" if latest nightly build is >24h old (nightly only)
# stale-age-hours — age of the nightly build in hours (nightly only)
# nightly-created — ISO timestamp of the nightly build (nightly only)
#
# Reads:
# .nightly-state/last-feature-ref (from GH Actions cache, may not exist)
# .nightly-state/last-feature-ref (nightly, from GH Actions cache)
# .hourly-state/last-feature-ref (hourly, from GH Actions cache)
#
# Requires: gh (GitHub CLI), jq, date
# Requires: gh (GitHub CLI), jq, date, git (hourly mode)
set -euo pipefail
FORCE="${1:-false}"
MODE="${2:-nightly}"
REPO="${GITHUB_REPOSITORY:-paradigmxyz/reth}"
echo "Mode: $MODE, Force: $FORCE"
# ==========================================================================
# Hourly mode: compare origin/main HEAD vs HEAD~1
# ==========================================================================
if [ "$MODE" = "hourly" ]; then
# --- Step 1: Resolve feature ref from git ---
echo "::group::Resolving hourly refs from git"
git fetch origin main --quiet
FEATURE_REF=$(git rev-parse origin/main)
echo "Feature (HEAD): $FEATURE_REF"
echo "::endgroup::"
# --- Step 2: Check for in-progress sibling runs ---
echo "::group::Checking for in-progress sibling runs"
CURRENT_RUN_ID="${GITHUB_RUN_ID:-0}"
IN_PROGRESS=$(gh run list \
-R "$REPO" \
--workflow=bench-scheduled.yml \
--status=in_progress \
--json databaseId \
--jq "[.[] | select(.databaseId != $CURRENT_RUN_ID)] | length")
SHOULD_SKIP="false"
if [ "$IN_PROGRESS" -gt 0 ]; then
echo "::warning::Previous bench run still in progress ($IN_PROGRESS sibling run(s) found). Skipping."
SHOULD_SKIP="true"
# Output a flag so the workflow can send a Slack alert
echo "long-running=true" >> "$GITHUB_OUTPUT"
else
echo "No in-progress sibling runs"
echo "long-running=false" >> "$GITHUB_OUTPUT"
fi
echo "::endgroup::"
# --- Step 3: Read last successful feature ref from cache ---
echo "::group::Reading cached state"
LAST_FEATURE_REF=""
STATE_FILE=".hourly-state/last-feature-ref"
if [ -f "$STATE_FILE" ]; then
LAST_FEATURE_REF=$(tr -d '[:space:]' < "$STATE_FILE")
echo "Previous feature ref: $LAST_FEATURE_REF"
else
echo "No cached state found (first run)"
fi
echo "::endgroup::"
# --- Step 4: Determine baseline and skip logic ---
echo "::group::Resolving baseline and skip logic"
if [ "$SHOULD_SKIP" = "true" ]; then
BASELINE_REF=$(git rev-parse origin/main~1)
echo "Already marked skip (sibling in progress)"
elif [ -z "$LAST_FEATURE_REF" ]; then
# First run: no previous state, fall back to HEAD~1
BASELINE_REF=$(git rev-parse origin/main~1)
echo "First run — using HEAD~1 as baseline"
elif [ "$LAST_FEATURE_REF" = "$FEATURE_REF" ]; then
BASELINE_REF="$LAST_FEATURE_REF"
if [ "$FORCE" = "true" ] || [ "$FORCE" = "--force" ]; then
echo "No new commits on main, but force=true — running anyway"
else
SHOULD_SKIP="true"
echo "No new commits on main since last run — will skip"
fi
else
# Normal case: use last benchmarked commit as baseline
BASELINE_REF="$LAST_FEATURE_REF"
echo "New commit(s) on main detected — comparing against last benchmarked commit"
fi
echo "Baseline: $BASELINE_REF"
echo "Feature: $FEATURE_REF"
echo "Skip: $SHOULD_SKIP"
echo "::endgroup::"
# --- Step 5: Write outputs ---
{
echo "baseline-ref=$BASELINE_REF"
echo "feature-ref=$FEATURE_REF"
echo "should-skip=$SHOULD_SKIP"
echo "is-stale=false"
echo "stale-age-hours=0"
echo "nightly-created="
} >> "$GITHUB_OUTPUT"
exit 0
fi
# ==========================================================================
# Nightly mode: query latest Docker nightly build (original logic)
# ==========================================================================
# --- Step 1: Query latest successful scheduled docker.yml run ---
echo "::group::Querying latest nightly docker build"

View File

@@ -20,5 +20,6 @@
"SuperFluffy": "U095BKHB2Q4",
"kamsz": "U0A2563UBRD",
"zerosnacks": "U09FARPMN74",
"samczsun": "U096R14E4H3"
"samczsun": "U096R14E4H3",
"laibe": "U09FARE0B9Q"
}

View File

@@ -2,9 +2,6 @@
set -uo pipefail
crates_to_check=(
reth-codecs-derive
reth-primitives
reth-primitives-traits
reth-network-peers
reth-trie-common
reth-trie-sparse

View File

@@ -22,6 +22,7 @@ exclude_crates=(
reth-downloaders
reth-e2e-test-utils
reth-engine-service
reth-execution-cache
reth-engine-tree
reth-engine-util
reth-eth-wire
@@ -55,6 +56,7 @@ exclude_crates=(
reth-ress-provider
# The following are not supposed to be working
reth # all of the crates below
reth-bb # binary-only, uses tokio features unsupported on wasm
reth-storage-rpc-provider
reth-invalid-block-hooks # reth-provider
reth-libmdbx # mdbx

View File

@@ -7,7 +7,7 @@ FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config
RUN apt-get update && apt-get install -y libclang-dev pkg-config
#
# We prepare the build plan

View File

@@ -11,8 +11,14 @@ go build .
# Run each hive command in the background for each simulator and wait
echo "Building images"
# TODO: test code has been moved from https://github.com/ethereum/execution-spec-tests to https://github.com/ethereum/execution-specs we need to pin eels branch with `--sim.buildarg branch=<release-branch-name>` once we have the fusaka release tagged on the new repo
./hive -client reth --sim "ethereum/eels" --sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz -sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/eels/consume-engine" \
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
--sim.buildarg branch=forks/osaka \
--sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/eels/consume-rlp" \
--sim.buildarg fixtures=https://github.com/ethereum/execution-spec-tests/releases/download/v5.3.0/fixtures_develop.tar.gz \
--sim.buildarg branch=forks/osaka \
--sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/engine" -sim.timelimit 1s || true &
./hive -client reth --sim "devp2p" -sim.timelimit 1s || true &
./hive -client reth --sim "ethereum/rpc-compat" -sim.timelimit 1s || true &

View File

@@ -1,9 +1,6 @@
# tracked by https://github.com/paradigmxyz/reth/issues/13879
rpc-compat:
- debug_getRawBlock/get-invalid-number (reth)
- debug_getRawHeader/get-invalid-number (reth)
- debug_getRawReceipts/get-invalid-number (reth)
- debug_getRawReceipts/get-block-n (reth)
- debug_getRawTransaction/get-invalid-hash (reth)
- eth_getStorageAt/get-storage-invalid-key-too-large (reth)

View File

@@ -13,7 +13,13 @@ if [[ "${sim}" == *"eels"* ]]; then
fi
run_hive() {
hive --sim "${sim}" --sim.limit "${limit}" --sim.parallelism "${parallelism}" --client reth 2>&1 | tee /tmp/log || true
hive \
--sim "${sim}" \
--sim.limit "${limit}" \
--sim.limit.exact=false \
--sim.parallelism "${parallelism}" \
--client reth \
2>&1 | tee /tmp/log || true
}
check_log() {

View File

@@ -1,19 +1,25 @@
# Nightly regression benchmark.
# Scheduled regression benchmarks (nightly + hourly).
#
# Compares the previous nightly build against the current nightly build to
# detect performance regressions. Runs daily after docker.yml produces a new
# nightly image at 01:00 UTC.
# Two modes:
# nightly — Compares the previous nightly Docker build against the current one.
# Runs daily after docker.yml produces a new nightly image.
# hourly — Compares main HEAD against the last benchmarked commit to catch
# regressions quickly. Falls back to HEAD~1 on first run.
# Skips if no new commits or if a previous run is still in progress.
#
# State is persisted between runs via GitHub Actions cache: each successful
# run saves the feature commit SHA so the next run knows what to compare against.
on:
schedule:
- cron: "30 5 * * *" # 06:30 UTC daily
# Nightly: compares previous vs current nightly Docker build
- cron: "30 5 * * *"
# Hourly: compares main HEAD vs last benchmarked commit, skips if no new commits
- cron: "0 * * * *"
workflow_dispatch:
inputs:
force:
description: "Force run even if no new nightly (bypass skip logic)"
description: "Force run even if no new commit (bypass skip logic)"
required: false
default: false
type: boolean
@@ -22,6 +28,14 @@ on:
required: false
default: true
type: boolean
mode:
description: "Benchmark mode"
required: false
default: "nightly"
type: choice
options:
- nightly
- hourly
env:
CARGO_TERM_COLOR: always
@@ -35,44 +49,105 @@ permissions:
jobs:
# ---------------------------------------------------------------------------
# Job 1: Resolve nightly refs, check staleness, manage state
# Job 1: Resolve refs, check staleness, manage state
# ---------------------------------------------------------------------------
resolve-refs:
name: resolve-refs
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.mode.outputs.mode }}
baseline-ref: ${{ steps.refs.outputs.baseline-ref }}
feature-ref: ${{ steps.refs.outputs.feature-ref }}
should-skip: ${{ steps.refs.outputs.should-skip }}
is-stale: ${{ steps.refs.outputs.is-stale }}
stale-age-hours: ${{ steps.refs.outputs.stale-age-hours }}
nightly-created: ${{ steps.refs.outputs.nightly-created }}
long-running: ${{ steps.refs.outputs.long-running }}
steps:
- uses: actions/checkout@v6
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
fetch-depth: 2
- name: Restore nightly state
- name: Detect mode
id: mode
run: |
# Maps cron schedules to modes (must match the schedule entries above)
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
MODE="${{ inputs.mode || 'nightly' }}"
elif [ "${{ github.event.schedule }}" = "30 5 * * *" ]; then
MODE="nightly"
else
MODE="hourly"
fi
echo "mode=$MODE" >> "$GITHUB_OUTPUT"
echo "Detected mode: $MODE"
- name: Restore state cache
id: state-cache
uses: actions/cache/restore@v4
with:
path: .nightly-state
key: bench-scheduled-state-dummy
path: .${{ steps.mode.outputs.mode == 'hourly' && 'hourly' || 'nightly' }}-state
key: bench-${{ steps.mode.outputs.mode }}-state-dummy
restore-keys: |
bench-scheduled-state-
bench-${{ steps.mode.outputs.mode }}-state-
- name: Resolve nightly refs
- name: Resolve refs
id: refs
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_RUN_ID: ${{ github.run_id }}
run: |
FORCE="${{ inputs.force || 'false' }}"
.github/scripts/bench-scheduled-refs.sh "$FORCE"
MODE="${{ steps.mode.outputs.mode }}"
.github/scripts/bench-scheduled-refs.sh "$FORCE" "$MODE"
- name: Alert on long-running hourly
if: steps.mode.outputs.mode == 'hourly' && steps.refs.outputs.long-running == 'true'
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
SLACK_BENCH_CHANNEL: ${{ secrets.SLACK_BENCH_CHANNEL }}
with:
script: |
const token = process.env.SLACK_BENCH_BOT_TOKEN;
const channel = process.env.SLACK_BENCH_CHANNEL;
if (!token || !channel) return;
const repo = '${{ github.repository }}';
const runUrl = `${context.serverUrl}/${repo}/actions/runs/${context.runId}`;
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: ':warning: Hourly Bench: previous run still in progress', emoji: true },
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'A previous hourly benchmark run is still in progress. This invocation will be skipped.\nThis may indicate a long-running or stuck job.',
},
},
{
type: 'actions',
elements: [{
type: 'button',
text: { type: 'plain_text', text: 'View Run :github:', emoji: true },
url: runUrl,
action_id: 'ci_button',
}],
},
];
await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ channel, blocks, text: 'Hourly bench: previous run still in progress', unfurl_links: false }),
});
- name: Alert on stale nightly
if: steps.refs.outputs.is-stale == 'true'
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
uses: actions/github-script@v8
env:
SLACK_BENCH_BOT_TOKEN: ${{ secrets.SLACK_BENCH_BOT_TOKEN }}
@@ -146,7 +221,7 @@ jobs:
}
- name: Fail on stale nightly
if: steps.refs.outputs.is-stale == 'true'
if: steps.mode.outputs.mode == 'nightly' && steps.refs.outputs.is-stale == 'true'
run: |
echo "::error::Nightly build is stale (>24h old). Aborting."
exit 1
@@ -167,7 +242,8 @@ jobs:
SCHELK_MOUNT: /reth-bench
BENCH_WORK_DIR: ${{ github.workspace }}/bench-work
BENCH_PR: ""
BENCH_ACTOR: "nightly-regression"
BENCH_MODE: ${{ needs.resolve-refs.outputs.mode }}
BENCH_ACTOR: "${{ needs.resolve-refs.outputs.mode }}-regression"
BENCH_BLOCKS: "2000"
BENCH_WARMUP_BLOCKS: "500"
BENCH_SAMPLY: "false"
@@ -273,8 +349,8 @@ jobs:
run: |
BASELINE_SHORT=$(echo "$BASELINE_REF" | cut -c1-8)
FEATURE_SHORT=$(echo "$FEATURE_REF" | cut -c1-8)
echo "baseline-name=nightly-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
echo "feature-name=nightly-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
echo "baseline-name=${BENCH_MODE}-${BASELINE_SHORT}" >> "$GITHUB_OUTPUT"
echo "feature-name=${BENCH_MODE}-${FEATURE_SHORT}" >> "$GITHUB_OUTPUT"
echo "baseline-ref=$BASELINE_REF" >> "$GITHUB_OUTPUT"
echo "feature-ref=$FEATURE_REF" >> "$GITHUB_OUTPUT"
@@ -390,7 +466,7 @@ jobs:
- name: Start metrics proxy
run: |
BENCH_ID="nightly-${{ github.run_id }}"
BENCH_ID="${BENCH_MODE}-${{ github.run_id }}"
BENCH_REFERENCE_EPOCH=$(date +%s)
echo "BENCH_ID=${BENCH_ID}" >> "$GITHUB_ENV"
echo "BENCH_REFERENCE_EPOCH=${BENCH_REFERENCE_EPOCH}" >> "$GITHUB_ENV"
@@ -519,7 +595,7 @@ jobs:
python3 .github/scripts/bench-reth-summary.py $SUMMARY_ARGS
- name: Generate charts
if: success()
if: success() && env.BENCH_MODE != 'hourly'
env:
BASELINE_NAME: ${{ steps.refs.outputs.baseline-name }}
FEATURE_NAME: ${{ steps.refs.outputs.feature-name }}
@@ -545,7 +621,7 @@ jobs:
- name: Push charts
id: push-charts
if: success()
if: success() && env.BENCH_MODE != 'hourly'
run: |
RUN_ID=${{ github.run_id }}
CHART_DIR="nightly/${RUN_ID}"
@@ -591,7 +667,9 @@ jobs:
const featureLink = `[\`${summary.feature.name}\`](${commitUrl}/${summary.feature.ref})`;
const diffUrl = `https://github.com/${repo}/compare/${summary.baseline.ref}...${summary.feature.ref}`;
let md = `# ${emoji} Nightly Regression: ${label}\n\n`;
const mode = process.env.BENCH_MODE || 'nightly';
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
let md = `# ${emoji} ${modeLabel}: ${label}\n\n`;
md += `**Baseline:** ${baselineLink}\n`;
md += `**Feature:** ${featureLink} ([diff](${diffUrl}))\n`;
md += blocksLabel(summary).map(p => `**${p.key}:** ${p.value}`).join(' · ') + '\n\n';
@@ -671,9 +749,19 @@ jobs:
return;
}
// Only notify on significant changes (regression OR improvement)
// Filter notifications based on mode
const changes = summary.changes || {};
const mode = process.env.BENCH_MODE || 'nightly';
const hasRegression = Object.values(changes).some(c => c.sig === 'bad');
const hasSignificant = Object.values(changes).some(c => c.sig === 'good' || c.sig === 'bad');
// Hourly mode: only notify on regressions
if (mode === 'hourly' && !hasRegression) {
core.info('Hourly mode: no regression detected, skipping Slack notification');
return;
}
// Nightly mode: notify on any significant change (regression or improvement)
if (!hasSignificant) {
core.info('No significant changes detected, skipping nightly Slack notification');
return;
@@ -697,8 +785,9 @@ jobs:
function cell(text) { return { type: 'raw_text', text: String(text) || ' ' }; }
const modeLabel = mode === 'hourly' ? 'Hourly Regression' : 'Nightly Regression';
const sectionText = [
'*Nightly Regression*',
`*${modeLabel}*`,
'',
`*Baseline:* ${baselineLink}`,
`*Feature:* ${featureLink}`,
@@ -714,7 +803,7 @@ jobs:
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: `${headerEmoji} Nightly: ${label}`, emoji: true },
text: { type: 'plain_text', text: `${headerEmoji} ${modeLabel}: ${label}`, emoji: true },
},
{
type: 'section',
@@ -744,7 +833,7 @@ jobs:
},
];
const text = `Nightly Regression: ${summary.baseline.name} vs ${summary.feature.name}`;
const text = `${modeLabel}: ${summary.baseline.name} vs ${summary.feature.name}`;
const resp = await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
@@ -812,14 +901,17 @@ jobs:
const repo = `${context.repo.owner}/${context.repo.repo}`;
const jobUrl = process.env.BENCH_JOB_URL || `${context.serverUrl}/${repo}/actions/runs/${context.runId}`;
const mode = process.env.BENCH_MODE || 'nightly';
const modeLabel = mode === 'hourly' ? 'Hourly' : 'Nightly';
const blocks = [
{
type: 'header',
text: { type: 'plain_text', text: ':rotating_light: Nightly Bench Failed', emoji: true },
text: { type: 'plain_text', text: `:rotating_light: ${modeLabel} Bench Failed`, emoji: true },
},
{
type: 'section',
text: { type: 'mrkdwn', text: `*Nightly regression* failed while *${failedStep}*` },
text: { type: 'mrkdwn', text: `*${modeLabel} regression* failed while *${failedStep}*\ncc <@U09FARE0B9Q> <@U09FAL2UMLJ>` },
},
{
type: 'actions',
@@ -841,7 +933,7 @@ jobs:
body: JSON.stringify({
channel,
blocks,
text: `Nightly bench failed while ${failedStep}`,
text: `${modeLabel} bench failed while ${failedStep}`,
unfurl_links: false,
}),
});
@@ -862,11 +954,13 @@ jobs:
steps:
- name: Write state file
run: |
mkdir -p .nightly-state
echo "${{ needs.resolve-refs.outputs.feature-ref }}" > .nightly-state/last-feature-ref
MODE="${{ needs.resolve-refs.outputs.mode }}"
STATE_DIR=".${MODE}-state"
mkdir -p "$STATE_DIR"
echo "${{ needs.resolve-refs.outputs.feature-ref }}" > "$STATE_DIR/last-feature-ref"
- name: Save nightly state
- name: Save state
uses: actions/cache/save@v4
with:
path: .nightly-state
key: bench-scheduled-state-${{ needs.resolve-refs.outputs.feature-ref }}
path: .${{ needs.resolve-refs.outputs.mode }}-state
key: bench-${{ needs.resolve-refs.outputs.mode }}-state-${{ needs.resolve-refs.outputs.feature-ref }}

View File

@@ -12,10 +12,15 @@ on:
workflow_dispatch:
inputs:
blocks:
description: "Number of blocks to benchmark (or 'big' for big blocks mode)"
description: "Number of blocks to benchmark"
required: false
default: "500"
type: string
big_blocks:
description: "Use big blocks mode (pre-generated merged payloads with reth-bb)"
required: false
default: "false"
type: boolean
warmup:
description: "Number of warmup blocks"
required: false
@@ -152,7 +157,7 @@ jobs:
samply = '${{ github.event.inputs.samply }}' === 'true' ? 'true' : 'false';
var noSlack = '${{ github.event.inputs.no_slack }}' !== 'false' ? 'true' : 'false';
cores = '${{ github.event.inputs.cores }}' || '0';
bigBlocks = blocks === 'big' ? 'true' : 'false';
bigBlocks = '${{ github.event.inputs.big_blocks }}' === 'true' ? 'true' : 'false';
var rethNewPayload = '${{ github.event.inputs.reth_newPayload }}' !== 'false' ? 'true' : 'false';
var abba = '${{ github.event.inputs.abba }}' !== 'false' ? 'true' : 'false';
var otlp = '${{ github.event.inputs.otlp }}' !== 'false' ? 'true' : 'false';
@@ -178,14 +183,13 @@ jobs:
actor = context.payload.comment.user.login;
const body = context.payload.comment.body.trim();
const intArgs = new Set(['warmup', 'cores']);
const intOrKeywordArgs = new Map([['blocks', new Set(['big'])]]);
const intArgs = new Set(['warmup', 'cores', 'blocks']);
const refArgs = new Set(['baseline', 'feature']);
const boolArgs = new Set(['samply', 'no-slack']);
const boolArgs = new Set(['samply', 'no-slack', 'big-blocks']);
const boolDefaultTrue = new Set(['reth_newPayload', 'abba', 'otlp']);
const durationArgs = new Set(['wait-time']);
const stringArgs = new Set(['baseline-args', 'feature-args']);
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '', samply: 'false', 'no-slack': 'false', cores: '0', reth_newPayload: 'true', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
const defaults = { blocks: '500', warmup: '100', baseline: '', feature: '', samply: 'false', 'no-slack': 'false', 'big-blocks': 'false', cores: '0', reth_newPayload: 'true', abba: 'true', otlp: 'true', 'wait-time': '', 'baseline-args': '', 'feature-args': '' };
const unknown = [];
const invalid = [];
const args = body.replace(/^(?:@decofe|derek) bench\s*/, '');
@@ -230,15 +234,6 @@ jobs:
} else {
defaults[key] = value;
}
} else if (intOrKeywordArgs.has(key)) {
const keywords = intOrKeywordArgs.get(key);
if (keywords.has(value)) {
defaults[key] = value;
} else if (/^\d+$/.test(value)) {
defaults[key] = value;
} else {
invalid.push(`\`${key}=${value}\` (must be a positive integer or one of: ${[...keywords].join(', ')})`);
}
} else if (refArgs.has(key)) {
if (!value) {
invalid.push(`\`${key}=\` (must be a git ref)`);
@@ -255,7 +250,7 @@ jobs:
if (unknown.length) errors.push(`Unknown argument(s): \`${unknown.join('`, `')}\``);
if (invalid.length) errors.push(`Invalid value(s): ${invalid.join(', ')}`);
if (errors.length) {
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`@decofe bench [blocks=N|big] [warmup=N] [baseline=REF] [feature=REF] [samply] [no-slack] [cores=N] [reth_newPayload=true|false] [abba=true|false] [otlp=true|false] [wait-time=DURATION] [baseline-args="..."] [feature-args="..."]\``;
const msg = `❌ **Invalid bench command**\n\n${errors.join('\n')}\n\n**Usage:** \`@decofe bench [blocks=N] [big-blocks] [warmup=N] [baseline=REF] [feature=REF] [samply] [no-slack] [cores=N] [reth_newPayload=true|false] [abba=true|false] [otlp=true|false] [wait-time=DURATION] [baseline-args="..."] [feature-args="..."]\``;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -272,7 +267,7 @@ jobs:
samply = defaults.samply;
var noSlack = defaults['no-slack'];
cores = defaults.cores;
bigBlocks = blocks === 'big' ? 'true' : 'false';
bigBlocks = defaults['big-blocks'];
var rethNewPayload = defaults.reth_newPayload;
var abba = defaults.abba;
var otlp = defaults.otlp;
@@ -508,6 +503,7 @@ jobs:
BENCH_OTLP: ${{ needs.reth-bench-ack.outputs.otlp }}
BENCH_COMMENT_ID: ${{ needs.reth-bench-ack.outputs.comment-id }}
BENCH_NO_SLACK: ${{ needs.reth-bench-ack.outputs.no-slack }}
BENCH_NODE_BIN: ${{ needs.reth-bench-ack.outputs.big-blocks == 'true' && 'reth-bb' || 'reth' }}
BENCH_METRICS_ADDR: "127.0.0.1:9100"
BENCH_OTLP_TRACES_ENDPOINT: ${{ needs.reth-bench-ack.outputs.otlp != 'false' && secrets.BENCH_OTLP_TRACES_ENDPOINT || '' }}
BENCH_OTLP_LOGS_ENDPOINT: ${{ needs.reth-bench-ack.outputs.otlp != 'false' && secrets.BENCH_OTLP_LOGS_ENDPOINT || '' }}
@@ -723,6 +719,39 @@ jobs:
core.setOutput('feature-ref', featureRef);
core.setOutput('feature-name', featureName);
- name: Check big-blocks freshness
if: env.BENCH_BIG_BLOCKS == 'true'
id: big-blocks-check
run: |
set -euo pipefail
MC="mc --config-dir /home/ubuntu/.mc"
MANIFEST="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.json"
HASH_FILE="$HOME/.reth-bench-big-blocks-hash"
echo "Fetching big-blocks manifest from $MANIFEST..."
BB_MANIFEST=$($MC cat "$MANIFEST" 2>/dev/null) || {
echo "::error::Failed to fetch big-blocks manifest from $MANIFEST"
exit 1
}
BASE_SNAPSHOT=$(echo "$BB_MANIFEST" | jq -r '.base_snapshot // empty')
if [ -z "$BASE_SNAPSHOT" ]; then
echo "::error::Big-blocks manifest missing base_snapshot field"
exit 1
fi
echo "Big-blocks base snapshot: $BASE_SNAPSHOT"
echo "BENCH_SNAPSHOT_NAME=${BASE_SNAPSHOT}" >> "$GITHUB_ENV"
REMOTE_HASH=$(echo "$BB_MANIFEST" | sha256sum | awk '{print $1}')
LOCAL_HASH=""
[ -f "$HASH_FILE" ] && LOCAL_HASH=$(cat "$HASH_FILE")
if [ "$REMOTE_HASH" = "$LOCAL_HASH" ]; then
echo "Big blocks up-to-date (hash: ${REMOTE_HASH:0:16}…)"
echo "needed=false" >> "$GITHUB_OUTPUT"
else
echo "Big blocks need update (local: ${LOCAL_HASH:+${LOCAL_HASH:0:16}…}${LOCAL_HASH:-<none>}, remote: ${REMOTE_HASH:0:16}…)"
echo "needed=true" >> "$GITHUB_OUTPUT"
echo "remote-hash=${REMOTE_HASH}" >> "$GITHUB_OUTPUT"
fi
- name: Check if snapshot needs update
id: snapshot-check
run: |
@@ -792,7 +821,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BENCH_REPO: ${{ github.repository }}
BENCH_RETH_BINARY: ${{ github.workspace }}/../reth-feature/target/profiling/reth
BENCH_RETH_BINARY: ${{ github.workspace }}/../reth-feature/target/profiling/${{ needs.reth-bench-ack.outputs.big-blocks == 'true' && 'reth-bb' || 'reth' }}
run: .github/scripts/bench-reth-snapshot.sh
# System tuning for reproducible benchmarks
@@ -850,13 +879,31 @@ jobs:
if: env.BENCH_BIG_BLOCKS == 'true'
run: |
set -euo pipefail
BIG_BLOCKS_DIR="$HOME/.reth-bench-big-blocks"
echo "BENCH_BIG_BLOCKS_DIR=${BIG_BLOCKS_DIR}" >> "$GITHUB_ENV"
if [ "${{ steps.big-blocks-check.outputs.needed }}" = "false" ]; then
echo "Big blocks cached at $BIG_BLOCKS_DIR, skipping download"
echo "Payload files: $(find "$BIG_BLOCKS_DIR/payloads" -name '*.json' | wc -l)"
exit 0
fi
MC="mc --config-dir /home/ubuntu/.mc"
BUCKET="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.tar.zst"
BIG_BLOCKS_DIR="${BENCH_WORK_DIR}/big-blocks"
MANIFEST="minio/reth-snapshots/reth-1-minimal-stable-big-blocks.json"
rm -rf "$BIG_BLOCKS_DIR"; mkdir -p "$BIG_BLOCKS_DIR"
echo "Downloading big blocks from $BUCKET..."
$MC cat "$BUCKET" | pzstd -d -p 6 | tar -xf - -C "$BIG_BLOCKS_DIR"
# Download and parse manifest
echo "Downloading manifest from $MANIFEST..."
$MC cat "$MANIFEST" > "$BIG_BLOCKS_DIR/manifest.json"
UPLOAD_PATH=$(jq -r '.upload_path' "$BIG_BLOCKS_DIR/manifest.json")
COUNT=$(jq -r '.count' "$BIG_BLOCKS_DIR/manifest.json")
TARGET_GAS=$(jq -r '.target_gas' "$BIG_BLOCKS_DIR/manifest.json")
echo "Manifest: count=$COUNT, target_gas=$TARGET_GAS, archive=$UPLOAD_PATH"
# Download and extract archive
ARCHIVE="minio/$UPLOAD_PATH"
echo "Downloading big blocks from $ARCHIVE..."
$MC cat "$ARCHIVE" | pzstd -d -p 6 | tar -xf - -C "$BIG_BLOCKS_DIR"
echo "Big blocks downloaded to $BIG_BLOCKS_DIR"
# Verify expected directory structure
if [ ! -d "$BIG_BLOCKS_DIR/payloads" ]; then
echo "::error::Big blocks archive missing expected payloads/ directory"
@@ -864,6 +911,8 @@ jobs:
exit 1
fi
echo "Payload files: $(find "$BIG_BLOCKS_DIR/payloads" -name '*.json' | wc -l)"
# Save manifest hash for freshness check on next run
echo "${{ steps.big-blocks-check.outputs.remote-hash }}" > "$HOME/.reth-bench-big-blocks-hash"
- name: Start metrics proxy
run: |
@@ -905,7 +954,7 @@ jobs:
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"baseline-1","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-1"
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-1"
- name: "Run benchmark: feature (1/2)"
id: run-feature-1
@@ -916,7 +965,7 @@ jobs:
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"feature-1","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-1"
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-1"
- name: "Run benchmark: feature (2/2)"
if: env.BENCH_ABBA != 'false'
@@ -928,7 +977,7 @@ jobs:
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"feature-2","run_type":"feature","git_ref":"${FEATURE_REF}","bench_sha":"${FEATURE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"$(date +%s)","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh feature ../reth-feature/target/profiling/reth "$BENCH_WORK_DIR/feature-2"
taskset -c 0 .github/scripts/bench-reth-run.sh feature "../reth-feature/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/feature-2"
- name: "Run benchmark: baseline (2/2)"
if: env.BENCH_ABBA != 'false'
@@ -942,7 +991,7 @@ jobs:
cat > "$BENCH_LABELS_FILE" <<LABELS
{"benchmark_run":"baseline-2","run_type":"baseline","git_ref":"${BASELINE_REF}","bench_sha":"${BASELINE_REF}","benchmark_id":"${BENCH_ID}","run_start_epoch":"${LAST_RUN_START}","reference_epoch":"${BENCH_REFERENCE_EPOCH}"}
LABELS
taskset -c 0 .github/scripts/bench-reth-run.sh baseline ../reth-baseline/target/profiling/reth "$BENCH_WORK_DIR/baseline-2"
taskset -c 0 .github/scripts/bench-reth-run.sh baseline "../reth-baseline/target/profiling/${BENCH_NODE_BIN}" "$BENCH_WORK_DIR/baseline-2"
- name: Stop metrics proxy & generate Grafana URL
id: metrics

View File

@@ -183,6 +183,8 @@ jobs:
*Run:* <https://github.com/paradigmxyz/reth/actions/runs/${{ github.run_id }}|View logs>
*Action required:* Re-run the workflow or investigate the build failure.
<@U0AAA8F0JEM> investigate and re-run if flaky
SLACK_FOOTER: "paradigmxyz/reth · docker.yml"
MSG_MINIMAL: true
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

View File

@@ -237,31 +237,6 @@ jobs:
- name: Ensure no arbitrary or proptest dependency on default build
run: cargo tree --package reth -e=features,no-dev | grep -Eq "arbitrary|proptest" && exit 1 || exit 0
# Checks that selected crates can compile with power set of features
features:
name: features
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: rui314/setup-mold@v1
- uses: dtolnay/rust-toolchain@clippy
- uses: mozilla-actions/sccache-action@v0.0.9
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: cargo install cargo-hack
uses: taiki-e/install-action@cargo-hack
- run: |
cargo hack check \
--package reth-codecs \
--package reth-primitives-traits \
--package reth-primitives \
--feature-powerset \
--depth 2
env:
RUSTFLAGS: -D warnings
# Check crates correctly propagate features
feature-propagation:
runs-on: ${{ github.repository == 'paradigmxyz/reth' && 'depot-ubuntu-latest' || 'ubuntu-latest' }}
@@ -297,7 +272,6 @@ jobs:
- typos
- grafana
- no-test-deps
- features
- feature-propagation
- deny
timeout-minutes: 30

549
AGENTS.md Normal file
View File

@@ -0,0 +1,549 @@
# Reth Development Guide for AI Agents
This guide provides comprehensive instructions for AI agents working on the Reth codebase. It covers the architecture, development workflows, and critical guidelines for effective contributions.
## Project Overview
Reth is a high-performance Ethereum execution client written in Rust, focusing on modularity, performance, and contributor-friendliness. The codebase is organized into well-defined crates with clear boundaries and responsibilities.
## Architecture Overview
### Core Components
1. **Consensus (`crates/consensus/`)**: Validates blocks according to Ethereum consensus rules
2. **Storage (`crates/storage/`)**: Hybrid database using MDBX + static files for optimal performance
3. **Networking (`crates/net/`)**: P2P networking stack with discovery, sync, and transaction propagation
4. **RPC (`crates/rpc/`)**: JSON-RPC server supporting all standard Ethereum APIs
5. **Execution (`crates/evm/`, `crates/ethereum/`)**: Transaction execution and state transitions
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
9. **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
### Key Design Principles
- **Modularity**: Each crate can be used as a standalone library
- **Performance**: Extensive use of parallelism, memory-mapped I/O, and optimized data structures
- **Extensibility**: Traits and generic types allow for different chain implementations
- **Type Safety**: Strong typing throughout with minimal use of dynamic dispatch
## Development Workflow
### Code Style and Standards
1. **Formatting**: Always use nightly rustfmt
```bash
cargo +nightly fmt --all
```
2. **Linting**: Run clippy with all features
```bash
cargo +nightly clippy --workspace --lib --examples --tests --benches --all-features
```
3. **Testing**: Use nextest for faster test execution
```bash
cargo nextest run --workspace
```
### Common Contribution Types
Based on actual recent PRs, here are typical contribution patterns:
#### 1. Small Bug Fixes (1-10 lines)
Real example: Fixing beacon block root handling ([#16767](https://github.com/paradigmxyz/reth/pull/16767))
```rust
// Changed a single line to fix logic error
- parent_beacon_block_root: parent.parent_beacon_block_root(),
+ parent_beacon_block_root: parent.parent_beacon_block_root().map(|_| B256::ZERO),
```
#### 2. Integration with Upstream Changes
Real example: Integrating revm updates ([#16752](https://github.com/paradigmxyz/reth/pull/16752))
```rust
// Update code to use new APIs from dependencies
- if self.fork_tracker.is_shanghai_activated() {
- if let Err(err) = transaction.ensure_max_init_code_size(MAX_INIT_CODE_BYTE_SIZE) {
+ if let Some(init_code_size_limit) = self.fork_tracker.max_initcode_size() {
+ if let Err(err) = transaction.ensure_max_init_code_size(init_code_size_limit) {
```
#### 3. Adding Comprehensive Tests
Real example: ETH69 protocol tests ([#16759](https://github.com/paradigmxyz/reth/pull/16759))
```rust
#[tokio::test(flavor = "multi_thread")]
async fn test_eth69_peers_can_connect() {
// Create test network with specific protocol versions
let p0 = PeerConfig::with_protocols(NoopProvider::default(), Some(EthVersion::Eth69.into()));
// Test connection and version negotiation
}
```
#### 4. Making Components Generic
Real example: Making EthEvmConfig generic over chainspec ([#16758](https://github.com/paradigmxyz/reth/pull/16758))
```rust
// Before: Hardcoded to ChainSpec
- pub struct EthEvmConfig<EvmFactory = EthEvmFactory> {
- pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<ChainSpec>, EvmFactory>,
// After: Generic over any chain spec type
+ pub struct EthEvmConfig<C = ChainSpec, EvmFactory = EthEvmFactory>
+ where
+ C: EthereumHardforks,
+ {
+ pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<C>, EvmFactory>,
```
#### 5. Resource Management Improvements
Real example: ETL directory cleanup ([#16770](https://github.com/paradigmxyz/reth/pull/16770))
```rust
// Add cleanup logic on startup
+ if let Err(err) = fs::remove_dir_all(&etl_path) {
+ warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
+ }
```
#### 6. Feature Additions
Real example: Sharded mempool support ([#16756](https://github.com/paradigmxyz/reth/pull/16756))
```rust
// Add new filtering policies for transaction announcements
pub struct ShardedMempoolAnnouncementFilter<T> {
pub inner: T,
pub shard_bits: u8,
pub node_id: Option<B256>,
}
```
### Testing Guidelines
1. **Unit Tests**: Test individual functions and components
2. **Integration Tests**: Test interactions between components
3. **Benchmarks**: For performance-critical code
4. **Fuzz Tests**: For parsing and serialization code
5. **Property Tests**: For checking component correctness on a wide variety of inputs
Example test structure:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_behavior() {
// Arrange
let component = Component::new();
// Act
let result = component.operation();
// Assert
assert_eq!(result, expected);
}
}
```
### Performance Considerations
1. **Avoid Allocations in Hot Paths**: Use references and borrowing
2. **Parallel Processing**: Use rayon for CPU-bound parallel work
3. **Async/Await**: Use tokio for I/O-bound operations
4. **File Operations**: Use `reth_fs_util` instead of `std::fs` for better error handling
### Common Pitfalls
1. **Don't Block Async Tasks**: Use `spawn_blocking` for CPU-intensive work or work with lots of blocking I/O
2. **Handle Errors Properly**: Use `?` operator and proper error types
### What to Avoid
Based on PR patterns, avoid:
1. **Large, sweeping changes**: Keep PRs focused and reviewable
2. **Mixing unrelated changes**: One logical change per PR
3. **Ignoring CI failures**: All checks must pass
4. **Incomplete implementations**: Finish features before submitting
5. **Modifying libmdbx sources**: Never modify files in `crates/storage/libmdbx-rs/mdbx-sys/libmdbx/` - this is vendored third-party code
### CI Requirements
Before submitting changes, ensure:
1. **Format Check**: `cargo +nightly fmt --all --check`
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. **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
* ... and so on, check the available labels for more options.
* if being tasked to open a pr, ensure that all changes are properly formatted: `cargo +nightly fmt --all`
If changes in reth include changes to dependencies, run commands `zepter` and `make lint-toml` before finalizing the pr. Assume `zepter` binary is installed.
### Debugging Tips
1. **Logging**: Use `tracing` crate with appropriate levels
```rust
tracing::debug!(target: "reth::component", ?value, "description");
```
2. **Metrics**: Add metrics for monitoring
```rust
metrics::counter!("reth_component_operations").increment(1);
```
3. **Test Isolation**: Use separate test databases/directories
### Finding Where to Contribute
1. **Check Issues**: Look for issues labeled `good-first-issue` or `help-wanted`
2. **Review TODOs**: Search for `TODO` comments in the codebase
3. **Improve Tests**: Areas with low test coverage are good targets
4. **Documentation**: Improve code comments and documentation
5. **Performance**: Profile and optimize hot paths (with benchmarks)
### Common PR Patterns
#### Small, Focused Changes
Most PRs change only 1-5 files. Examples:
- Single-line bug fixes
- Adding a missing trait implementation
- Updating error messages
- Adding test cases for edge conditions
#### Integration Work
When dependencies update (especially revm), code needs updating:
- Check for breaking API changes
- Update to use new features (like EIP implementations)
- Ensure compatibility with new versions
#### Test Improvements
Tests often need expansion for:
- New protocol versions (ETH68, ETH69)
- Edge cases in state transitions
- Network behavior under specific conditions
- Concurrent operations
#### Making Code More Generic
Common refactoring pattern:
- Replace concrete types with generics
- Add trait bounds for flexibility
- Enable reuse across different chain types
#### When to Comment
Write comments that remain valuable after the PR is merged. Future readers won't have PR context - they only see the current code.
##### ✅ DO: Add Value
**Explain WHY and non-obvious behavior:**
```rust
// Process must handle allocations atomically to prevent race conditions
// between dealloc on drop and concurrent limit checks
unsafe impl GlobalAlloc for LimitedAllocator { ... }
// Binary search requires sorted input. Panics on unsorted slices.
fn find_index(items: &[Item], target: &Item) -> Option<usize>
// Timeout set to 5s to match EVM block processing limits
const TRACER_TIMEOUT: Duration = Duration::from_secs(5);
```
**Document constraints and assumptions:**
```rust
/// Returns heap size estimate.
///
/// Note: May undercount shared references (Rc/Arc). For precise
/// accounting, combine with an allocator-based approach.
fn deep_size_of(&self) -> usize
```
**Explain complex logic:**
```rust
// We reset limits at task start because tokio reuses threads in
// spawn_blocking pool. Without reset, second task inherits first
// task's allocation count and immediately hits limit.
THREAD_ALLOCATED.with(|allocated| allocated.set(0));
```
##### ❌ DON'T: Describe Changes
```rust
// ❌ BAD - Describes the change, not the code
// Changed from Vec to HashMap for O(1) lookups
// ✅ GOOD - Explains the decision
// HashMap provides O(1) symbol lookups during trace replay
```
```rust
// ❌ BAD - PR-specific context
// Fix for issue #234 where memory wasn't freed
// ✅ GOOD - Documents the actual behavior
// Explicitly drop allocations before limit check to ensure
// accurate accounting
```
```rust
// ❌ BAD - States the obvious
// Increment counter
counter += 1;
// ✅ GOOD - Explains non-obvious purpose
// Track allocations across all threads for global limit enforcement
GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
```
✅ **Comment when:**
- Non-obvious behavior or edge cases
- Performance trade-offs
- Safety requirements (unsafe blocks must always be documented)
- Limitations or gotchas
- Why simpler alternatives don't work
❌ **Don't comment when:**
- Code is self-explanatory
- Just restating the code in English
- Describing what changed in this PR
##### The Test: "Will this make sense in 6 months?"
Before adding a comment, ask: Would someone reading just the current code (no PR, no history) find this helpful?
#### Rust Style Guides
##### Type Ordering in Files
When defining structs, traits, and functions in a file, follow this ordering convention. The file's primary type (matching the file name) comes first, followed by supporting public types, then private types and helpers.
```rust
use ...;
/// The primary type of this file (matches filename).
pub struct PayloadProcessor { ... }
impl PayloadProcessor { ... }
// Followed by public auxiliary types that support the primary type
/// Configuration for the processor.
pub struct PayloadProcessorConfig { ... }
/// Result type returned by processor operations.
pub struct ProcessorResult { ... }
// Followed by public traits related to the primary type
pub trait ProcessorExt { ... }
// Followed by private helper types
struct InternalState { ... }
// Followed by private helper functions
fn validate_input() { ... }
```
❌ **Bad**: Adding new traits and auxiliary types **above** the file's primary type (see [#22133](https://github.com/paradigmxyz/reth/pull/22133)):
```rust
use ...;
// ❌ BAD - new auxiliary struct added before the file's main type
pub struct CacheWaitDurations { ... }
// ❌ BAD - new trait added before the file's main type
pub trait WaitForCaches { ... }
// The file's primary type is buried below unrelated additions
pub struct PayloadProcessor { ... }
```
✅ **Good**: New types go **after** the primary type:
```rust
use ...;
// ✅ The file's primary type stays at the top
pub struct PayloadProcessor { ... }
impl PayloadProcessor { ... }
// ✅ Auxiliary types follow the primary type
pub struct CacheWaitDurations { ... }
pub trait WaitForCaches { ... }
impl WaitForCaches for PayloadProcessor { ... }
```
### Example Contribution Workflow
Let's say you want to fix a bug where external IP resolution fails on startup:
1. **Create a branch**:
```bash
git checkout -b fix-external-ip-resolution
```
2. **Find the relevant code**:
```bash
# Search for IP resolution code
rg "external.*ip" --type rust
```
3. **Reason about the problem, when the problem is identified, make the fix**:
```rust
// In crates/net/discv4/src/lib.rs
pub fn resolve_external_ip() -> Option<IpAddr> {
// Add fallback mechanism
nat::external_ip()
.or_else(|| nat::external_ip_from_stun())
.or_else(|| Some(DEFAULT_IP))
}
```
4. **Add a test**:
```rust
#[test]
fn test_external_ip_fallback() {
// Test that resolution has proper fallbacks
}
```
5. **Run checks** (IMPORTANT!):
```bash
cargo +nightly fmt --all
cargo clippy --workspace --all-features # Make sure WHOLE WORKSPACE compiles!
cargo nextest run -p reth-discv4
```
6. **Commit with clear message**:
```bash
git commit -m "fix: add fallback for external IP resolution
Previously, node startup could fail if external IP resolution
failed. This adds fallback mechanisms to ensure the node can
always start with a reasonable default."
```
## Quick Reference
### Essential Commands
```bash
# Format code
cargo +nightly fmt --all
# Run lints
cargo +nightly clippy --workspace --all-features
# Run tests
cargo nextest run --workspace
# Run specific benchmark
cargo bench --bench bench_name
# Build optimized binary
cargo build --release
# Check compilation for all features
cargo check --workspace --all-features
# Check documentation
cargo docs --document-private-items
# Regenerate CLI reference docs (after CLI changes)
make update-book-cli
```

549
CLAUDE.md
View File

@@ -1,549 +0,0 @@
# Reth Development Guide for AI Agents
This guide provides comprehensive instructions for AI agents working on the Reth codebase. It covers the architecture, development workflows, and critical guidelines for effective contributions.
## Project Overview
Reth is a high-performance Ethereum execution client written in Rust, focusing on modularity, performance, and contributor-friendliness. The codebase is organized into well-defined crates with clear boundaries and responsibilities.
## Architecture Overview
### Core Components
1. **Consensus (`crates/consensus/`)**: Validates blocks according to Ethereum consensus rules
2. **Storage (`crates/storage/`)**: Hybrid database using MDBX + static files for optimal performance
3. **Networking (`crates/net/`)**: P2P networking stack with discovery, sync, and transaction propagation
4. **RPC (`crates/rpc/`)**: JSON-RPC server supporting all standard Ethereum APIs
5. **Execution (`crates/evm/`, `crates/ethereum/`)**: Transaction execution and state transitions
6. **Pipeline (`crates/stages/`)**: Staged sync architecture for blockchain synchronization
7. **Trie (`crates/trie/`)**: Merkle Patricia Trie implementation with parallel state root computation
8. **Node Builder (`crates/node/`)**: High-level node orchestration and configuration
9. **The Consensus Engine (`crates/engine/`)**: Handles processing blocks received from the consensus layer with the Engine API (newPayload, forkchoiceUpdated)
### Key Design Principles
- **Modularity**: Each crate can be used as a standalone library
- **Performance**: Extensive use of parallelism, memory-mapped I/O, and optimized data structures
- **Extensibility**: Traits and generic types allow for different chain implementations
- **Type Safety**: Strong typing throughout with minimal use of dynamic dispatch
## Development Workflow
### Code Style and Standards
1. **Formatting**: Always use nightly rustfmt
```bash
cargo +nightly fmt --all
```
2. **Linting**: Run clippy with all features
```bash
cargo +nightly clippy --workspace --lib --examples --tests --benches --all-features
```
3. **Testing**: Use nextest for faster test execution
```bash
cargo nextest run --workspace
```
### Common Contribution Types
Based on actual recent PRs, here are typical contribution patterns:
#### 1. Small Bug Fixes (1-10 lines)
Real example: Fixing beacon block root handling ([#16767](https://github.com/paradigmxyz/reth/pull/16767))
```rust
// Changed a single line to fix logic error
- parent_beacon_block_root: parent.parent_beacon_block_root(),
+ parent_beacon_block_root: parent.parent_beacon_block_root().map(|_| B256::ZERO),
```
#### 2. Integration with Upstream Changes
Real example: Integrating revm updates ([#16752](https://github.com/paradigmxyz/reth/pull/16752))
```rust
// Update code to use new APIs from dependencies
- if self.fork_tracker.is_shanghai_activated() {
- if let Err(err) = transaction.ensure_max_init_code_size(MAX_INIT_CODE_BYTE_SIZE) {
+ if let Some(init_code_size_limit) = self.fork_tracker.max_initcode_size() {
+ if let Err(err) = transaction.ensure_max_init_code_size(init_code_size_limit) {
```
#### 3. Adding Comprehensive Tests
Real example: ETH69 protocol tests ([#16759](https://github.com/paradigmxyz/reth/pull/16759))
```rust
#[tokio::test(flavor = "multi_thread")]
async fn test_eth69_peers_can_connect() {
// Create test network with specific protocol versions
let p0 = PeerConfig::with_protocols(NoopProvider::default(), Some(EthVersion::Eth69.into()));
// Test connection and version negotiation
}
```
#### 4. Making Components Generic
Real example: Making EthEvmConfig generic over chainspec ([#16758](https://github.com/paradigmxyz/reth/pull/16758))
```rust
// Before: Hardcoded to ChainSpec
- pub struct EthEvmConfig<EvmFactory = EthEvmFactory> {
- pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<ChainSpec>, EvmFactory>,
// After: Generic over any chain spec type
+ pub struct EthEvmConfig<C = ChainSpec, EvmFactory = EthEvmFactory>
+ where
+ C: EthereumHardforks,
+ {
+ pub executor_factory: EthBlockExecutorFactory<RethReceiptBuilder, Arc<C>, EvmFactory>,
```
#### 5. Resource Management Improvements
Real example: ETL directory cleanup ([#16770](https://github.com/paradigmxyz/reth/pull/16770))
```rust
// Add cleanup logic on startup
+ if let Err(err) = fs::remove_dir_all(&etl_path) {
+ warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
+ }
```
#### 6. Feature Additions
Real example: Sharded mempool support ([#16756](https://github.com/paradigmxyz/reth/pull/16756))
```rust
// Add new filtering policies for transaction announcements
pub struct ShardedMempoolAnnouncementFilter<T> {
pub inner: T,
pub shard_bits: u8,
pub node_id: Option<B256>,
}
```
### Testing Guidelines
1. **Unit Tests**: Test individual functions and components
2. **Integration Tests**: Test interactions between components
3. **Benchmarks**: For performance-critical code
4. **Fuzz Tests**: For parsing and serialization code
5. **Property Tests**: For checking component correctness on a wide variety of inputs
Example test structure:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_behavior() {
// Arrange
let component = Component::new();
// Act
let result = component.operation();
// Assert
assert_eq!(result, expected);
}
}
```
### Performance Considerations
1. **Avoid Allocations in Hot Paths**: Use references and borrowing
2. **Parallel Processing**: Use rayon for CPU-bound parallel work
3. **Async/Await**: Use tokio for I/O-bound operations
4. **File Operations**: Use `reth_fs_util` instead of `std::fs` for better error handling
### Common Pitfalls
1. **Don't Block Async Tasks**: Use `spawn_blocking` for CPU-intensive work or work with lots of blocking I/O
2. **Handle Errors Properly**: Use `?` operator and proper error types
### What to Avoid
Based on PR patterns, avoid:
1. **Large, sweeping changes**: Keep PRs focused and reviewable
2. **Mixing unrelated changes**: One logical change per PR
3. **Ignoring CI failures**: All checks must pass
4. **Incomplete implementations**: Finish features before submitting
5. **Modifying libmdbx sources**: Never modify files in `crates/storage/libmdbx-rs/mdbx-sys/libmdbx/` - this is vendored third-party code
### CI Requirements
Before submitting changes, ensure:
1. **Format Check**: `cargo +nightly fmt --all --check`
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. **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
* ... and so on, check the available labels for more options.
* if being tasked to open a pr, ensure that all changes are properly formatted: `cargo +nightly fmt --all`
If changes in reth include changes to dependencies, run commands `zepter` and `make lint-toml` before finalizing the pr. Assume `zepter` binary is installed.
### Debugging Tips
1. **Logging**: Use `tracing` crate with appropriate levels
```rust
tracing::debug!(target: "reth::component", ?value, "description");
```
2. **Metrics**: Add metrics for monitoring
```rust
metrics::counter!("reth_component_operations").increment(1);
```
3. **Test Isolation**: Use separate test databases/directories
### Finding Where to Contribute
1. **Check Issues**: Look for issues labeled `good-first-issue` or `help-wanted`
2. **Review TODOs**: Search for `TODO` comments in the codebase
3. **Improve Tests**: Areas with low test coverage are good targets
4. **Documentation**: Improve code comments and documentation
5. **Performance**: Profile and optimize hot paths (with benchmarks)
### Common PR Patterns
#### Small, Focused Changes
Most PRs change only 1-5 files. Examples:
- Single-line bug fixes
- Adding a missing trait implementation
- Updating error messages
- Adding test cases for edge conditions
#### Integration Work
When dependencies update (especially revm), code needs updating:
- Check for breaking API changes
- Update to use new features (like EIP implementations)
- Ensure compatibility with new versions
#### Test Improvements
Tests often need expansion for:
- New protocol versions (ETH68, ETH69)
- Edge cases in state transitions
- Network behavior under specific conditions
- Concurrent operations
#### Making Code More Generic
Common refactoring pattern:
- Replace concrete types with generics
- Add trait bounds for flexibility
- Enable reuse across different chain types
#### When to Comment
Write comments that remain valuable after the PR is merged. Future readers won't have PR context - they only see the current code.
##### ✅ DO: Add Value
**Explain WHY and non-obvious behavior:**
```rust
// Process must handle allocations atomically to prevent race conditions
// between dealloc on drop and concurrent limit checks
unsafe impl GlobalAlloc for LimitedAllocator { ... }
// Binary search requires sorted input. Panics on unsorted slices.
fn find_index(items: &[Item], target: &Item) -> Option<usize>
// Timeout set to 5s to match EVM block processing limits
const TRACER_TIMEOUT: Duration = Duration::from_secs(5);
```
**Document constraints and assumptions:**
```rust
/// Returns heap size estimate.
///
/// Note: May undercount shared references (Rc/Arc). For precise
/// accounting, combine with an allocator-based approach.
fn deep_size_of(&self) -> usize
```
**Explain complex logic:**
```rust
// We reset limits at task start because tokio reuses threads in
// spawn_blocking pool. Without reset, second task inherits first
// task's allocation count and immediately hits limit.
THREAD_ALLOCATED.with(|allocated| allocated.set(0));
```
##### ❌ DON'T: Describe Changes
```rust
// ❌ BAD - Describes the change, not the code
// Changed from Vec to HashMap for O(1) lookups
// ✅ GOOD - Explains the decision
// HashMap provides O(1) symbol lookups during trace replay
```
```rust
// ❌ BAD - PR-specific context
// Fix for issue #234 where memory wasn't freed
// ✅ GOOD - Documents the actual behavior
// Explicitly drop allocations before limit check to ensure
// accurate accounting
```
```rust
// ❌ BAD - States the obvious
// Increment counter
counter += 1;
// ✅ GOOD - Explains non-obvious purpose
// Track allocations across all threads for global limit enforcement
GLOBAL_COUNTER.fetch_add(1, Ordering::SeqCst);
```
✅ **Comment when:**
- Non-obvious behavior or edge cases
- Performance trade-offs
- Safety requirements (unsafe blocks must always be documented)
- Limitations or gotchas
- Why simpler alternatives don't work
❌ **Don't comment when:**
- Code is self-explanatory
- Just restating the code in English
- Describing what changed in this PR
##### The Test: "Will this make sense in 6 months?"
Before adding a comment, ask: Would someone reading just the current code (no PR, no history) find this helpful?
#### Rust Style Guides
##### Type Ordering in Files
When defining structs, traits, and functions in a file, follow this ordering convention. The file's primary type (matching the file name) comes first, followed by supporting public types, then private types and helpers.
```rust
use ...;
/// The primary type of this file (matches filename).
pub struct PayloadProcessor { ... }
impl PayloadProcessor { ... }
// Followed by public auxiliary types that support the primary type
/// Configuration for the processor.
pub struct PayloadProcessorConfig { ... }
/// Result type returned by processor operations.
pub struct ProcessorResult { ... }
// Followed by public traits related to the primary type
pub trait ProcessorExt { ... }
// Followed by private helper types
struct InternalState { ... }
// Followed by private helper functions
fn validate_input() { ... }
```
❌ **Bad**: Adding new traits and auxiliary types **above** the file's primary type (see [#22133](https://github.com/paradigmxyz/reth/pull/22133)):
```rust
use ...;
// ❌ BAD - new auxiliary struct added before the file's main type
pub struct CacheWaitDurations { ... }
// ❌ BAD - new trait added before the file's main type
pub trait WaitForCaches { ... }
// The file's primary type is buried below unrelated additions
pub struct PayloadProcessor { ... }
```
✅ **Good**: New types go **after** the primary type:
```rust
use ...;
// ✅ The file's primary type stays at the top
pub struct PayloadProcessor { ... }
impl PayloadProcessor { ... }
// ✅ Auxiliary types follow the primary type
pub struct CacheWaitDurations { ... }
pub trait WaitForCaches { ... }
impl WaitForCaches for PayloadProcessor { ... }
```
### Example Contribution Workflow
Let's say you want to fix a bug where external IP resolution fails on startup:
1. **Create a branch**:
```bash
git checkout -b fix-external-ip-resolution
```
2. **Find the relevant code**:
```bash
# Search for IP resolution code
rg "external.*ip" --type rust
```
3. **Reason about the problem, when the problem is identified, make the fix**:
```rust
// In crates/net/discv4/src/lib.rs
pub fn resolve_external_ip() -> Option<IpAddr> {
// Add fallback mechanism
nat::external_ip()
.or_else(|| nat::external_ip_from_stun())
.or_else(|| Some(DEFAULT_IP))
}
```
4. **Add a test**:
```rust
#[test]
fn test_external_ip_fallback() {
// Test that resolution has proper fallbacks
}
```
5. **Run checks** (IMPORTANT!):
```bash
cargo +nightly fmt --all
cargo clippy --workspace --all-features # Make sure WHOLE WORKSPACE compiles!
cargo nextest run -p reth-discv4
```
6. **Commit with clear message**:
```bash
git commit -m "fix: add fallback for external IP resolution
Previously, node startup could fail if external IP resolution
failed. This adds fallback mechanisms to ensure the node can
always start with a reasonable default."
```
## Quick Reference
### Essential Commands
```bash
# Format code
cargo +nightly fmt --all
# Run lints
cargo +nightly clippy --workspace --all-features
# Run tests
cargo nextest run --workspace
# Run specific benchmark
cargo bench --bench bench_name
# Build optimized binary
cargo build --release
# Check compilation for all features
cargo check --workspace --all-features
# Check documentation
cargo docs --document-private-items
# Regenerate CLI reference docs (after CLI changes)
make update-book-cli
```

1
CLAUDE.md Symbolic link
View File

@@ -0,0 +1 @@
AGENTS.md

1078
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ exclude = [".github/"]
[workspace]
members = [
"bin/reth-bb/",
"bin/reth-bench/",
"bin/reth/",
"crates/storage/rpc-provider/",
@@ -26,6 +27,7 @@ members = [
"crates/engine/invalid-block-hooks/",
"crates/engine/local",
"crates/engine/primitives/",
"crates/engine/execution-cache/",
"crates/engine/tree/",
"crates/engine/util/",
"crates/era",
@@ -76,8 +78,6 @@ members = [
"crates/payload/primitives/",
"crates/payload/validator/",
"crates/payload/util/",
"crates/primitives-traits/",
"crates/primitives/",
"crates/prune/db",
"crates/prune/prune",
"crates/prune/types",
@@ -99,8 +99,6 @@ members = [
"crates/stages/types/",
"crates/static-file/static-file",
"crates/static-file/types/",
"crates/storage/codecs/",
"crates/storage/codecs/derive/",
"crates/storage/db-api/",
"crates/storage/db-common",
"crates/storage/db-models/",
@@ -111,7 +109,6 @@ members = [
"crates/storage/nippy-jar/",
"crates/storage/provider/",
"crates/storage/storage-api/",
"crates/storage/zstd-compressors/",
"crates/tasks/",
"crates/tokio-util/",
"crates/tracing/",
@@ -329,8 +326,8 @@ reth-cli = { path = "crates/cli/cli" }
reth-cli-commands = { path = "crates/cli/commands" }
reth-cli-runner = { path = "crates/cli/runner" }
reth-cli-util = { path = "crates/cli/util" }
reth-codecs = { path = "crates/storage/codecs" }
reth-codecs-derive = { path = "crates/storage/codecs/derive" }
reth-codecs = { version = "0.1.0", default-features = false }
reth-codecs-derive = "0.1.0"
reth-config = { path = "crates/config", default-features = false }
reth-consensus = { path = "crates/consensus/consensus", default-features = false }
reth-consensus-common = { path = "crates/consensus/common", default-features = false }
@@ -346,6 +343,7 @@ reth-downloaders = { path = "crates/net/downloaders" }
reth-e2e-test-utils = { path = "crates/e2e-test-utils" }
reth-ecies = { path = "crates/net/ecies" }
reth-engine-local = { path = "crates/engine/local" }
reth-execution-cache = { path = "crates/engine/execution-cache" }
reth-engine-primitives = { path = "crates/engine/primitives", default-features = false }
reth-engine-tree = { path = "crates/engine/tree" }
reth-engine-util = { path = "crates/engine/util" }
@@ -397,8 +395,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, features = ["__internal"] }
reth-primitives-traits = { path = "crates/primitives-traits", default-features = false }
reth-primitives-traits = { version = "0.1.0", default-features = false }
reth-provider = { path = "crates/storage/provider" }
reth-prune = { path = "crates/prune/prune" }
reth-prune-types = { path = "crates/prune/types", default-features = false }
@@ -414,6 +411,7 @@ reth-rpc-eth-types = { path = "crates/rpc/rpc-eth-types", default-features = fal
reth-rpc-layer = { path = "crates/rpc/rpc-layer" }
reth-rpc-server-types = { path = "crates/rpc/rpc-server-types" }
reth-rpc-convert = { path = "crates/rpc/rpc-convert" }
reth-rpc-traits = { version = "0.1.0", default-features = false }
reth-stages = { path = "crates/stages/stages" }
reth-stages-api = { path = "crates/stages/api" }
reth-stages-types = { path = "crates/stages/types", default-features = false }
@@ -432,7 +430,7 @@ reth-trie-common = { path = "crates/trie/common", default-features = false }
reth-trie-db = { path = "crates/trie/db" }
reth-trie-parallel = { path = "crates/trie/parallel" }
reth-trie-sparse = { path = "crates/trie/sparse", default-features = false }
reth-zstd-compressors = { path = "crates/storage/zstd-compressors", default-features = false }
reth-zstd-compressors = { version = "0.1.0", default-features = false }
# revm
revm = { version = "36.0.0", default-features = false }
@@ -449,42 +447,42 @@ alloy-dyn-abi = "1.5.6"
alloy-primitives = { version = "1.5.6", default-features = false, features = ["map-foldhash"] }
alloy-sol-types = { version = "1.5.6", default-features = false }
alloy-chains = { version = "0.2.5", default-features = false }
alloy-chains = { version = "0.2.33", default-features = false }
alloy-eip2124 = { version = "0.2.0", default-features = false }
alloy-eip7928 = { version = "0.3.0", default-features = false }
alloy-evm = { version = "0.29.2", default-features = false }
alloy-evm = { version = "0.30.0", default-features = false }
alloy-rlp = { version = "0.3.13", default-features = false, features = ["core-net"] }
alloy-trie = { version = "0.9.4", default-features = false }
alloy-hardforks = "0.4.5"
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 }
alloy-consensus = { version = "1.8.2", default-features = false }
alloy-contract = { version = "1.8.2", default-features = false }
alloy-eips = { version = "1.8.2", default-features = false }
alloy-genesis = { version = "1.8.2", default-features = false }
alloy-json-rpc = { version = "1.8.2", default-features = false }
alloy-network = { version = "1.8.2", default-features = false }
alloy-network-primitives = { version = "1.8.2", default-features = false }
alloy-provider = { version = "1.8.2", features = ["reqwest", "debug-api"], default-features = false }
alloy-pubsub = { version = "1.8.2", default-features = false }
alloy-rpc-client = { version = "1.8.2", default-features = false }
alloy-rpc-types = { version = "1.8.2", features = ["eth"], default-features = false }
alloy-rpc-types-admin = { version = "1.8.2", default-features = false }
alloy-rpc-types-anvil = { version = "1.8.2", default-features = false }
alloy-rpc-types-beacon = { version = "1.8.2", default-features = false }
alloy-rpc-types-debug = { version = "1.8.2", default-features = false }
alloy-rpc-types-engine = { version = "1.8.2", default-features = false }
alloy-rpc-types-eth = { version = "1.8.2", default-features = false }
alloy-rpc-types-mev = { version = "1.8.2", default-features = false }
alloy-rpc-types-trace = { version = "1.8.2", default-features = false }
alloy-rpc-types-txpool = { version = "1.8.2", default-features = false }
alloy-serde = { version = "1.8.2", default-features = false }
alloy-signer = { version = "1.8.2", default-features = false }
alloy-signer-local = { version = "1.8.2", default-features = false }
alloy-transport = { version = "1.8.2" }
alloy-transport-http = { version = "1.8.2", features = ["reqwest-rustls-tls"], default-features = false }
alloy-transport-ipc = { version = "1.8.2", default-features = false }
alloy-transport-ws = { version = "1.8.2", default-features = false }
# op
op-alloy-rpc-types = { version = "0.24.0", default-features = false }
@@ -581,7 +579,7 @@ futures-util = { version = "0.3", default-features = false }
hyper = "1.3"
hyper-util = "0.1.5"
pin-project = "1.0.12"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots", "stream"] }
reqwest = { version = "0.13", default-features = false, features = ["rustls", "stream"] }
tracing-futures = "0.2"
tower = "0.5"
tower-http = "0.6"
@@ -647,6 +645,7 @@ ethereum_ssz_derive = "0.10.1"
# allocators
jemalloc_pprof = { version = "0.8", default-features = false }
tikv-jemalloc-ctl = "0.6"
tikv-jemalloc-sys = "0.6"
tikv-jemallocator = "0.6"
tracy-client = { version = "0.18.0", features = ["demangle"] }
snmalloc-rs = { version = "0.3.7", features = ["build_cc"] }
@@ -681,7 +680,6 @@ memmap2 = "0.9.4"
mev-share-sse = { version = "0.5.0", default-features = false }
num-traits = "0.2.15"
page_size = "0.6.0"
parity-scale-codec = "3.2.1"
plain_hasher = "0.2"
pretty_assertions = "1.4"
ratatui = { version = "0.30", default-features = false }
@@ -694,7 +692,7 @@ snap = "1.1.1"
socket2 = { version = "0.6", default-features = false }
sysinfo = { version = "0.38", default-features = false }
tracing-journald = "0.3"
tracing-logfmt = "=0.3.5"
tracing-logfmt = "0.3.7"
tracing-samply = "0.1"
tracing-subscriber = { version = "0.3", default-features = false }
tracing-tracy = "0.11"

View File

@@ -70,7 +70,7 @@ build-%-reproducible:
LC_ALL=C \
TZ=UTC \
JEMALLOC_OVERRIDE=/usr/lib/x86_64-linux-gnu/libjemalloc.a \
cargo build --bin reth --features "$(FEATURES) jemalloc-unprefixed" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
cargo build --bin reth --features "$(FEATURES)" --profile "reproducible" --locked --target x86_64-unknown-linux-gnu
.PHONY: build-debug
build-debug: ## Build the reth binary into `target/debug` directory.

100
bin/reth-bb/Cargo.toml Normal file
View File

@@ -0,0 +1,100 @@
[package]
name = "reth-bb"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
description = "Reth node configured for big block payload execution"
[lints]
workspace = true
[dependencies]
# reth
reth-ethereum-cli.workspace = true
reth-chainspec.workspace = true
reth-ethereum-primitives.workspace = true
reth-cli-util.workspace = true
reth-node-core.workspace = true
reth-node-ethereum.workspace = true
reth-node-builder.workspace = true
reth-node-api.workspace = true
reth-ethereum-consensus.workspace = true
reth-engine-primitives = { workspace = true, features = ["std"] }
reth-engine-tree.workspace = true
reth-primitives-traits.workspace = true
reth-payload-primitives.workspace = true
reth-provider.workspace = true
reth-rpc-api.workspace = true
reth-rpc-engine-api.workspace = true
reth-evm.workspace = true
reth-evm-ethereum.workspace = true
reth-ethereum-forks.workspace = true
reth-revm.workspace = true
reth-consensus.workspace = true
reth-chain-state.workspace = true
reth-errors.workspace = true
reth-storage-errors.workspace = true
# alloy
alloy-rpc-types = { workspace = true, features = ["engine"] }
alloy-primitives.workspace = true
alloy-rlp.workspace = true
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-evm.workspace = true
# tracing
tracing.workspace = true
# misc
clap = { workspace = true, features = ["derive", "env"] }
jsonrpsee = { workspace = true, features = ["server", "macros"] }
async-trait.workspace = true
derive_more.workspace = true
crossbeam-channel.workspace = true
tokio = { workspace = true, features = ["sync"] }
revm.workspace = true
revm-primitives.workspace = true
alloy-hardforks.workspace = true
metrics.workspace = true
# std
eyre.workspace = true
[features]
default = [
"jemalloc",
"reth-cli-util/jemalloc",
"asm-keccak",
"min-debug-logs",
]
jemalloc = [
"reth-cli-util/jemalloc",
"reth-node-core/jemalloc",
"reth-ethereum-cli/jemalloc",
"reth-provider/jemalloc",
]
asm-keccak = [
"reth-node-core/asm-keccak",
"reth-ethereum-cli/asm-keccak",
"reth-node-ethereum/asm-keccak",
"alloy-primitives/asm-keccak",
"alloy-evm/asm-keccak",
"revm/asm-keccak",
"revm-primitives/asm-keccak",
]
min-debug-logs = [
"tracing/release_max_level_debug",
"reth-ethereum-cli/min-debug-logs",
"reth-node-core/min-debug-logs",
]
[[bin]]
name = "reth-bb"
path = "src/main.rs"

67
bin/reth-bb/README.md Normal file
View File

@@ -0,0 +1,67 @@
# reth-bb
A modified reth node for benchmarking **big block** execution — payloads that merge transactions from multiple consecutive blocks into a single block to simulate high-gas workloads.
> **Not for production use.** reth-bb disables some consensus-related validations to allow artificially large blocks. It is intended solely for performance benchmarking.
## How it works
reth-bb extends the standard Ethereum node with:
1. **Multi-segment execution** — a custom `reth_newPayload` handler that accepts optional `BigBlockData` alongside the payload. When present, the block is executed in multiple segments, each with its own EVM environment (matching the original blocks that were merged).
2. **Relaxed consensus** — the gas-limit bound-divisor check and blob gas validation are skipped, since merged blocks exceed single-block limits.
## Quick start
The full workflow has four steps: **build** binaries, **generate** big blocks, **start** reth-bb, and **replay** the payloads.
### Prerequisites
- A synced reth datadir for the target chain (e.g. hoodi)
- Rust toolchain
### 1. Build
```bash
cargo build --profile profiling -p reth-bb -p reth-bench
```
### 2. Generate big blocks
Fetch consecutive blocks from an RPC and merge them until a target gas is reached. Use `--from-block` set to the block number following the one the node is currently synced to (i.e. the next block the node would process):
```bash
reth-bench generate-big-block \
--rpc-url https://rpc.hoodi.ethpandaops.io \
--chain hoodi \
--from-block 910020 \
--target-gas 2G \
--num-big-blocks 5 \
--output-dir /tmp/payloads
```
This produces one JSON file per big block in the output directory.
### 3. Start reth-bb
```bash
reth-bb node \
--datadir /data/reth/hoodi \
--chain hoodi \
--http --http.api debug,eth \
--authrpc.jwtsecret /tmp/jwt.hex \
-d
```
### 4. Replay payloads
```bash
reth-bench replay-payloads \
--engine-rpc-url http://localhost:8551 \
--jwt-secret /tmp/jwt.hex \
--payload-dir /tmp/payloads \
--reth-new-payload
```
The `--reth-new-payload` flag is required for big blocks — it uses the `reth_newPayload` endpoint which carries the multi-segment execution metadata.

570
bin/reth-bb/src/evm.rs Normal file
View File

@@ -0,0 +1,570 @@
//! Big-block executor.
//!
//! Provides [`BbBlockExecutor`] and [`BbBlockExecutorFactory`] which handle
//! segment boundaries within big-block payloads.
//!
//! [`BbBlockExecutor`] wraps [`EthBlockExecutor`] and intercepts
//! `execute_transaction` to apply segment-boundary changes.
use crate::evm_config::BigBlockSegment;
use alloy_eips::eip7685::Requests;
use alloy_evm::{
block::{
BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockExecutorFactory,
BlockExecutorFor, ExecutableTx, OnStateHook, StateChangeSource, StateDB,
},
eth::{EthBlockExecutionCtx, EthBlockExecutor, EthEvmContext, EthTxResult},
precompiles::PrecompilesMap,
Database, EthEvm, EthEvmFactory, Evm, FromRecoveredTx, FromTxWithEncoded,
};
use alloy_primitives::B256;
use reth_ethereum_primitives::{Receipt, TransactionSigned};
use reth_evm_ethereum::RethReceiptBuilder;
use revm::{
context::{BlockEnv, TxEnv},
context_interface::result::{EVMError, HaltReason},
handler::PrecompileProvider,
interpreter::InterpreterResult,
primitives::hardfork::SpecId,
Inspector,
};
use std::sync::{Arc, Mutex};
use tracing::{debug, trace};
// ---------------------------------------------------------------------------
// BbEvmPlan — runtime segment tracking state
// ---------------------------------------------------------------------------
/// Runtime state for segment boundary tracking.
pub(crate) struct BbEvmPlan {
/// The segment boundaries and environments.
pub(crate) segments: Vec<BigBlockSegment>,
/// Index of the next segment to switch to (starts at 1).
pub(crate) next_segment: usize,
/// Number of user transactions executed so far.
pub(crate) tx_counter: usize,
/// Block hashes to seed for inter-segment BLOCKHASH resolution.
/// Includes both prior block hashes and inter-segment hashes.
pub(crate) block_hashes_to_seed: Vec<(u64, B256)>,
}
impl BbEvmPlan {
/// Creates a new `BbEvmPlan` from segments and hardfork flags.
pub(crate) fn new(segments: Vec<BigBlockSegment>) -> Self {
// Pre-compute all inter-segment block hashes.
let mut block_hashes_to_seed = Vec::new();
for seg in segments.iter().skip(1) {
let finished_block_number = seg.evm_env.block_env.number.saturating_to::<u64>() - 1;
let finished_block_hash = seg.ctx.parent_hash;
block_hashes_to_seed.push((finished_block_number, finished_block_hash));
}
Self { segments, next_segment: 1, tx_counter: 0, block_hashes_to_seed }
}
/// Returns the 256 block hashes relevant to a segment with the given block
/// number. BLOCKHASH can look back 256 blocks, so we select entries in
/// `[block_number - 256, block_number)`.
pub(crate) fn hashes_for_block(&self, block_number: u64) -> Vec<(u64, B256)> {
let min = block_number.saturating_sub(256);
self.block_hashes_to_seed
.iter()
.copied()
.filter(|(n, _)| *n >= min && *n < block_number)
.collect()
}
}
impl std::fmt::Debug for BbEvmPlan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BbEvmPlan")
.field("segments", &self.segments)
.field("next_segment", &self.next_segment)
.field("tx_counter", &self.tx_counter)
.field("block_hashes_to_seed", &self.block_hashes_to_seed)
.finish()
}
}
// ---------------------------------------------------------------------------
// BbBlockExecutor — handles segment boundaries
// ---------------------------------------------------------------------------
/// Function pointer that seeds block hashes into the DB's block hash cache.
///
/// Injected from `ConfigureEvm::create_executor` where the concrete `State<DB>`
/// type is known, allowing `BbBlockExecutor` to reseed the ring buffer at
/// segment boundaries without requiring additional trait bounds on `DB`.
pub(crate) type BlockHashSeeder<DB> = fn(&mut DB, &[(u64, B256)]);
/// Block executor that wraps [`EthBlockExecutor`] and handles segment-boundary
/// changes for big-block execution.
///
/// At segment boundaries, the inner executor is finished (applying its
/// end-of-block logic: post-execution system calls, withdrawal balance
/// increments) and a new one is constructed for the next segment (applying
/// its start-of-block logic: EIP-2935/EIP-4788 system calls).
///
/// Gas counters reset at each boundary so that each segment's real gas limit
/// is used (preserving correct GASLIMIT opcode behavior). Accumulated offsets
/// are applied to receipts and totals in `finish()`.
pub(crate) struct BbBlockExecutor<'a, DB, I, P, Spec>
where
DB: Database,
{
/// The inner executor. `None` transiently during `apply_segment_boundary`.
inner: Option<EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder>>,
plan: Option<BbEvmPlan>,
/// Requests accumulated from segments that have been finished at
/// boundaries. Merged into the final result in `finish()`.
accumulated_requests: Requests,
/// Cumulative gas used by all segments that have been finished at
/// boundaries. Added to receipts and the final gas total in `finish()`.
gas_used_offset: u64,
/// Cumulative blob gas used by all segments that have been finished at
/// boundaries.
blob_gas_used_offset: u64,
/// Shared state hook that survives inner executor finish/reconstruct
/// cycles at segment boundaries. Each inner executor receives a
/// forwarding hook that delegates to this shared instance.
shared_hook: Arc<Mutex<Option<Box<dyn OnStateHook>>>>,
/// Callback to reseed block hashes into the DB's cache at segment
/// boundaries. See [`BlockHashSeeder`].
block_hash_seeder: Option<BlockHashSeeder<DB>>,
}
impl<'a, DB, I, P, Spec> BbBlockExecutor<'a, DB, I, P, Spec>
where
DB: StateDB,
I: Inspector<EthEvmContext<DB>>,
P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
EthEvm<DB, I, P>: Evm<
DB = DB,
Tx = TxEnv,
HaltReason = HaltReason,
Error = EVMError<DB::Error>,
Spec = SpecId,
BlockEnv = BlockEnv,
>,
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
{
pub(crate) fn new(
evm: EthEvm<DB, I, P>,
ctx: EthBlockExecutionCtx<'a>,
spec: Spec,
receipt_builder: RethReceiptBuilder,
plan: Option<BbEvmPlan>,
block_hash_seeder: Option<BlockHashSeeder<DB>>,
) -> Self {
let inner = EthBlockExecutor::new(evm, ctx, spec, receipt_builder);
Self {
inner: Some(inner),
plan,
accumulated_requests: Requests::default(),
gas_used_offset: 0,
blob_gas_used_offset: 0,
shared_hook: Arc::new(Mutex::new(None)),
block_hash_seeder,
}
}
/// Creates a forwarding `OnStateHook` that delegates to the shared hook.
fn forwarding_hook(&self) -> Option<Box<dyn OnStateHook>> {
let shared = self.shared_hook.clone();
Some(Box::new(move |source: StateChangeSource, state: &revm::state::EvmState| {
if let Some(hook) = shared.lock().unwrap().as_mut() {
hook.on_state(source, state);
}
}))
}
const fn inner(&self) -> &EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
self.inner.as_ref().expect("inner executor must exist")
}
const fn inner_mut(
&mut self,
) -> &mut EthBlockExecutor<'a, EthEvm<DB, I, P>, Spec, RethReceiptBuilder> {
self.inner.as_mut().expect("inner executor must exist")
}
fn reseed_block_hashes_for(&mut self, block_number: u64) {
let Some(seeder) = self.block_hash_seeder else { return };
let hashes = match &self.plan {
Some(plan) => plan.hashes_for_block(block_number),
None => return,
};
seeder(self.inner_mut().evm_mut().db_mut(), &hashes);
}
fn maybe_apply_boundary(&mut self) -> Result<(), BlockExecutionError> {
loop {
let plan = match &self.plan {
Some(p) => p,
None => return Ok(()),
};
if plan.next_segment >= plan.segments.len() ||
plan.tx_counter != plan.segments[plan.next_segment].start_tx
{
return Ok(());
}
self.apply_segment_boundary()?;
}
}
fn apply_segment_boundary(&mut self) -> Result<(), BlockExecutionError> {
let plan = self.plan.as_mut().expect("plan must exist");
let seg_idx = plan.next_segment;
let prev_seg_idx = seg_idx - 1;
debug!(
target: "engine::bb::evm",
seg_idx,
tx_counter = plan.tx_counter,
"Applying segment boundary"
);
// Swap the inner executor's ctx to the finished segment's values so
// that finish() applies the correct withdrawals and post-execution
// system calls for that segment.
let prev_segment = &plan.segments[prev_seg_idx];
let prev_ctx = EthBlockExecutionCtx {
parent_hash: prev_segment.ctx.parent_hash,
parent_beacon_block_root: prev_segment.ctx.parent_beacon_block_root,
ommers: prev_segment.ctx.ommers,
withdrawals: prev_segment.ctx.withdrawals.clone(),
extra_data: prev_segment.ctx.extra_data.clone(),
tx_count_hint: prev_segment.ctx.tx_count_hint,
};
// Clone the next segment's data before we consume inner.
let new_segment = &plan.segments[seg_idx];
let new_block_env = new_segment.evm_env.block_env.clone();
let mut new_cfg_env = new_segment.evm_env.cfg_env.clone();
new_cfg_env.disable_base_fee = true;
let new_ctx = EthBlockExecutionCtx {
parent_hash: new_segment.ctx.parent_hash,
parent_beacon_block_root: new_segment.ctx.parent_beacon_block_root,
ommers: new_segment.ctx.ommers,
withdrawals: new_segment.ctx.withdrawals.clone(),
extra_data: new_segment.ctx.extra_data.clone(),
tx_count_hint: new_segment.ctx.tx_count_hint,
};
plan.next_segment += 1;
// Finish the inner executor for the completed segment. This applies
// post-execution system calls (EIP-7002/7251) and withdrawal balance
// increments via EthBlockExecutor::finish().
let mut inner = self.inner.take().expect("inner executor must exist");
inner.ctx = prev_ctx;
let spec = inner.spec.clone();
let receipt_builder = inner.receipt_builder;
let (mut evm, result) = inner.finish()?;
// Receipts already have globally-correct cumulative_gas_used (fixed
// up in commit_transaction). Update the offset with this segment's
// gas so that subsequent segments' receipts are adjusted correctly.
self.gas_used_offset += result.gas_used;
self.blob_gas_used_offset += result.blob_gas_used;
self.accumulated_requests.extend(result.requests);
let last_receipt_cumulative =
result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
let seg_block_number = prev_segment.evm_env.block_env.number.saturating_to::<u64>();
debug!(
target: "engine::bb::evm",
prev_seg_idx,
seg_block_number,
segment_gas_used = result.gas_used,
gas_used_offset = self.gas_used_offset,
last_receipt_cumulative,
receipt_count = result.receipts.len(),
"Finished segment"
);
// Swap EVM env to the next segment's values (using real gas_limit).
let ctx = evm.ctx_mut();
ctx.block = new_block_env;
ctx.cfg = new_cfg_env;
// Build a new inner executor for the next segment. gas_used starts
// at 0 so the per-transaction gas check uses this segment's real
// gas_limit correctly.
let mut new_inner = EthBlockExecutor::new(evm, new_ctx, spec, receipt_builder);
// Carry forward receipts from prior segments.
new_inner.receipts = result.receipts;
// Re-install the forwarding state hook so the parallel state root
// task continues to receive state changes.
if self.shared_hook.lock().unwrap().is_some() {
new_inner.set_state_hook(self.forwarding_hook());
}
self.inner = Some(new_inner);
// Reseed the block hash cache for the new segment's 256-block window
// before applying pre-execution changes (which may use BLOCKHASH).
let new_block_number = self.plan.as_ref().unwrap().segments[seg_idx]
.evm_env
.block_env
.number
.saturating_to::<u64>();
self.reseed_block_hashes_for(new_block_number);
// Apply pre-execution changes for the new segment (EIP-2935, EIP-4788).
self.inner_mut().apply_pre_execution_changes()?;
trace!(target: "engine::bb::evm", "Started segment {seg_idx}");
Ok(())
}
}
impl<'a, DB, I, P, Spec> BlockExecutor for BbBlockExecutor<'a, DB, I, P, Spec>
where
DB: StateDB,
I: Inspector<EthEvmContext<DB>>,
P: PrecompileProvider<EthEvmContext<DB>, Output = InterpreterResult>,
Spec: alloy_evm::eth::spec::EthExecutorSpec + Clone,
EthEvm<DB, I, P>: Evm<
DB = DB,
Tx = TxEnv,
HaltReason = HaltReason,
Error = EVMError<DB::Error>,
Spec = SpecId,
BlockEnv = BlockEnv,
>,
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
{
type Transaction = TransactionSigned;
type Receipt = Receipt;
type Evm = EthEvm<DB, I, P>;
type Result = EthTxResult<HaltReason, alloy_consensus::TxType>;
fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
// Swap the EVM's block_env and executor ctx to the first segment's
// values so that the initial EIP-2935/EIP-4788 system calls use the
// correct block number and parent hash. Without this, the outer big
// block header's block_number (which is synthetic) would be used,
// writing to wrong EIP-2935 slots and corrupting state.
if let Some(seg0) = self.plan.as_ref().map(|p| &p.segments[0]) {
let block_env = seg0.evm_env.block_env.clone();
let block_number = block_env.number.saturating_to::<u64>();
let mut cfg_env = seg0.evm_env.cfg_env.clone();
cfg_env.disable_base_fee = true;
let seg0_ctx = EthBlockExecutionCtx {
parent_hash: seg0.ctx.parent_hash,
parent_beacon_block_root: seg0.ctx.parent_beacon_block_root,
ommers: seg0.ctx.ommers,
withdrawals: seg0.ctx.withdrawals.clone(),
extra_data: seg0.ctx.extra_data.clone(),
tx_count_hint: seg0.ctx.tx_count_hint,
};
let inner = self.inner_mut();
let evm_ctx = inner.evm.ctx_mut();
evm_ctx.block = block_env;
evm_ctx.cfg = cfg_env;
inner.ctx = seg0_ctx;
self.reseed_block_hashes_for(block_number);
}
self.inner_mut().apply_pre_execution_changes()
}
fn execute_transaction_without_commit(
&mut self,
tx: impl ExecutableTx<Self>,
) -> Result<Self::Result, BlockExecutionError> {
self.maybe_apply_boundary()?;
self.inner_mut().execute_transaction_without_commit(tx)
}
fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
let gas_used = self.inner_mut().commit_transaction(output)?;
// Fix up cumulative_gas_used on the just-committed receipt so that
// the receipt root task (which reads receipts incrementally) sees
// globally-correct values across all segments.
let offset = self.gas_used_offset;
if offset > 0 &&
let Some(receipt) = self.inner_mut().receipts.last_mut()
{
receipt.cumulative_gas_used += offset;
}
if let Some(plan) = &mut self.plan {
plan.tx_counter += 1;
}
Ok(gas_used)
}
fn finish(
mut self,
) -> Result<(Self::Evm, BlockExecutionResult<Self::Receipt>), BlockExecutionError> {
// Swap the inner executor's ctx to the last segment's ctx so that
// EthBlockExecutor::finish() applies the correct withdrawal balance
// increments and post-execution system calls.
if let Some(last_seg) = self.plan.as_ref().map(|p| p.segments.last().unwrap()) {
let last_ctx = EthBlockExecutionCtx {
parent_hash: last_seg.ctx.parent_hash,
parent_beacon_block_root: last_seg.ctx.parent_beacon_block_root,
ommers: last_seg.ctx.ommers,
withdrawals: last_seg.ctx.withdrawals.clone(),
extra_data: last_seg.ctx.extra_data.clone(),
tx_count_hint: last_seg.ctx.tx_count_hint,
};
self.inner_mut().ctx = last_ctx;
}
let inner = self.inner.take().expect("inner executor must exist");
let (evm, mut result) = inner.finish()?;
// Receipts already have globally-correct cumulative_gas_used (fixed
// up in commit_transaction). Add the offset to the totals so they
// reflect gas across all segments.
let last_segment_gas = result.gas_used;
result.gas_used += self.gas_used_offset;
result.blob_gas_used += self.blob_gas_used_offset;
let last_receipt_cumulative =
result.receipts.last().map(|r| r.cumulative_gas_used).unwrap_or(0);
debug!(
target: "engine::bb::evm",
last_segment_gas,
gas_used_offset = self.gas_used_offset,
total_gas_used = result.gas_used,
last_receipt_cumulative,
receipt_count = result.receipts.len(),
"Finished final segment"
);
// Merge requests accumulated from earlier segment boundaries into
// the final result.
if !self.accumulated_requests.is_empty() {
let mut merged = self.accumulated_requests;
merged.extend(result.requests);
result.requests = merged;
}
Ok((evm, result))
}
fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
if self.plan.is_some() {
// Store the real hook in the shared slot and give the inner
// executor a lightweight forwarder. This way the hook survives
// inner executor finish/reconstruct cycles at segment boundaries.
*self.shared_hook.lock().unwrap() = hook;
let fwd = self.forwarding_hook();
self.inner_mut().set_state_hook(fwd);
} else {
self.inner_mut().set_state_hook(hook);
}
}
fn evm_mut(&mut self) -> &mut Self::Evm {
self.inner_mut().evm_mut()
}
fn evm(&self) -> &Self::Evm {
self.inner().evm()
}
fn receipts(&self) -> &[Self::Receipt] {
self.inner().receipts()
}
}
// ---------------------------------------------------------------------------
// BbBlockExecutorFactory
// ---------------------------------------------------------------------------
/// Block executor factory that produces [`BbBlockExecutor`] for
/// boundary-aware big-block execution.
#[derive(Debug, Clone)]
pub struct BbBlockExecutorFactory<Spec> {
receipt_builder: RethReceiptBuilder,
spec: Spec,
evm_factory: EthEvmFactory,
/// Staged plan consumed by the next [`BbBlockExecutor`].
pub(crate) staged_plan: Arc<Mutex<Option<BbEvmPlan>>>,
}
impl<Spec> BbBlockExecutorFactory<Spec> {
pub fn new(
receipt_builder: RethReceiptBuilder,
spec: Spec,
evm_factory: EthEvmFactory,
) -> Self {
Self { receipt_builder, spec, evm_factory, staged_plan: Arc::new(Mutex::new(None)) }
}
pub const fn evm_factory(&self) -> &EthEvmFactory {
&self.evm_factory
}
pub const fn spec(&self) -> &Spec {
&self.spec
}
pub const fn receipt_builder(&self) -> &RethReceiptBuilder {
&self.receipt_builder
}
pub(crate) fn stage_plan(&self, plan: BbEvmPlan) {
*self.staged_plan.lock().unwrap() = Some(plan);
}
fn take_plan(&self) -> Option<BbEvmPlan> {
self.staged_plan.lock().unwrap().take()
}
pub(crate) fn create_executor_with_seeder<'a, DB, I>(
&'a self,
evm: EthEvm<DB, I, PrecompilesMap>,
ctx: EthBlockExecutionCtx<'a>,
block_hash_seeder: Option<BlockHashSeeder<DB>>,
) -> BbBlockExecutor<'a, DB, I, PrecompilesMap, &'a Spec>
where
Spec: alloy_evm::eth::spec::EthExecutorSpec,
DB: StateDB + 'a,
I: Inspector<EthEvmContext<DB>> + 'a,
{
let plan = self.take_plan();
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, block_hash_seeder)
}
}
impl<Spec> BlockExecutorFactory for BbBlockExecutorFactory<Spec>
where
Spec: alloy_evm::eth::spec::EthExecutorSpec + 'static,
TxEnv: FromRecoveredTx<TransactionSigned> + FromTxWithEncoded<TransactionSigned>,
{
type EvmFactory = EthEvmFactory;
type ExecutionCtx<'a> = EthBlockExecutionCtx<'a>;
type Transaction = TransactionSigned;
type Receipt = Receipt;
fn evm_factory(&self) -> &Self::EvmFactory {
&self.evm_factory
}
fn create_executor<'a, DB, I>(
&'a self,
evm: EthEvm<DB, I, PrecompilesMap>,
ctx: EthBlockExecutionCtx<'a>,
) -> impl BlockExecutorFor<'a, Self, DB, I>
where
DB: StateDB + 'a,
I: Inspector<EthEvmContext<DB>> + 'a,
{
let plan = self.take_plan();
BbBlockExecutor::new(evm, ctx, &self.spec, self.receipt_builder, plan, None)
}
}

View File

@@ -0,0 +1,291 @@
//! Big-block EVM configuration.
//!
//! Wraps [`EthEvmConfig`] to create executors that handle multi-segment
//! big-block execution internally. At transaction boundaries defined by
//! [`BigBlockData`], the executor swaps the EVM environment (block env,
//! cfg env) and applies pre/post execution changes for each segment.
pub(crate) use reth_engine_primitives::BigBlockData;
use crate::{
evm::{BbBlockExecutorFactory, BbEvmPlan},
BigBlockMap,
};
use alloy_consensus::Header;
use alloy_evm::eth::EthBlockExecutionCtx;
use alloy_primitives::B256;
use alloy_rpc_types::engine::ExecutionData;
use core::convert::Infallible;
use reth_chainspec::{ChainSpec, EthChainSpec};
use reth_ethereum_forks::Hardforks;
use reth_ethereum_primitives::EthPrimitives;
use reth_evm::{
ConfigureEngineEvm, ConfigureEvm, Database, EvmEnv, ExecutableTxIterator,
NextBlockEnvAttributes,
};
use reth_evm_ethereum::{EthBlockAssembler, EthEvmConfig, RethReceiptBuilder};
use reth_primitives_traits::{SealedBlock, SealedHeader};
use revm::primitives::hardfork::SpecId;
use std::sync::Arc;
use tracing::debug;
use alloy_evm::{eth::spec::EthExecutorSpec, EthEvmFactory};
use reth_evm::{EvmEnvFor, ExecutionCtxFor};
// ---------------------------------------------------------------------------
// Execution plan types
// ---------------------------------------------------------------------------
/// A single execution segment within a big block.
#[derive(Debug, Clone)]
pub(crate) struct BigBlockSegment {
/// Transaction index at which this segment starts.
pub start_tx: usize,
/// The EVM environment for this segment.
pub evm_env: EvmEnv,
/// The execution context for this segment.
pub ctx: EthBlockExecutionCtx<'static>,
}
// ---------------------------------------------------------------------------
// BbEvmConfig
// ---------------------------------------------------------------------------
/// EVM configuration for big-block execution.
///
/// Wraps [`EthEvmConfig`] and a shared [`BigBlockMap`]. When a big-block
/// payload is received, the plan is staged on the [`BbBlockExecutorFactory`]
/// and consumed when the executor is created. Block hashes for inter-segment
/// BLOCKHASH resolution are reseeded into `State::block_hashes` at each
/// segment boundary via a [`BlockHashSeeder`](crate::evm::BlockHashSeeder)
/// callback injected in [`ConfigureEvm::create_executor`].
#[derive(Debug, Clone)]
pub struct BbEvmConfig<C = ChainSpec> {
/// The inner Ethereum EVM configuration (used for env computation).
pub inner: EthEvmConfig<C>,
/// Shared map of pending big-block metadata.
pub pending: BigBlockMap,
/// Block executor factory for big-block execution.
executor_factory: BbBlockExecutorFactory<Arc<C>>,
/// Block assembler.
block_assembler: EthBlockAssembler<C>,
}
impl<C> BbEvmConfig<C> {
/// Creates a new big-block EVM configuration.
pub fn new(inner: EthEvmConfig<C>, pending: BigBlockMap) -> Self
where
C: Clone,
{
let chain_spec = inner.chain_spec().clone();
let executor_factory = BbBlockExecutorFactory::new(
RethReceiptBuilder::default(),
chain_spec,
EthEvmFactory::default(),
);
let block_assembler = inner.block_assembler.clone();
Self { inner, pending, executor_factory, block_assembler }
}
}
// ---------------------------------------------------------------------------
// Block hash seeder for State<DB>
// ---------------------------------------------------------------------------
/// Reseeds `State::block_hashes` with the given hashes.
///
/// This is used as a [`BlockHashSeeder`](crate::evm::BlockHashSeeder) callback,
/// injected into [`BbBlockExecutor`](crate::evm::BbBlockExecutor) from
/// `ConfigureEvm::create_executor` where the concrete `State<DB>` type is known.
/// At each segment boundary the executor calls this to populate the ring buffer
/// with the 256 block hashes relevant to the new segment's block number window.
fn seed_state_block_hashes<DB>(state: &mut &mut revm::database::State<DB>, hashes: &[(u64, B256)]) {
for &(number, hash) in hashes {
state.block_hashes.insert(number, hash);
}
}
// ---------------------------------------------------------------------------
// ConfigureEvm
// ---------------------------------------------------------------------------
impl<C> ConfigureEvm for BbEvmConfig<C>
where
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
{
type Primitives = EthPrimitives;
type Error = Infallible;
type NextBlockEnvCtx = NextBlockEnvAttributes;
type BlockExecutorFactory = BbBlockExecutorFactory<Arc<C>>;
type BlockAssembler = EthBlockAssembler<C>;
fn block_executor_factory(&self) -> &Self::BlockExecutorFactory {
&self.executor_factory
}
fn block_assembler(&self) -> &Self::BlockAssembler {
&self.block_assembler
}
fn evm_env(&self, header: &Header) -> Result<EvmEnv<SpecId>, Self::Error> {
self.inner.evm_env(header)
}
fn next_evm_env(
&self,
parent: &Header,
attributes: &NextBlockEnvAttributes,
) -> Result<EvmEnv, Self::Error> {
self.inner.next_evm_env(parent, attributes)
}
fn context_for_block<'a>(
&self,
block: &'a SealedBlock<reth_ethereum_primitives::Block>,
) -> Result<EthBlockExecutionCtx<'a>, Self::Error> {
self.inner.context_for_block(block)
}
fn context_for_next_block(
&self,
parent: &SealedHeader,
attributes: Self::NextBlockEnvCtx,
) -> Result<EthBlockExecutionCtx<'_>, Self::Error> {
self.inner.context_for_next_block(parent, attributes)
}
fn create_executor<'a, DB, I>(
&'a self,
evm: reth_evm::EvmFor<Self, &'a mut revm::database::State<DB>, I>,
ctx: EthBlockExecutionCtx<'a>,
) -> impl alloy_evm::block::BlockExecutorFor<
'a,
Self::BlockExecutorFactory,
&'a mut revm::database::State<DB>,
I,
>
where
DB: Database,
I: reth_evm::InspectorFor<Self, &'a mut revm::database::State<DB>> + 'a,
{
// Use create_executor_with_seeder to inject a concrete seeder that
// can reseed State::block_hashes at segment boundaries. The seeder
// is a function pointer that knows the concrete State<DB> type,
// allowing the generic BbBlockExecutor to reseed without additional
// trait bounds on DB.
self.executor_factory.create_executor_with_seeder(
evm,
ctx,
Some(seed_state_block_hashes::<DB>),
)
}
}
// ---------------------------------------------------------------------------
// ConfigureEngineEvm — intercepts payload methods for big blocks
// ---------------------------------------------------------------------------
impl<C> ConfigureEngineEvm<ExecutionData> for BbEvmConfig<C>
where
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
{
fn evm_env_for_payload(&self, payload: &ExecutionData) -> Result<EvmEnvFor<Self>, Self::Error> {
let payload_hash = payload.block_hash();
let has_plan = self.pending.lock().unwrap().contains_key(&payload_hash);
if has_plan {
// Compute the env from the first segment BEFORE removing the
// entry (stage_plan_for_payload removes it).
let first_exec_data = {
let pending = self.pending.lock().unwrap();
let bb_data = pending.get(&payload_hash).unwrap();
bb_data.env_switches[0].1.clone()
};
let mut env = self.inner.evm_env_for_payload(&first_exec_data)?;
// Disable basefee validation: transactions from different
// original blocks may have gas prices below the big block's
// effective basefee.
env.cfg_env.disable_base_fee = true;
// Now stage the plan on the factory (removes the entry).
self.stage_plan_for_payload(&payload_hash);
Ok(env)
} else {
self.inner.evm_env_for_payload(payload)
}
}
fn context_for_payload<'a>(
&self,
payload: &'a ExecutionData,
) -> Result<ExecutionCtxFor<'a, Self>, Self::Error> {
self.inner.context_for_payload(payload)
}
fn tx_iterator_for_payload(
&self,
payload: &ExecutionData,
) -> Result<impl ExecutableTxIterator<Self>, Self::Error> {
self.inner.tx_iterator_for_payload(payload)
}
}
// ---------------------------------------------------------------------------
// Plan construction and staging
// ---------------------------------------------------------------------------
impl<C> BbEvmConfig<C>
where
C: EthExecutorSpec + EthChainSpec<Header = Header> + Hardforks + 'static,
{
/// Takes the big-block plan for a payload hash, builds a [`BbEvmPlan`],
/// and stages it on the factory.
///
/// Must be called before `evm_with_env` is invoked for this payload.
/// In practice, this is called from `evm_env_for_payload` in the
/// engine pipeline.
pub fn stage_plan_for_payload(&self, payload_hash: &B256) {
let bb = match self.pending.lock().unwrap().remove(payload_hash) {
Some(bb) => bb,
None => return,
};
let segments: Vec<_> = bb
.env_switches
.into_iter()
.map(|(start_tx, exec_data)| {
let evm_env = self.inner.evm_env_for_payload(&exec_data).unwrap();
let ctx = self.inner.context_for_payload(&exec_data).unwrap();
let ctx = EthBlockExecutionCtx {
tx_count_hint: ctx.tx_count_hint,
parent_hash: ctx.parent_hash,
parent_beacon_block_root: ctx.parent_beacon_block_root,
ommers: &[],
withdrawals: ctx.withdrawals.map(|w| std::borrow::Cow::Owned(w.into_owned())),
extra_data: ctx.extra_data,
};
BigBlockSegment { start_tx, evm_env, ctx }
})
.collect();
debug!(
target: "engine::bb",
?payload_hash,
segments = segments.len(),
seed_hashes = bb.prior_block_hashes.len(),
"Staging multi-segment plan"
);
let mut plan = BbEvmPlan::new(segments);
// Add prior block hashes to the seeding list.
plan.block_hashes_to_seed.extend(bb.prior_block_hashes);
plan.block_hashes_to_seed.sort_unstable_by_key(|(n, _)| *n);
self.executor_factory.stage_plan(plan);
}
}

374
bin/reth-bb/src/main.rs Normal file
View File

@@ -0,0 +1,374 @@
//! reth-bb: a modified reth node for benchmarking big block execution.
#![allow(missing_docs)]
#[global_allocator]
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
mod evm;
mod evm_config;
use alloy_primitives::B256;
use alloy_rpc_types::engine::{ExecutionData, ForkchoiceState, ForkchoiceUpdated};
use async_trait::async_trait;
use clap::Parser;
use evm_config::{BbEvmConfig, BigBlockData};
use jsonrpsee::core::RpcResult;
use reth_chainspec::{ChainSpec, EthChainSpec, EthereumHardforks, Hardforks};
use reth_engine_primitives::ConsensusEngineHandle;
use reth_ethereum_cli::{chainspec::EthereumChainSpecParser, interface::Cli};
use reth_ethereum_consensus::EthBeaconConsensus;
use reth_ethereum_primitives::EthPrimitives;
use reth_evm_ethereum::EthEvmConfig;
use reth_node_api::{AddOnsContext, FullNodeComponents, NodeTypes, PayloadTypes};
use reth_node_builder::{
components::{
BasicPayloadServiceBuilder, ComponentsBuilder, ConsensusBuilder, ExecutorBuilder,
},
node::FullNodeTypes,
rpc::{
BasicEngineApiBuilder, BasicEngineValidatorBuilder, EngineApiBuilder, EngineValidatorAddOn,
EngineValidatorBuilder, PayloadValidatorBuilder, RethRpcAddOns, RpcAddOns, RpcHandle,
RpcHooks,
},
BuilderContext, Node,
};
use reth_node_ethereum::{
EthEngineTypes, EthereumEngineValidatorBuilder, EthereumEthApiBuilder, EthereumNetworkBuilder,
EthereumNode, EthereumPayloadBuilder, EthereumPoolBuilder,
};
use reth_payload_primitives::ExecutionPayload;
use reth_primitives_traits::SealedBlock;
use reth_provider::EthStorage;
use reth_rpc_api::{RethNewPayloadInput, RethPayloadStatus};
use reth_rpc_engine_api::EngineApiError;
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
use tracing::{info, trace};
/// Shared map for big block data, keyed by payload hash.
pub type BigBlockMap = Arc<Mutex<HashMap<B256, BigBlockData<ExecutionData>>>>;
// ---------------------------------------------------------------------------
// Custom RPC trait for big-block payloads
// ---------------------------------------------------------------------------
/// Big-block extension of the `reth_` engine API.
#[jsonrpsee::proc_macros::rpc(server, namespace = "reth")]
pub trait BbRethEngineApi {
/// `reth_newPayload` with optional big-block data.
#[method(name = "newPayload")]
async fn reth_new_payload(
&self,
payload: RethNewPayloadInput<ExecutionData>,
wait_for_persistence: Option<bool>,
wait_for_caches: Option<bool>,
big_block_data: Option<BigBlockData<ExecutionData>>,
) -> RpcResult<RethPayloadStatus>;
/// `reth_forkchoiceUpdated` pass-through.
#[method(name = "forkchoiceUpdated")]
async fn reth_forkchoice_updated(
&self,
forkchoice_state: ForkchoiceState,
) -> RpcResult<ForkchoiceUpdated>;
}
/// Server-side implementation of `BbRethEngineApi`.
#[derive(Debug)]
struct BbRethEngineApiHandler {
pending: BigBlockMap,
engine: ConsensusEngineHandle<EthEngineTypes>,
}
#[async_trait]
impl BbRethEngineApiServer for BbRethEngineApiHandler {
async fn reth_new_payload(
&self,
input: RethNewPayloadInput<ExecutionData>,
wait_for_persistence: Option<bool>,
wait_for_caches: Option<bool>,
big_block_data: Option<BigBlockData<ExecutionData>>,
) -> RpcResult<RethPayloadStatus> {
let wait_for_persistence = wait_for_persistence.unwrap_or(true);
let wait_for_caches = wait_for_caches.unwrap_or(true);
trace!(
target: "rpc::engine",
wait_for_persistence,
wait_for_caches,
has_big_block_data = big_block_data.is_some(),
"Serving bb reth_newPayload"
);
let payload = match input {
RethNewPayloadInput::ExecutionData(data) => data,
RethNewPayloadInput::BlockRlp(rlp) => {
let block = alloy_rlp::Decodable::decode(&mut rlp.as_ref())
.map_err(|err| EngineApiError::Internal(Box::new(err)))?;
<EthEngineTypes as PayloadTypes>::block_to_payload(SealedBlock::new_unhashed(block))
}
};
if let Some(data) = big_block_data {
let hash = ExecutionPayload::block_hash(&payload);
self.pending.lock().unwrap().insert(hash, data);
}
let (status, timings) = self
.engine
.reth_new_payload(payload, wait_for_persistence, wait_for_caches)
.await
.map_err(EngineApiError::from)?;
Ok(RethPayloadStatus {
status,
latency_us: timings.latency.as_micros() as u64,
persistence_wait_us: timings.persistence_wait.map(|d| d.as_micros() as u64),
execution_cache_wait_us: timings.execution_cache_wait.map(|d| d.as_micros() as u64),
sparse_trie_wait_us: timings.sparse_trie_wait.map(|d| d.as_micros() as u64),
})
}
async fn reth_forkchoice_updated(
&self,
forkchoice_state: ForkchoiceState,
) -> RpcResult<ForkchoiceUpdated> {
trace!(target: "rpc::engine", "Serving reth_forkchoiceUpdated");
self.engine
.fork_choice_updated(forkchoice_state, None)
.await
.map_err(|e| EngineApiError::from(e).into())
}
}
// ---------------------------------------------------------------------------
// Node add-ons wrapper
// ---------------------------------------------------------------------------
/// Add-ons for the big-block node.
#[derive(Debug)]
pub struct BbAddOns {
pending: BigBlockMap,
}
impl BbAddOns {
const fn new(pending: BigBlockMap) -> Self {
Self { pending }
}
fn make_rpc_add_ons<N: FullNodeComponents>(
&self,
) -> RpcAddOns<
N,
EthereumEthApiBuilder,
EthereumEngineValidatorBuilder,
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>,
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>,
>
where
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
{
RpcAddOns::new(
EthereumEthApiBuilder::default(),
EthereumEngineValidatorBuilder::default(),
BasicEngineApiBuilder::default(),
BasicEngineValidatorBuilder::default(),
Default::default(),
)
}
}
impl<N> reth_node_api::NodeAddOns<N> for BbAddOns
where
N: FullNodeComponents<
Types: NodeTypes<
ChainSpec: EthereumHardforks + Hardforks + Clone + 'static,
Payload = EthEngineTypes,
Primitives = EthPrimitives,
>,
>,
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
EthereumEngineValidatorBuilder: PayloadValidatorBuilder<N>,
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>: EngineApiBuilder<N>,
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
{
type Handle =
RpcHandle<N, <EthereumEthApiBuilder as reth_node_builder::rpc::EthApiBuilder<N>>::EthApi>;
async fn launch_add_ons(self, ctx: AddOnsContext<'_, N>) -> eyre::Result<Self::Handle> {
let engine_handle = ctx.beacon_engine_handle.clone();
let pending = self.pending.clone();
let rpc_add_ons = self.make_rpc_add_ons::<N>();
rpc_add_ons
.launch_add_ons_with(ctx, move |container| {
let handler = BbRethEngineApiHandler { pending, engine: engine_handle };
let bb_module = BbRethEngineApiServer::into_rpc(handler);
container.auth_module.replace_auth_methods(bb_module.remove_context())?;
Ok(())
})
.await
}
}
impl<N> RethRpcAddOns<N> for BbAddOns
where
N: FullNodeComponents<
Types: NodeTypes<
ChainSpec: EthereumHardforks + Hardforks + Clone + 'static,
Payload = EthEngineTypes,
Primitives = EthPrimitives,
>,
>,
EthereumEthApiBuilder: reth_node_builder::rpc::EthApiBuilder<N>,
EthereumEngineValidatorBuilder: PayloadValidatorBuilder<N>,
BasicEngineApiBuilder<EthereumEngineValidatorBuilder>: EngineApiBuilder<N>,
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
{
type EthApi = <EthereumEthApiBuilder as reth_node_builder::rpc::EthApiBuilder<N>>::EthApi;
fn hooks_mut(&mut self) -> &mut RpcHooks<N, Self::EthApi> {
unimplemented!("BbAddOns does not support dynamic hook mutation")
}
}
impl<N> EngineValidatorAddOn<N> for BbAddOns
where
N: FullNodeComponents,
BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>: EngineValidatorBuilder<N>,
{
type ValidatorBuilder = BasicEngineValidatorBuilder<EthereumEngineValidatorBuilder>;
fn engine_validator_builder(&self) -> Self::ValidatorBuilder {
BasicEngineValidatorBuilder::default()
}
}
// ---------------------------------------------------------------------------
// Custom executor builder
// ---------------------------------------------------------------------------
/// Executor builder that creates a [`BbEvmConfig`].
#[derive(Debug)]
pub struct BbExecutorBuilder {
pending: BigBlockMap,
}
impl<Node> ExecutorBuilder<Node> for BbExecutorBuilder
where
Node: FullNodeTypes<
Types: NodeTypes<
ChainSpec: reth_ethereum_forks::Hardforks
+ alloy_evm::eth::spec::EthExecutorSpec
+ EthereumHardforks,
Primitives = EthPrimitives,
>,
>,
{
type EVM = BbEvmConfig<<Node::Types as NodeTypes>::ChainSpec>;
async fn build_evm(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::EVM> {
Ok(BbEvmConfig::new(EthEvmConfig::new(ctx.chain_spec()), self.pending))
}
}
// ---------------------------------------------------------------------------
// Node type
// ---------------------------------------------------------------------------
/// Node type for big block execution.
#[derive(Debug, Clone)]
pub struct BbNode {
pending: BigBlockMap,
}
impl BbNode {
const fn new(pending: BigBlockMap) -> Self {
Self { pending }
}
}
impl NodeTypes for BbNode {
type Primitives = EthPrimitives;
type ChainSpec = ChainSpec;
type Storage = EthStorage;
type Payload = EthEngineTypes;
}
impl<N> Node<N> for BbNode
where
N: FullNodeTypes<Types = Self>,
{
type ComponentsBuilder = ComponentsBuilder<
N,
EthereumPoolBuilder,
BasicPayloadServiceBuilder<EthereumPayloadBuilder>,
EthereumNetworkBuilder,
BbExecutorBuilder,
BbConsensusBuilder,
>;
type AddOns = BbAddOns;
fn components_builder(&self) -> Self::ComponentsBuilder {
EthereumNode::components()
.executor(BbExecutorBuilder { pending: self.pending.clone() })
.consensus(BbConsensusBuilder)
}
fn add_ons(&self) -> Self::AddOns {
BbAddOns::new(self.pending.clone())
}
}
// ---------------------------------------------------------------------------
// Consensus builder
// ---------------------------------------------------------------------------
/// Consensus builder for big block execution.
#[derive(Debug, Default, Clone, Copy)]
pub struct BbConsensusBuilder;
impl<Node> ConsensusBuilder<Node> for BbConsensusBuilder
where
Node: FullNodeTypes<
Types: NodeTypes<ChainSpec: EthChainSpec + EthereumHardforks, Primitives = EthPrimitives>,
>,
{
type Consensus = Arc<EthBeaconConsensus<<Node::Types as NodeTypes>::ChainSpec>>;
async fn build_consensus(self, ctx: &BuilderContext<Node>) -> eyre::Result<Self::Consensus> {
Ok(Arc::new(
EthBeaconConsensus::new(ctx.chain_spec())
.with_skip_gas_limit_ramp_check(true)
.with_skip_blob_gas_used_check(true)
.with_skip_requests_hash_check(true),
))
}
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
fn main() {
reth_cli_util::sigsegv_handler::install();
if std::env::var_os("RUST_BACKTRACE").is_none() {
unsafe { std::env::set_var("RUST_BACKTRACE", "1") };
}
let pending: BigBlockMap = Arc::new(Mutex::new(HashMap::new()));
if let Err(err) = Cli::<EthereumChainSpecParser>::parse().run(async move |builder, _| {
info!(target: "reth::cli", "Launching big block node");
let handle = builder.launch_node(BbNode::new(pending.clone())).await?;
handle.wait_for_node_exit().await
}) {
eprintln!("Error: {err:?}");
std::process::exit(1);
}
}

View File

@@ -14,9 +14,13 @@ workspace = true
[dependencies]
# reth
reth-chainspec.workspace = true
reth-cli.workspace = true
reth-cli-runner.workspace = true
reth-cli-util.workspace = true
reth-engine-primitives.workspace = true
reth-ethereum-cli.workspace = true
reth-ethereum-primitives.workspace = true
reth-fs-util.workspace = true
reth-node-api.workspace = true
reth-node-core.workspace = true
@@ -26,6 +30,7 @@ reth-rpc-api.workspace = true
reth-tracing.workspace = true
# alloy
alloy-consensus.workspace = true
alloy-eips.workspace = true
alloy-json-rpc.workspace = true
@@ -79,39 +84,54 @@ default = ["jemalloc"]
asm-keccak = [
"reth-node-core/asm-keccak",
"reth-ethereum-cli/asm-keccak",
"alloy-primitives/asm-keccak",
]
jemalloc = [
"reth-cli-util/jemalloc",
"reth-node-core/jemalloc",
"reth-ethereum-cli/jemalloc",
]
jemalloc-prof = [
"reth-cli-util/jemalloc-prof",
"reth-ethereum-cli/jemalloc-prof",
]
tracy-allocator = [
"reth-cli-util/tracy-allocator",
"tracy",
"reth-ethereum-cli/tracy-allocator",
]
jemalloc-prof = ["reth-cli-util/jemalloc-prof"]
tracy-allocator = ["reth-cli-util/tracy-allocator", "tracy"]
tracy = [
"reth-node-core/tracy",
"reth-tracing/tracy",
"reth-ethereum-cli/tracy",
]
min-error-logs = [
"tracing/release_max_level_error",
"reth-node-core/min-error-logs",
"reth-ethereum-cli/min-error-logs",
]
min-warn-logs = [
"tracing/release_max_level_warn",
"reth-node-core/min-warn-logs",
"reth-ethereum-cli/min-warn-logs",
]
min-info-logs = [
"tracing/release_max_level_info",
"reth-node-core/min-info-logs",
"reth-ethereum-cli/min-info-logs",
]
min-debug-logs = [
"tracing/release_max_level_debug",
"reth-node-core/min-debug-logs",
"reth-ethereum-cli/min-debug-logs",
]
min-trace-logs = [
"tracing/release_max_level_trace",
"reth-node-core/min-trace-logs",
"reth-ethereum-cli/min-trace-logs",
]
# no-op feature flag for CI matrices

View File

@@ -52,8 +52,7 @@ impl InnerTransport {
url: Url,
jwt: JwtSecret,
) -> Result<(Self, Claims), AuthenticatedTransportError> {
let mut client_builder =
reqwest::Client::builder().tls_built_in_root_certs(url.scheme() == "https");
let mut client_builder = reqwest::Client::builder();
let mut headers = reqwest::header::HeaderMap::new();
// Add the JWT to the headers if we can decode it.

View File

@@ -9,7 +9,7 @@ use alloy_rpc_client::ClientBuilder;
use alloy_rpc_types_engine::JwtSecret;
use alloy_transport::layers::{RateLimitRetryPolicy, RetryBackoffLayer};
use reqwest::Url;
use reth_node_core::args::BenchmarkArgs;
use reth_node_core::args::{BenchmarkArgs, WaitForPersistence};
use tracing::info;
/// This is intended to be used by benchmarks that replay blocks from an RPC.
@@ -33,8 +33,8 @@ pub(crate) struct BenchContext {
pub(crate) use_reth_namespace: bool,
/// Whether to fetch and replay RLP-encoded blocks.
pub(crate) rlp_blocks: bool,
/// Whether to skip waiting for persistence (pass `wait_for_persistence: false`).
pub(crate) no_wait_for_persistence: bool,
/// Controls when `reth_newPayload` waits for persistence.
pub(crate) wait_for_persistence: WaitForPersistence,
/// Whether to skip waiting for caches (pass `wait_for_caches: false`).
pub(crate) no_wait_for_caches: bool,
}
@@ -166,8 +166,9 @@ impl BenchContext {
let next_block = first_block.header.number + 1;
let rlp_blocks = bench_args.rlp_blocks;
let wait_for_persistence =
bench_args.wait_for_persistence.unwrap_or(WaitForPersistence::Never);
let use_reth_namespace = bench_args.reth_new_payload || rlp_blocks;
let no_wait_for_persistence = bench_args.no_wait_for_persistence;
let no_wait_for_caches = bench_args.no_wait_for_caches;
Ok(Self {
auth_provider,
@@ -177,7 +178,7 @@ impl BenchContext {
is_optimism,
use_reth_namespace,
rlp_blocks,
no_wait_for_persistence,
wait_for_persistence,
no_wait_for_caches,
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,8 @@ mod context;
mod generate_big_block;
pub(crate) mod helpers;
pub use generate_big_block::{
RawTransaction, RpcTransactionSource, TransactionCollector, TransactionSource,
compute_payload_block_hash, BigBlockPayload, RawTransaction, RpcTransactionSource,
TransactionCollector, TransactionSource,
};
pub(crate) mod metrics_scraper;
mod new_payload_fcu;
@@ -50,16 +51,16 @@ pub enum Subcommands {
/// --jwt-secret $(cat ~/.local/share/reth/mainnet/jwt.hex)`
SendPayload(send_payload::Command),
/// Generate a large block by packing transactions from existing blocks.
/// Generate a large block by merging consecutive blocks from an RPC.
///
/// This command fetches transactions from real blocks and packs them into a single
/// block using the `testing_buildBlockV1` RPC endpoint.
/// Fetches N consecutive blocks, takes block 0 as the base payload, concatenates
/// transactions from blocks 1..N-1, and saves the result to disk as a JSON file
/// containing the merged execution data and environment switches at block boundaries.
///
/// Example:
///
/// `reth-bench generate-big-block --rpc-url http://localhost:8545 --engine-rpc-url
/// http://localhost:8551 --jwt-secret ~/.local/share/reth/mainnet/jwt.hex --target-gas
/// 30000000`
/// `reth-bench generate-big-block --rpc-url http://localhost:8545 --from-block 20000000
/// --count 10 --output-dir ./payloads`
GenerateBigBlock(generate_big_block::Command),
/// Replay pre-generated payloads from a directory.

View File

@@ -95,7 +95,7 @@ impl Command {
is_optimism,
use_reth_namespace,
rlp_blocks,
no_wait_for_persistence,
wait_for_persistence,
no_wait_for_caches,
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
@@ -204,7 +204,7 @@ impl Command {
is_optimism,
rlp,
use_reth_namespace,
no_wait_for_persistence,
wait_for_persistence,
no_wait_for_caches,
)?;
let start = Instant::now();

View File

@@ -52,7 +52,7 @@ impl Command {
is_optimism,
use_reth_namespace,
rlp_blocks,
no_wait_for_persistence,
wait_for_persistence,
no_wait_for_caches,
} = BenchContext::new(&self.benchmark, self.rpc_url).await?;
@@ -129,7 +129,7 @@ impl Command {
is_optimism,
rlp,
use_reth_namespace,
no_wait_for_persistence,
wait_for_persistence,
no_wait_for_caches,
)?;

View File

@@ -3,6 +3,7 @@
use crate::{
authenticated_transport::AuthenticatedTransportConnect,
bench::{
generate_big_block::BigBlockPayload,
helpers::parse_duration,
metrics_scraper::MetricsScraper,
output::{
@@ -21,7 +22,9 @@ use alloy_rpc_types_engine::{
use clap::Parser;
use eyre::Context;
use reth_cli_runner::CliContext;
use reth_engine_primitives::BigBlockData;
use reth_node_api::EngineApiMessageVersion;
use reth_node_core::args::WaitForPersistence;
use reth_rpc_api::RethNewPayloadInput;
use std::{
path::PathBuf,
@@ -57,8 +60,7 @@ pub struct Command {
#[arg(long, value_name = "SKIP", default_value = "0")]
skip: usize,
/// Deprecated: gas ramp is no longer needed. Use `--testing.skip-gas-limit-ramp-check`
/// and `--testing.gas-limit` on the reth node instead. This flag is accepted but ignored.
/// Deprecated: gas ramp is no longer needed. This flag is accepted but ignored.
#[arg(long, value_name = "GAS_RAMP_DIR", hide = true)]
gas_ramp_dir: Option<PathBuf>,
@@ -81,12 +83,22 @@ pub struct Command {
#[arg(long, default_value = "false", verbatim_doc_comment)]
reth_new_payload: bool,
/// Skip waiting for in-flight persistence before processing.
/// Control when `reth_newPayload` waits for in-flight persistence.
///
/// Only works with `--reth-new-payload`. When set, passes `wait_for_persistence: false`
/// to the `reth_newPayload` endpoint.
#[arg(long, default_value = "false", verbatim_doc_comment, requires = "reth_new_payload")]
no_wait_for_persistence: bool,
/// Accepts `always` (default — wait on every block), `never`, or a number N
/// to wait every N blocks and skip the rest.
///
/// Requires `--reth-new-payload`.
#[arg(
long = "wait-for-persistence",
value_name = "MODE",
num_args = 0..=1,
default_missing_value = "always",
value_parser = clap::value_parser!(WaitForPersistence),
requires = "reth_new_payload",
verbatim_doc_comment
)]
wait_for_persistence: Option<WaitForPersistence>,
/// Skip waiting for execution cache and sparse trie locks before processing.
///
@@ -108,10 +120,12 @@ pub struct Command {
struct LoadedPayload {
/// The index (from filename).
index: u64,
/// The payload envelope.
envelope: ExecutionPayloadEnvelopeV4,
/// The execution data for the block.
execution_data: ExecutionData,
/// The block hash.
block_hash: B256,
/// Big block data containing environment switches and prior block hashes.
big_block_data: BigBlockData<ExecutionData>,
}
impl Command {
@@ -160,8 +174,7 @@ impl Command {
if self.gas_ramp_dir.is_some() {
warn!(
target: "reth-bench",
"--gas-ramp-dir is deprecated and ignored. Use --testing.skip-gas-limit-ramp-check \
and --testing.gas-limit on the reth node instead."
"--gas-ramp-dir is deprecated and ignored."
);
}
@@ -172,22 +185,33 @@ impl Command {
}
info!(target: "reth-bench", count = payloads.len(), "Loaded main payloads from disk");
// If any payload has env_switches but we're not using reth_newPayload, warn the user
if !self.reth_new_payload {
let has_env_switches =
payloads.iter().any(|p| !p.big_block_data.env_switches.is_empty());
if has_env_switches {
warn!(
target: "reth-bench",
"Payloads contain env_switches but --reth-new-payload is not set. \
env_switches are only supported with reth_newPayload and will be ignored."
);
}
}
let mut parent_hash = initial_parent_hash;
let mut results = Vec::new();
let total_benchmark_duration = Instant::now();
for (i, payload) in payloads.iter().enumerate() {
let envelope = &payload.envelope;
let execution_data = &payload.execution_data;
let block_hash = payload.block_hash;
let execution_payload = &envelope.envelope_inner.execution_payload;
let inner_payload = &execution_payload.payload_inner.payload_inner;
let v1 = execution_data.payload.as_v1();
let gas_used = inner_payload.gas_used;
let gas_limit = inner_payload.gas_limit;
let block_number = inner_payload.block_number;
let transaction_count =
execution_payload.payload_inner.payload_inner.transactions.len() as u64;
let gas_used = v1.gas_used;
let gas_limit = v1.gas_limit;
let block_number = v1.block_number;
let transaction_count = v1.transactions.len() as u64;
debug!(
target: "reth-bench",
@@ -208,34 +232,36 @@ impl Command {
);
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(),
},
),
let big_block_data_param = if payload.big_block_data.env_switches.is_empty() &&
payload.big_block_data.prior_block_hashes.is_empty()
{
None
} else {
Some(payload.big_block_data.clone())
};
let wait_for_persistence = self
.wait_for_persistence
.unwrap_or(WaitForPersistence::Never)
.rpc_value(block_number);
(
None,
serde_json::to_value((
RethNewPayloadInput::ExecutionData(reth_data),
self.no_wait_for_persistence.then_some(false),
RethNewPayloadInput::ExecutionData(execution_data.clone()),
wait_for_persistence,
self.no_wait_for_caches.then_some(false),
big_block_data_param,
))?,
)
} else {
let requests =
execution_data.sidecar.requests().cloned().unwrap_or_default().to_vec();
(
Some(EngineApiMessageVersion::V4),
serde_json::to_value((
execution_payload.clone(),
execution_data.payload.clone(),
Vec::<B256>::new(),
B256::ZERO,
envelope.execution_requests.to_vec(),
requests,
))?,
)
};
@@ -291,6 +317,10 @@ impl Command {
tracing::warn!(target: "reth-bench", %err, block_number, "Failed to scrape metrics");
}
if let Some(wait_time) = self.wait_time {
tokio::time::sleep(wait_time).await;
}
let gas_row =
TotalGasRow { block_number, transaction_count, gas_used, time: current_duration };
results.push((gas_row, combined_result));
@@ -326,28 +356,42 @@ impl Command {
}
/// Load and parse all payload files from the directory.
///
/// Tries to load each file as a [`BigBlockPayload`] first (which includes `env_switches`),
/// falling back to [`ExecutionPayloadEnvelopeV4`] for backwards compatibility.
fn load_payloads(&self) -> eyre::Result<Vec<LoadedPayload>> {
let mut payloads = Vec::new();
// Read directory entries
// Read directory entries — match both legacy "payload_block_*.json" and new
// "big_block_*.json" formats
let entries: Vec<_> = std::fs::read_dir(&self.payload_dir)
.wrap_err_with(|| format!("Failed to read directory {:?}", self.payload_dir))?
.filter_map(|e| e.ok())
.filter(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
e.path().extension().and_then(|s| s.to_str()) == Some("json") &&
e.file_name().to_string_lossy().starts_with("payload_block_")
(name_str.starts_with("payload_block_") ||
name_str.starts_with("big_block_"))
})
.collect();
// Parse filenames to get indices and sort
// Parse filenames to get indices and sort.
// Supports "payload_block_N.json" and "big_block_FROM_to_TO.json" naming.
let mut indexed_paths: Vec<(u64, PathBuf)> = entries
.into_iter()
.filter_map(|e| {
let name = e.file_name();
let name_str = name.to_string_lossy();
// Extract index from "payload_NNN.json"
let index_str = name_str.strip_prefix("payload_block_")?.strip_suffix(".json")?;
let index: u64 = index_str.parse().ok()?;
let index = if let Some(rest) = name_str.strip_prefix("payload_block_") {
rest.strip_suffix(".json")?.parse::<u64>().ok()?
} else if let Some(rest) = name_str.strip_prefix("big_block_") {
// "big_block_FROM_to_TO.json" — use FROM as the index
let rest = rest.strip_suffix(".json")?;
rest.split("_to_").next()?.parse::<u64>().ok()?
} else {
return None;
};
Some((index, e.path()))
})
.collect();
@@ -365,21 +409,42 @@ impl Command {
for (index, path) in indexed_paths {
let content = std::fs::read_to_string(&path)
.wrap_err_with(|| format!("Failed to read {:?}", path))?;
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
let block_hash =
envelope.envelope_inner.execution_payload.payload_inner.payload_inner.block_hash;
// Try BigBlockPayload first, then fall back to legacy ExecutionPayloadEnvelopeV4
let (execution_data, big_block_data) =
if let Ok(big_block) = serde_json::from_str::<BigBlockPayload>(&content) {
(big_block.execution_data, big_block.big_block_data)
} else {
let envelope: ExecutionPayloadEnvelopeV4 = serde_json::from_str(&content)
.wrap_err_with(|| format!("Failed to parse {:?}", path))?;
let execution_data = ExecutionData {
payload: envelope.envelope_inner.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(),
},
),
};
(execution_data, BigBlockData::default())
};
let block_hash = execution_data.payload.as_v1().block_hash;
debug!(
target: "reth-bench",
index = index,
block_hash = %block_hash,
env_switches = big_block_data.env_switches.len(),
prior_block_hashes = big_block_data.prior_block_hashes.len(),
path = %path.display(),
"Loaded payload"
);
payloads.push(LoadedPayload { index, envelope, block_hash });
payloads.push(LoadedPayload { index, execution_data, block_hash, big_block_data });
}
Ok(payloads)

View File

@@ -14,6 +14,9 @@
#[global_allocator]
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
#[cfg(all(feature = "jemalloc", unix))]
use reth_cli_util::allocator::tikv_jemalloc_sys as _;
pub mod authenticated_transport;
pub mod bench;
pub mod bench_mode;

View File

@@ -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_node_core::args::WaitForPersistence;
use reth_rpc_api::RethNewPayloadInput;
use serde::Deserialize;
use std::time::Duration;
@@ -168,22 +169,25 @@ where
///
/// Returns `(version, versioned_params, execution_data)`.
///
/// When `no_wait_for_persistence` or `no_wait_for_caches` is `true` and using `reth_newPayload`,
/// passes the corresponding `wait_for_*: false` to skip that wait.
/// `wait_for_persistence` controls how `wait_for_persistence` is passed to
/// `reth_newPayload` on a per-block basis.
pub(crate) fn block_to_new_payload(
block: AnyRpcBlock,
is_optimism: bool,
rlp: Option<Bytes>,
reth_new_payload: bool,
no_wait_for_persistence: bool,
wait_for_persistence: WaitForPersistence,
no_wait_for_caches: bool,
) -> eyre::Result<(Option<EngineApiMessageVersion>, serde_json::Value)> {
let block_number = block.header.number;
let wait_for_persistence = wait_for_persistence.rpc_value(block_number);
if let Some(rlp) = rlp {
return Ok((
None,
serde_json::to_value((
RethNewPayloadInput::<ExecutionData>::BlockRlp(rlp),
no_wait_for_persistence.then_some(false),
wait_for_persistence,
no_wait_for_caches.then_some(false),
))?,
));
@@ -207,7 +211,7 @@ pub(crate) fn block_to_new_payload(
None,
serde_json::to_value((
RethNewPayloadInput::ExecutionData(execution_data),
no_wait_for_persistence.then_some(false),
wait_for_persistence,
no_wait_for_caches.then_some(false),
))?,
))

View File

@@ -127,7 +127,7 @@ jemalloc = [
"reth-provider/jemalloc",
]
jemalloc-prof = [
"reth-cli-util/jemalloc",
"jemalloc",
"reth-cli-util/jemalloc-prof",
"reth-ethereum-cli/jemalloc-prof",
"reth-node-metrics/jemalloc-prof",
@@ -136,12 +136,6 @@ jemalloc-symbols = [
"jemalloc-prof",
"reth-ethereum-cli/jemalloc-symbols",
]
jemalloc-unprefixed = [
"reth-cli-util/jemalloc-unprefixed",
"reth-node-core/jemalloc",
"reth-node-metrics/jemalloc",
"reth-ethereum-cli/jemalloc",
]
tracy-allocator = [
"reth-cli-util/tracy-allocator",
"reth-ethereum-cli/tracy-allocator",

View File

@@ -22,7 +22,6 @@
//! and leak detection functionality. See [jemalloc's opt.prof](https://jemalloc.net/jemalloc.3.html#opt.prof)
//! documentation for usage details. This is **not recommended on Windows**.
//! - `jemalloc-symbols`: Enables jemalloc symbols for profiling. Includes `jemalloc-prof`.
//! - `jemalloc-unprefixed`: Uses unprefixed jemalloc symbols.
//! - `tracy-allocator`: Enables [Tracy](https://github.com/wolfpld/tracy) profiler allocator
//! integration for memory profiling.
//! - `snmalloc`: Uses [snmalloc](https://github.com/microsoft/snmalloc) as the global allocator.

View File

@@ -3,8 +3,12 @@
#[global_allocator]
static ALLOC: reth_cli_util::allocator::Allocator = reth_cli_util::allocator::new_allocator();
// Required for "override_allocator_on_supported_platforms".
#[cfg(all(feature = "jemalloc", unix))]
use reth_cli_util::allocator::tikv_jemalloc_sys as _;
#[cfg(all(feature = "jemalloc-prof", unix))]
#[unsafe(export_name = "_rjem_malloc_conf")]
#[unsafe(export_name = "malloc_conf")]
static MALLOC_CONF: &[u8] = b"prof:true,prof_active:true,lg_prof_sample:19\0";
use clap::Parser;

View File

@@ -84,7 +84,7 @@ tracing.workspace = true
backon.workspace = true
secp256k1 = { workspace = true, features = ["global-context", "std", "recovery"] }
tokio-stream.workspace = true
reqwest.workspace = true
reqwest = { workspace = true, features = ["blocking"] }
url.workspace = true
metrics.workspace = true
blake3.workspace = true

View File

@@ -75,10 +75,15 @@ pub struct EnvironmentArgs<C: ChainSpecParser> {
impl<C: ChainSpecParser> EnvironmentArgs<C> {
/// Returns the storage settings for new database initialization.
///
/// Always returns [`StorageSettings::v2()`] — v2 is the default for all new
/// databases. Existing databases use the settings persisted in their metadata.
/// Determined by the `--storage.v2` flag (defaults to `true`).
/// Existing databases retain whatever settings are persisted in their
/// metadata (checked during genesis init).
pub fn storage_settings(&self) -> StorageSettings {
StorageSettings::v2()
if self.storage.v2 {
StorageSettings::v2()
} else {
StorageSettings::v1()
}
}
/// Initializes environment according to [`AccessRights`] and returns an instance of

View File

@@ -8,8 +8,9 @@ use reth_db::{tables, DatabaseEnv};
use reth_db_api::table::Table;
use reth_db_common::DbTool;
use reth_node_builder::NodeTypesWithDBAdapter;
use reth_primitives_traits::FastInstant as Instant;
use reth_provider::RocksDBProviderFactory;
use std::{hash::Hasher, time::Instant};
use std::hash::Hasher;
use tracing::info;
/// RocksDB tables that can be checksummed.

View File

@@ -22,6 +22,7 @@ mod settings;
mod stage_checkpoints;
mod state;
mod static_file_header;
mod static_files;
mod stats;
/// DB List TUI
mod tui;
@@ -63,6 +64,8 @@ pub enum Subcommands {
RepairTrie(repair_trie::Command),
/// Reads and displays the static file segment header
StaticFileHeader(static_file_header::Command),
/// Static file operations (split, etc.)
StaticFiles(static_files::Command),
/// Lists current and local database versions
Version,
/// Returns the full database path
@@ -188,6 +191,11 @@ impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C>
command.execute(&tool)?;
});
}
Subcommands::StaticFiles(command) => {
db_exec!(self.env, tool, N, AccessRights::RO, {
command.execute(&tool)?;
});
}
Subcommands::Version => {
let local_db_version = match get_db_version(&db_path) {
Ok(version) => Some(version),

View File

@@ -0,0 +1,31 @@
//! Static file related CLI commands
mod split;
pub use split::SplitCommand;
use clap::{Parser, Subcommand};
use reth_db_common::DbTool;
use reth_provider::providers::ProviderNodeTypes;
/// Static files subcommands
#[derive(Debug, Parser)]
pub struct Command {
#[command(subcommand)]
command: Subcommands,
}
#[derive(Debug, Subcommand)]
enum Subcommands {
/// Split static files into new files with different blocks-per-file setting
Split(SplitCommand),
}
impl Command {
/// Execute the static files command
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
match self.command {
Subcommands::Split(cmd) => cmd.execute(tool),
}
}
}

View File

@@ -0,0 +1,878 @@
use clap::Parser;
use reth_codecs::Compact;
use reth_db::{
cursor::DbCursorRO,
static_file::{
AccountChangesetMask, BlockHashMask, HeaderMask, ReceiptMask, StorageChangesetMask,
TotalDifficultyMask, TransactionMask, TransactionSenderMask,
},
tables,
transaction::DbTx,
};
use reth_db_api::models::{CompactU256, StoredBlockBodyIndices};
use reth_db_common::DbTool;
use reth_primitives_traits::NodePrimitives;
use reth_provider::{
providers::{ProviderNodeTypes, StaticFileProvider},
DBProvider, StaticFileProviderBuilder, StaticFileProviderFactory, StaticFileWriter,
};
use reth_static_file_types::StaticFileSegment;
use std::{collections::HashMap, path::PathBuf};
use tracing::info;
/// Split static files into new files with different blocks-per-file setting
#[derive(Debug, Parser)]
pub struct SplitCommand {
/// Source static files directory.
/// If not specified, uses the datadir's static_files directory.
#[arg(long, value_name = "PATH")]
static_files_dir: Option<PathBuf>,
/// Output directory for the new static files.
/// Required unless --in-place is specified.
#[arg(long, value_name = "PATH", required_unless_present = "in_place")]
output_dir: Option<PathBuf>,
/// Number of blocks per output file
#[arg(long, value_name = "NUM")]
blocks_per_file: u64,
/// Segments to split (default: all)
#[arg(long, value_delimiter = ',')]
segments: Option<Vec<StaticFileSegment>>,
/// Start block number (default: 0)
#[arg(long)]
from_block: Option<u64>,
/// End block number (default: highest available)
#[arg(long)]
to_block: Option<u64>,
/// Print what would be done without writing
#[arg(long)]
dry_run: bool,
/// Split in-place: write to temp dir, verify, then atomically swap.
/// Original files are preserved in static_files.bak
#[arg(long, conflicts_with = "output_dir")]
in_place: bool,
/// Skip verification step when using --in-place
#[arg(long, requires = "in_place")]
skip_verify: bool,
}
impl SplitCommand {
/// Execute the split command
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()>
where
N::Primitives: NodePrimitives<BlockHeader: Compact, SignedTx: Compact, Receipt: Compact>,
{
let segments = self.segments.clone().unwrap_or_else(|| StaticFileSegment::iter().collect());
// Use custom static files dir if provided, otherwise use datadir's static files
let (source_provider, source_dir) =
if let Some(ref static_files_dir) = self.static_files_dir {
let provider = StaticFileProviderBuilder::read_only(static_files_dir)
.build::<N::Primitives>()?;
let dir = static_files_dir.clone();
(provider, dir)
} else {
let provider = tool.provider_factory.static_file_provider();
let dir = provider.directory().to_path_buf();
(provider, dir)
};
// Determine output directory
let (output_dir, is_in_place) = if self.in_place {
let temp_dir = source_dir.with_file_name("static_files.tmp");
(temp_dir, true)
} else {
(self.output_dir.clone().expect("output_dir required when not in_place"), false)
};
info!(
target: "reth::cli",
output_dir = %output_dir.display(),
blocks_per_file = self.blocks_per_file,
?segments,
from_block = ?self.from_block,
to_block = ?self.to_block,
dry_run = self.dry_run,
in_place = is_in_place,
"Splitting static files"
);
if self.dry_run {
println!("Dry run mode - no files will be written");
if is_in_place {
println!("In-place mode:");
println!(" 1. Write to: {}", output_dir.display());
println!(" 2. Verify output integrity");
println!(" 3. Rename {} -> {}.bak", source_dir.display(), source_dir.display());
println!(" 4. Rename {} -> {}", output_dir.display(), source_dir.display());
}
for segment in &segments {
let min_block = source_provider.get_lowest_range_start(*segment);
let max_block = source_provider.get_highest_static_file_block(*segment);
if let (Some(min_block), Some(max_block)) = (min_block, max_block) {
let from_block = self.from_block.unwrap_or(min_block).max(min_block);
let to_block = self.to_block.unwrap_or(max_block).min(max_block);
let num_blocks = to_block.saturating_sub(from_block) + 1;
let num_files = num_blocks.div_ceil(self.blocks_per_file);
println!(
" {segment}: blocks {from_block}..={to_block} ({num_blocks} blocks) -> {num_files} files"
);
} else {
println!(" {segment}: no data available");
}
}
return Ok(());
}
// Clean up output directory if it exists
// For in-place mode: remove previous incomplete temp directory
// For regular mode: ensure we start fresh to avoid block number mismatches
if output_dir.exists() {
info!(target: "reth::cli", output_dir = %output_dir.display(), "Removing existing output directory");
reth_fs_util::remove_dir_all(&output_dir)?;
}
reth_fs_util::create_dir_all(&output_dir)?;
// Calculate segment ranges first to determine the global starting block
let mut segment_ranges = Vec::new();
for &segment in &segments {
let Some(min_block) = source_provider.get_lowest_range_start(segment) else {
continue;
};
let Some(max_block) = source_provider.get_highest_static_file_block(segment) else {
continue;
};
let from_block = self.from_block.unwrap_or(min_block).max(min_block);
let to_block = self.to_block.unwrap_or(max_block).min(max_block);
if from_block <= to_block {
segment_ranges.push((segment, from_block, to_block));
}
}
// Pre-load block body indices for segments that need them (transactions, receipts,
// transaction senders). This avoids holding a long-lived DB read transaction open and
// is much faster than seeking per-block since the entire table is small.
let needs_indices = segment_ranges.iter().any(|(seg, _, _)| {
matches!(
seg,
StaticFileSegment::Transactions |
StaticFileSegment::Receipts |
StaticFileSegment::TransactionSenders
)
});
let block_body_indices = if needs_indices {
let global_from = segment_ranges
.iter()
.filter(|(seg, _, _)| {
matches!(
seg,
StaticFileSegment::Transactions |
StaticFileSegment::Receipts |
StaticFileSegment::TransactionSenders
)
})
.map(|(_, from, _)| *from)
.min()
.unwrap();
let global_to = segment_ranges
.iter()
.filter(|(seg, _, _)| {
matches!(
seg,
StaticFileSegment::Transactions |
StaticFileSegment::Receipts |
StaticFileSegment::TransactionSenders
)
})
.map(|(_, _, to)| *to)
.max()
.unwrap();
info!(target: "reth::cli", from_block = global_from, to_block = global_to, "Loading block body indices");
Self::load_block_body_indices(tool, global_from, global_to)?
} else {
HashMap::new()
};
for (segment, from_block, to_block) in segment_ranges {
info!(target: "reth::cli", ?segment, from_block, to_block, "Processing segment");
// Build output provider per-segment with genesis_block_number set to this segment's
// starting block. This prevents the writer from trying to load non-existent previous
// files when segments have different starting blocks (e.g., pruned transactions).
let output_provider = StaticFileProviderBuilder::read_write(&output_dir)
.with_blocks_per_file(self.blocks_per_file)
.with_genesis_block_number(from_block)
.build::<N::Primitives>()?;
match segment {
StaticFileSegment::Headers => {
self.split_headers::<N>(
&source_provider,
&output_provider,
from_block,
to_block,
)?;
}
StaticFileSegment::Transactions => {
self.split_transactions::<N>(
&block_body_indices,
&source_provider,
&output_provider,
from_block,
to_block,
)?;
}
StaticFileSegment::Receipts => {
self.split_receipts::<N>(
&block_body_indices,
&source_provider,
&output_provider,
from_block,
to_block,
)?;
}
StaticFileSegment::TransactionSenders => {
self.split_transaction_senders::<N>(
&block_body_indices,
&source_provider,
&output_provider,
from_block,
to_block,
)?;
}
StaticFileSegment::AccountChangeSets => {
self.split_account_changesets::<N>(
&source_provider,
&output_provider,
from_block,
to_block,
)?;
}
StaticFileSegment::StorageChangeSets => {
self.split_storage_changesets::<N>(
&source_provider,
&output_provider,
from_block,
to_block,
)?;
}
}
info!(target: "reth::cli", ?segment, "Segment complete");
// Drop the output provider to release file handles before processing next segment
drop(output_provider);
}
// In-place mode: verify and swap directories
if is_in_place {
// Verification step
if !self.skip_verify {
info!(target: "reth::cli", "Verifying output integrity");
self.verify_output::<N>(&output_dir, &segments)?;
}
// Atomic swap
let backup_dir = source_dir.with_file_name("static_files.bak");
// Remove old backup if exists
if backup_dir.exists() {
info!(target: "reth::cli", backup_dir = %backup_dir.display(), "Removing old backup");
reth_fs_util::remove_dir_all(&backup_dir)?;
}
// Drop source provider to release file handles
drop(source_provider);
// Rename: source -> backup
info!(target: "reth::cli",
from = %source_dir.display(),
to = %backup_dir.display(),
"Moving original to backup"
);
reth_fs_util::rename(&source_dir, &backup_dir)?;
// Rename: temp -> source
info!(target: "reth::cli",
from = %output_dir.display(),
to = %source_dir.display(),
"Moving new files into place"
);
reth_fs_util::rename(&output_dir, &source_dir)?;
info!(target: "reth::cli",
backup = %backup_dir.display(),
"In-place split complete. Original files preserved in backup directory"
);
}
info!(target: "reth::cli", "Static file split complete");
Ok(())
}
/// Verify the output static files have valid data
fn verify_output<N: ProviderNodeTypes>(
&self,
output_dir: &PathBuf,
segments: &[StaticFileSegment],
) -> eyre::Result<()> {
let provider = StaticFileProviderBuilder::read_only(output_dir).build::<N::Primitives>()?;
for &segment in segments {
let Some(lowest) = provider.get_lowest_range_start(segment) else {
return Err(eyre::eyre!("Verification failed: no data for segment {segment}"));
};
let Some(highest) = provider.get_highest_static_file_block(segment) else {
return Err(eyre::eyre!("Verification failed: no data for segment {segment}"));
};
// Verify we can read the first and last blocks
provider.get_segment_provider(segment, lowest)?;
provider.get_segment_provider(segment, highest)?;
info!(target: "reth::cli", ?segment, from_block = lowest, to_block = highest, "Verified");
}
Ok(())
}
fn split_headers<N: ProviderNodeTypes>(
&self,
source: &StaticFileProvider<N::Primitives>,
output: &StaticFileProvider<N::Primitives>,
from_block: u64,
to_block: u64,
) -> eyre::Result<()>
where
<N::Primitives as NodePrimitives>::BlockHeader: Compact,
{
let mut writer = output.get_writer(from_block, StaticFileSegment::Headers)?;
for block in from_block..=to_block {
let jar = source.get_segment_provider(StaticFileSegment::Headers, block)?;
let mut cursor = jar.cursor()?;
let header: <N::Primitives as NodePrimitives>::BlockHeader = cursor
.get_one::<HeaderMask<_>>(block.into())?
.ok_or_else(|| eyre::eyre!("Missing header for block {block}"))?;
let td: CompactU256 = cursor
.get_one::<TotalDifficultyMask>(block.into())?
.ok_or_else(|| eyre::eyre!("Missing TD for block {block}"))?;
let hash = cursor
.get_one::<BlockHashMask>(block.into())?
.ok_or_else(|| eyre::eyre!("Missing hash for block {block}"))?;
writer.append_header_with_td(&header, td.into(), &hash)?;
if block % 100_000 == 0 {
info!(target: "reth::cli", block, to_block, "Headers progress");
}
}
writer.commit()?;
Ok(())
}
fn load_block_body_indices<N: ProviderNodeTypes>(
tool: &DbTool<N>,
from_block: u64,
to_block: u64,
) -> eyre::Result<HashMap<u64, StoredBlockBodyIndices>> {
let provider = tool.provider_factory.provider()?.disable_long_read_transaction_safety();
let tx = provider.tx_ref();
let mut cursor = tx.cursor_read::<tables::BlockBodyIndices>()?;
let mut indices = HashMap::with_capacity((to_block - from_block + 1) as usize);
for entry in cursor.walk_range(from_block..=to_block)? {
let (block, body_indices) = entry?;
indices.insert(block, body_indices);
}
info!(target: "reth::cli", count = indices.len(), "Loaded block body indices");
Ok(indices)
}
fn split_transactions<N: ProviderNodeTypes>(
&self,
block_body_indices: &HashMap<u64, StoredBlockBodyIndices>,
source: &StaticFileProvider<N::Primitives>,
output: &StaticFileProvider<N::Primitives>,
from_block: u64,
to_block: u64,
) -> eyre::Result<()>
where
<N::Primitives as NodePrimitives>::SignedTx: Compact,
{
let mut writer = output.get_writer(from_block, StaticFileSegment::Transactions)?;
let mut block = from_block;
let mut block_incremented = false;
while block <= to_block {
if !block_incremented {
writer.increment_block(block)?;
}
block_incremented = false;
// Skip blocks with no transactions until we find one that needs a jar
let Some(indices) =
block_body_indices.get(&block).filter(|i| i.tx_count > 0)
else {
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Transactions progress");
}
block += 1;
continue;
};
// Open jar + cursor, reuse for all subsequent blocks within this jar's range
let jar =
source.get_segment_provider(StaticFileSegment::Transactions, indices.first_tx_num)?;
let jar_tx_end =
jar.user_header().tx_range().map(|r| r.end()).unwrap_or(u64::MAX);
let mut cursor = jar.cursor()?;
loop {
if let Some(indices) = block_body_indices.get(&block) {
for tx_num in indices.first_tx_num..indices.first_tx_num + indices.tx_count {
let transaction: <N::Primitives as NodePrimitives>::SignedTx = cursor
.get_one::<TransactionMask<_>>(tx_num.into())?
.ok_or_else(|| eyre::eyre!("Missing transaction {tx_num}"))?;
writer.append_transaction(tx_num, &transaction)?;
}
}
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Transactions progress");
}
block += 1;
if block > to_block {
break;
}
writer.increment_block(block)?;
block_incremented = true;
// Check if next block's txs need a different jar
if let Some(next_indices) = block_body_indices.get(&block) &&
next_indices.tx_count > 0 && next_indices.first_tx_num > jar_tx_end
{
break;
}
}
}
writer.commit()?;
Ok(())
}
fn split_receipts<N: ProviderNodeTypes>(
&self,
block_body_indices: &HashMap<u64, StoredBlockBodyIndices>,
source: &StaticFileProvider<N::Primitives>,
output: &StaticFileProvider<N::Primitives>,
from_block: u64,
to_block: u64,
) -> eyre::Result<()>
where
<N::Primitives as NodePrimitives>::Receipt: Compact,
{
let mut writer = output.get_writer(from_block, StaticFileSegment::Receipts)?;
let mut block = from_block;
let mut block_incremented = false;
while block <= to_block {
if !block_incremented {
writer.increment_block(block)?;
}
block_incremented = false;
let Some(indices) =
block_body_indices.get(&block).filter(|i| i.tx_count > 0)
else {
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Receipts progress");
}
block += 1;
continue;
};
let jar =
source.get_segment_provider(StaticFileSegment::Receipts, indices.first_tx_num)?;
let jar_tx_end =
jar.user_header().tx_range().map(|r| r.end()).unwrap_or(u64::MAX);
let mut cursor = jar.cursor()?;
loop {
if let Some(indices) = block_body_indices.get(&block) {
for tx_num in indices.first_tx_num..indices.first_tx_num + indices.tx_count {
let receipt: <N::Primitives as NodePrimitives>::Receipt = cursor
.get_one::<ReceiptMask<_>>(tx_num.into())?
.ok_or_else(|| eyre::eyre!("Missing receipt {tx_num}"))?;
writer.append_receipt(tx_num, &receipt)?;
}
}
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Receipts progress");
}
block += 1;
if block > to_block {
break;
}
writer.increment_block(block)?;
block_incremented = true;
if let Some(next_indices) = block_body_indices.get(&block) &&
next_indices.tx_count > 0 && next_indices.first_tx_num > jar_tx_end
{
break;
}
}
}
writer.commit()?;
Ok(())
}
fn split_transaction_senders<N: ProviderNodeTypes>(
&self,
block_body_indices: &HashMap<u64, StoredBlockBodyIndices>,
source: &StaticFileProvider<N::Primitives>,
output: &StaticFileProvider<N::Primitives>,
from_block: u64,
to_block: u64,
) -> eyre::Result<()> {
let mut writer = output.get_writer(from_block, StaticFileSegment::TransactionSenders)?;
let mut block = from_block;
let mut block_incremented = false;
while block <= to_block {
if !block_incremented {
writer.increment_block(block)?;
}
block_incremented = false;
let Some(indices) =
block_body_indices.get(&block).filter(|i| i.tx_count > 0)
else {
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Transaction senders progress");
}
block += 1;
continue;
};
let jar = source
.get_segment_provider(StaticFileSegment::TransactionSenders, indices.first_tx_num)?;
let jar_tx_end =
jar.user_header().tx_range().map(|r| r.end()).unwrap_or(u64::MAX);
let mut cursor = jar.cursor()?;
loop {
if let Some(indices) = block_body_indices.get(&block) {
for tx_num in indices.first_tx_num..indices.first_tx_num + indices.tx_count {
let sender = cursor
.get_one::<TransactionSenderMask>(tx_num.into())?
.ok_or_else(|| eyre::eyre!("Missing sender {tx_num}"))?;
writer.append_transaction_sender(tx_num, &sender)?;
}
}
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Transaction senders progress");
}
block += 1;
if block > to_block {
break;
}
writer.increment_block(block)?;
block_incremented = true;
if let Some(next_indices) = block_body_indices.get(&block) &&
next_indices.tx_count > 0 && next_indices.first_tx_num > jar_tx_end
{
break;
}
}
}
writer.commit()?;
Ok(())
}
fn split_account_changesets<N: ProviderNodeTypes>(
&self,
source: &StaticFileProvider<N::Primitives>,
output: &StaticFileProvider<N::Primitives>,
from_block: u64,
to_block: u64,
) -> eyre::Result<()> {
let mut writer = output.get_writer(from_block, StaticFileSegment::AccountChangeSets)?;
let mut block = from_block;
while block <= to_block {
// Open jar + cursor, reuse for all blocks within this jar's range
let jar =
source.get_segment_provider(StaticFileSegment::AccountChangeSets, block)?;
let jar_block_end = jar
.user_header()
.block_range()
.map(|r| r.end())
.unwrap_or(u64::MAX);
let mut cursor = jar.cursor()?;
loop {
let mut changes = Vec::new();
if let Some(offset) = jar.read_changeset_offset(block)? {
for i in offset.changeset_range() {
if let Some(change) =
cursor.get_one::<AccountChangesetMask>(i.into())?
{
changes.push(change);
}
}
}
writer.append_account_changeset(changes, block)?;
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Account changesets progress");
}
block += 1;
if block > to_block || block > jar_block_end {
break;
}
}
}
writer.commit()?;
Ok(())
}
fn split_storage_changesets<N: ProviderNodeTypes>(
&self,
source: &StaticFileProvider<N::Primitives>,
output: &StaticFileProvider<N::Primitives>,
from_block: u64,
to_block: u64,
) -> eyre::Result<()> {
let mut writer = output.get_writer(from_block, StaticFileSegment::StorageChangeSets)?;
let mut block = from_block;
while block <= to_block {
let jar =
source.get_segment_provider(StaticFileSegment::StorageChangeSets, block)?;
let jar_block_end = jar
.user_header()
.block_range()
.map(|r| r.end())
.unwrap_or(u64::MAX);
let mut cursor = jar.cursor()?;
loop {
let mut changes = Vec::new();
if let Some(offset) = jar.read_changeset_offset(block)? {
for i in offset.changeset_range() {
if let Some(change) =
cursor.get_one::<StorageChangesetMask>(i.into())?
{
changes.push(change);
}
}
}
writer.append_storage_changeset(changes, block)?;
if block.is_multiple_of(100_000) {
info!(target: "reth::cli", block, to_block, "Storage changesets progress");
}
block += 1;
if block > to_block || block > jar_block_end {
break;
}
}
}
writer.commit()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[derive(Parser)]
struct TestCli {
#[command(subcommand)]
command: TestCommand,
}
#[derive(clap::Subcommand)]
enum TestCommand {
Split(SplitCommand),
}
#[test]
fn parse_split_command_minimal() {
let args = TestCli::try_parse_from([
"test",
"split",
"--output-dir",
"/tmp/output",
"--blocks-per-file",
"100000",
])
.unwrap();
match args.command {
TestCommand::Split(cmd) => {
assert_eq!(cmd.output_dir, Some(PathBuf::from("/tmp/output")));
assert_eq!(cmd.blocks_per_file, 100000);
assert!(cmd.segments.is_none());
assert!(cmd.from_block.is_none());
assert!(cmd.to_block.is_none());
assert!(!cmd.dry_run);
assert!(!cmd.in_place);
}
}
}
#[test]
fn parse_split_command_full() {
let args = TestCli::try_parse_from([
"test",
"split",
"--output-dir",
"/tmp/output",
"--blocks-per-file",
"50000",
"--segments",
"headers,receipts",
"--from-block",
"1000",
"--to-block",
"500000",
"--dry-run",
])
.unwrap();
match args.command {
TestCommand::Split(cmd) => {
assert_eq!(cmd.output_dir, Some(PathBuf::from("/tmp/output")));
assert_eq!(cmd.blocks_per_file, 50000);
assert_eq!(
cmd.segments,
Some(vec![StaticFileSegment::Headers, StaticFileSegment::Receipts])
);
assert_eq!(cmd.from_block, Some(1000));
assert_eq!(cmd.to_block, Some(500000));
assert!(cmd.dry_run);
assert!(!cmd.in_place);
}
}
}
#[test]
fn parse_split_command_in_place() {
let args =
TestCli::try_parse_from(["test", "split", "--in-place", "--blocks-per-file", "100000"])
.unwrap();
match args.command {
TestCommand::Split(cmd) => {
assert!(cmd.output_dir.is_none());
assert_eq!(cmd.blocks_per_file, 100000);
assert!(cmd.in_place);
assert!(!cmd.skip_verify);
}
}
}
#[test]
fn parse_split_command_in_place_skip_verify() {
let args = TestCli::try_parse_from([
"test",
"split",
"--in-place",
"--skip-verify",
"--blocks-per-file",
"100000",
])
.unwrap();
match args.command {
TestCommand::Split(cmd) => {
assert!(cmd.in_place);
assert!(cmd.skip_verify);
}
}
}
#[test]
fn parse_split_command_output_dir_conflicts_with_in_place() {
let result = TestCli::try_parse_from([
"test",
"split",
"--output-dir",
"/tmp/out",
"--in-place",
"--blocks-per-file",
"100000",
]);
assert!(result.is_err());
}
#[test]
fn parse_split_command_skip_verify_requires_in_place() {
// --skip-verify without --in-place should fail
let result = TestCli::try_parse_from([
"test",
"split",
"--skip-verify",
"--blocks-per-file",
"100000",
]);
assert!(result.is_err(), "--skip-verify should require --in-place");
}
#[test]
fn parse_split_command_all_segments() {
let args = TestCli::try_parse_from([
"test",
"split",
"--output-dir",
"/tmp/out",
"--blocks-per-file",
"10",
"--segments",
"headers,transactions,receipts,transaction-senders,account-change-sets,storage-change-sets",
])
.unwrap();
match args.command {
TestCommand::Split(cmd) => {
let segments = cmd.segments.unwrap();
assert_eq!(segments.len(), 6);
assert!(segments.contains(&StaticFileSegment::Headers));
assert!(segments.contains(&StaticFileSegment::Transactions));
assert!(segments.contains(&StaticFileSegment::Receipts));
assert!(segments.contains(&StaticFileSegment::TransactionSenders));
assert!(segments.contains(&StaticFileSegment::AccountChangeSets));
assert!(segments.contains(&StaticFileSegment::StorageChangeSets));
}
}
}
}

View File

@@ -3,9 +3,10 @@ use clap::Parser;
use eyre::{Result, WrapErr};
use reth_db::{mdbx::DatabaseArguments, open_db_read_only, tables, Database};
use reth_db_api::transaction::DbTx;
use reth_primitives_traits::FastInstant as Instant;
use reth_stages_types::StageId;
use reth_static_file_types::DEFAULT_BLOCKS_PER_STATIC_FILE;
use std::{path::PathBuf, time::Instant};
use std::path::PathBuf;
use tracing::{info, warn};
/// Generate modular chunk archives and a snapshot manifest from a source datadir.

View File

@@ -84,6 +84,10 @@ pub struct DownloadDefaults {
///
/// Falls back to [`default_base_url`](Self::default_base_url) when `None`.
pub default_chain_aware_base_url: Option<Cow<'static, str>>,
/// URL for the snapshot discovery API that lists available snapshots.
///
/// Defaults to `https://snapshots.reth.rs/api/snapshots`.
pub snapshot_api_url: Cow<'static, str>,
/// Optional custom long help text that overrides the generated help
pub long_help: Option<String>,
}
@@ -108,6 +112,7 @@ impl DownloadDefaults {
],
default_base_url: Cow::Borrowed(RETH_SNAPSHOTS_BASE_URL),
default_chain_aware_base_url: None,
snapshot_api_url: Cow::Borrowed(RETH_SNAPSHOTS_API_URL),
long_help: None,
}
}
@@ -121,10 +126,11 @@ impl DownloadDefaults {
return custom_help.clone();
}
let mut help = String::from(
let mut help = format!(
"Specify a snapshot URL or let the command propose a default one.\n\n\
Browse available snapshots at https://snapshots.reth.rs\n\
Browse available snapshots at {}\n\
or use --list-snapshots to see them from the CLI.\n\nAvailable snapshot sources:\n",
self.snapshot_api_url.trim_end_matches("/api/snapshots"),
);
for source in &self.available_snapshots {
@@ -169,6 +175,12 @@ impl DownloadDefaults {
self
}
/// Set the snapshot discovery API URL.
pub fn with_snapshot_api_url(mut self, url: impl Into<Cow<'static, str>>) -> Self {
self.snapshot_api_url = url.into();
self
}
/// Builder: Set custom long help text, overriding the generated help
pub fn with_long_help(mut self, help: impl Into<String>) -> Self {
self.long_help = Some(help.into());
@@ -191,7 +203,7 @@ pub struct DownloadCommand<C: ChainSpecParser> {
/// Custom URL to download a single snapshot archive (legacy mode).
///
/// When provided, downloads and extracts a single archive without component selection.
/// Browse available snapshots at <https://snapshots.reth.rs> or use --list-snapshots.
/// Browse available snapshots with --list-snapshots.
#[arg(long, short, long_help = DownloadDefaults::get_global().long_help())]
url: Option<String>,
@@ -261,7 +273,7 @@ pub struct DownloadCommand<C: ChainSpecParser> {
#[arg(long, default_value_t = MAX_CONCURRENT_DOWNLOADS)]
download_concurrency: usize,
/// List available snapshots from snapshots.reth.rs and exit.
/// List available snapshots and exit.
///
/// Queries the snapshots API and prints all available snapshots for the selected chain,
/// including block number, size, and manifest URL.
@@ -1328,7 +1340,17 @@ fn streaming_download_and_extract(
let response = match client.get(url).send().and_then(|r| r.error_for_status()) {
Ok(r) => r,
Err(e) => {
last_error = Some(e.into());
let err = eyre::Error::from(e);
if attempt < MAX_DOWNLOAD_RETRIES {
warn!(target: "reth::cli",
url = %url,
attempt,
max = MAX_DOWNLOAD_RETRIES,
err = %err,
"Streaming request failed, retrying"
);
}
last_error = Some(err);
if attempt < MAX_DOWNLOAD_RETRIES {
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
}
@@ -1355,6 +1377,15 @@ fn streaming_download_and_extract(
match result {
Ok(()) => return Ok(()),
Err(e) => {
if attempt < MAX_DOWNLOAD_RETRIES {
warn!(target: "reth::cli",
url = %url,
attempt,
max = MAX_DOWNLOAD_RETRIES,
err = %e,
"Streaming extraction failed, retrying"
);
}
last_error = Some(e);
if attempt < MAX_DOWNLOAD_RETRIES {
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
@@ -1520,6 +1551,7 @@ fn blocking_process_modular_archive(
}
let format = CompressionFormat::from_url(&archive.file_name)?;
let mut last_error: Option<eyre::Error> = None;
for attempt in 1..=MAX_DOWNLOAD_RETRIES {
cleanup_output_files(target_dir, &archive.output_files);
@@ -1527,13 +1559,31 @@ fn blocking_process_modular_archive(
let cache_dir = cache_dir.ok_or_else(|| eyre::eyre!("Missing cache directory"))?;
let archive_path = cache_dir.join(&archive.file_name);
let part_path = cache_dir.join(format!("{}.part", archive.file_name));
let (downloaded_path, _downloaded_size) =
resumable_download(&archive.url, cache_dir, shared.as_ref(), cancel_token.clone())?;
let file = fs::open(&downloaded_path)?;
extract_archive_raw(file, format, target_dir)?;
let result =
resumable_download(&archive.url, cache_dir, shared.as_ref(), cancel_token.clone())
.and_then(|(downloaded_path, _)| {
let file = fs::open(&downloaded_path)?;
extract_archive_raw(file, format, target_dir)
});
let _ = fs::remove_file(&archive_path);
let _ = fs::remove_file(&part_path);
if let Err(e) = result {
warn!(target: "reth::cli",
file = %archive.file_name,
component = %planned.component,
attempt,
err = %e,
"Download or extraction failed, retrying"
);
last_error = Some(e);
if attempt < MAX_DOWNLOAD_RETRIES {
std::thread::sleep(Duration::from_secs(RETRY_BACKOFF_SECS));
}
continue;
}
} else {
// streaming_download_and_extract already has its own internal retry loop
streaming_download_and_extract(
&archive.url,
format,
@@ -1553,6 +1603,13 @@ fn blocking_process_modular_archive(
warn!(target: "reth::cli", file = %archive.file_name, component = %planned.component, attempt, "Extracted files failed integrity checks, retrying");
}
if let Some(e) = last_error {
return Err(e.wrap_err(format!(
"Failed after {} attempts for {}",
MAX_DOWNLOAD_RETRIES, archive.file_name
)));
}
eyre::bail!(
"Failed integrity validation after {} attempts for {}",
MAX_DOWNLOAD_RETRIES,
@@ -1608,10 +1665,11 @@ fn file_blake3_hex(path: &Path) -> Result<String> {
/// Discovers the latest snapshot manifest URL for the given chain from the snapshots API.
///
/// Queries `snapshots.reth.rs/api/snapshots` and returns the manifest URL for the most
/// Queries the configured snapshot API and returns the manifest URL for the most
/// recent modular snapshot matching the requested chain.
async fn discover_manifest_url(chain_id: u64) -> Result<String> {
let api_url = RETH_SNAPSHOTS_API_URL;
let defaults = DownloadDefaults::get_global();
let api_url = &*defaults.snapshot_api_url;
info!(target: "reth::cli", %api_url, %chain_id, "Discovering latest snapshot manifest");
@@ -1624,8 +1682,9 @@ async fn discover_manifest_url(chain_id: u64) -> Result<String> {
{chain_id} at {api_url}\n\n\
You can provide a manifest URL directly with --manifest-url, or\n\
use a direct snapshot URL with -u from:\n\
\t- https://snapshots.reth.rs\n\n\
Use --list to see all available snapshots."
\t- {}\n\n\
Use --list to see all available snapshots.",
api_url.trim_end_matches("/api/snapshots"),
)
})?;
@@ -1656,7 +1715,7 @@ where
}
}
/// An entry from the `snapshots.reth.rs/api/snapshots` listing.
/// An entry from the snapshot discovery API listing.
#[derive(serde::Deserialize)]
#[serde(rename_all = "camelCase")]
struct SnapshotApiEntry {
@@ -1681,7 +1740,7 @@ impl SnapshotApiEntry {
/// Fetches the full snapshot listing from the snapshots API, filtered by chain ID.
async fn fetch_snapshot_api_entries(chain_id: u64) -> Result<Vec<SnapshotApiEntry>> {
let api_url = RETH_SNAPSHOTS_API_URL;
let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
let entries: Vec<SnapshotApiEntry> = Client::new()
.get(api_url)
@@ -1699,7 +1758,11 @@ async fn fetch_snapshot_api_entries(chain_id: u64) -> Result<Vec<SnapshotApiEntr
fn print_snapshot_listing(entries: &[SnapshotApiEntry], chain_id: u64) {
let modular: Vec<_> = entries.iter().filter(|e| e.is_modular()).collect();
println!("Available snapshots for chain {chain_id} (https://snapshots.reth.rs):\n");
let api_url = &*DownloadDefaults::get_global().snapshot_api_url;
println!(
"Available snapshots for chain {chain_id} ({}):\n",
api_url.trim_end_matches("/api/snapshots"),
);
println!("{:<12} {:>10} {:<10} {:>10} MANIFEST URL", "DATE", "BLOCK", "PROFILE", "SIZE");
println!("{}", "-".repeat(100));
@@ -1738,14 +1801,18 @@ async fn fetch_manifest_from_source(source: &str) -> Result<SnapshotManifest> {
.await
.and_then(|r| r.error_for_status())
.wrap_err_with(|| {
let sources = DownloadDefaults::get_global()
.available_snapshots
.iter()
.map(|s| format!("\t- {s}"))
.collect::<Vec<_>>()
.join("\n");
format!(
"Failed to fetch snapshot manifest from {source}\n\n\
The manifest endpoint may not be available for this snapshot source.\n\
You can use a direct snapshot URL instead:\n\n\
\treth download -u <snapshot-url>\n\n\
Available snapshot sources:\n\
\t- https://snapshots.reth.rs\n\
\t- https://publicnode.com/snapshots"
Available snapshot sources:\n{sources}"
)
})?;
Ok(response.json().await?)

View File

@@ -178,6 +178,8 @@ where
ext,
} = self;
engine.validate()?;
// set up node config
let mut node_config = NodeConfig {
datadir,

View File

@@ -6,7 +6,7 @@ use reth_db_api::{
};
use reth_db_common::DbTool;
use reth_evm::ConfigureEvm;
use reth_node_builder::NodeTypesWithDB;
use reth_node_api::HeaderTy;
use reth_node_core::dirs::{ChainPath, DataDirPath};
use reth_provider::{
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
@@ -58,7 +58,7 @@ where
}
/// Imports all the tables that can be copied over a range.
fn import_tables_with_range<N: NodeTypesWithDB>(
fn import_tables_with_range<N: ProviderNodeTypes>(
output_db: &DatabaseEnv,
db_tool: &DbTool<N>,
from: u64,
@@ -74,7 +74,7 @@ fn import_tables_with_range<N: NodeTypesWithDB>(
)
})??;
output_db.update(|tx| {
tx.import_table_with_range::<tables::Headers, _>(
tx.import_table_with_range::<tables::Headers<HeaderTy<N>>, _>(
&db_tool.provider_factory.db_ref().tx()?,
Some(from),
to,

View File

@@ -10,6 +10,7 @@ use reth_db_api::{database::Database, models::BlockNumberAddress, table::TableIm
use reth_db_common::DbTool;
use reth_evm::ConfigureEvm;
use reth_exex::ExExManagerHandle;
use reth_node_api::HeaderTy;
use reth_node_core::dirs::{ChainPath, DataDirPath};
use reth_provider::{
providers::{ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
@@ -41,7 +42,7 @@ where
let (output_db, tip_block_number) = setup(from, to, &output_datadir.db(), db_tool)?;
output_db.update(|tx| {
tx.import_table_with_range::<tables::Headers, _>(
tx.import_table_with_range::<tables::Headers<HeaderTy<N>>, _>(
&db_tool.provider_factory.db_ref().tx()?,
Some(from),
to,

View File

@@ -28,6 +28,7 @@ use reth_node_metrics::{
server::{MetricServer, MetricServerConfig},
version::VersionInfo,
};
use reth_primitives_traits::FastInstant as Instant;
use reth_provider::{
ChainSpecProvider, DBProvider, DatabaseProviderFactory, StageCheckpointReader,
StageCheckpointWriter,
@@ -40,7 +41,7 @@ use reth_stages::{
},
ExecInput, ExecOutput, ExecutionStageThresholds, Stage, StageExt, UnwindInput, UnwindOutput,
};
use std::{any::Any, net::SocketAddr, sync::Arc, time::Instant};
use std::{any::Any, net::SocketAddr, sync::Arc};
use tokio::sync::watch;
use tracing::*;

View File

@@ -33,19 +33,21 @@ reth-tracing = { workspace = true, optional = true }
rand.workspace = true
[target.'cfg(unix)'.dependencies]
tikv-jemalloc-sys = { workspace = true, optional = true }
tikv-jemallocator = { workspace = true, optional = true }
snmalloc-rs = { workspace = true, optional = true }
libc = "0.2"
[features]
jemalloc = ["dep:tikv-jemallocator"]
jemalloc = [
"dep:tikv-jemallocator",
"dep:tikv-jemalloc-sys",
"tikv-jemallocator?/override_allocator_on_supported_platforms",
]
# Enables jemalloc profiling features
jemalloc-prof = ["jemalloc", "tikv-jemallocator?/profiling"]
# Enables unprefixed malloc (reproducible builds support)
jemalloc-unprefixed = ["jemalloc", "tikv-jemallocator?/unprefixed_malloc_on_supported_platforms"]
# Wraps the selected allocator in the tracy profiling allocator
tracy-allocator = ["dep:tracy-client", "dep:reth-tracing"]

View File

@@ -15,6 +15,11 @@ cfg_if::cfg_if! {
}
}
// Re-export jemalloc-sys so that binaries can `use` it in main.rs to make it
// visible to the linker, which is required for `override_allocator_on_supported_platforms`.
#[cfg(all(feature = "jemalloc", unix))]
pub use tikv_jemalloc_sys;
// This is to prevent clippy unused warnings when we do `--all-features`
cfg_if::cfg_if! {
if #[cfg(all(feature = "snmalloc", feature = "jemalloc", unix))] {

View File

@@ -10,6 +10,8 @@
#[cfg(feature = "tracy-allocator")]
use reth_tracing as _;
#[cfg(feature = "tracy-allocator")]
use tracy_client as _;
pub mod allocator;
pub mod cancellation;

View File

@@ -1,4 +1,23 @@
//! Consensus protocol functions
//!
//! # Trait hierarchy
//!
//! Consensus validation is split across three traits, each adding a layer:
//!
//! - [`HeaderValidator`] — validates a header in isolation and against its parent. Used early in
//! the validation pipeline before block execution.
//!
//! - [`Consensus`] — extends `HeaderValidator` with block body validation. Checks that the body
//! matches the header (tx root, ommer hash, withdrawals) and runs pre-execution checks. Used
//! before a block is executed.
//!
//! - [`FullConsensus`] — extends `Consensus` with post-execution validation. Checks execution
//! results against the header (gas used, receipt root, logs bloom). Used after block execution to
//! verify the outcome.
//!
//! In the engine, these are applied in order during payload validation (`engine_newPayload`).
//! Payload attribute validation for block building (`engine_forkchoiceUpdated`) is handled
//! separately at the engine API layer and does not use these traits.
#![doc(
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",

View File

@@ -29,7 +29,7 @@ auto_impl.workspace = true
derive_more.workspace = true
futures.workspace = true
eyre.workspace = true
reqwest.workspace = true
reqwest = { workspace = true, features = ["query"] }
serde = { workspace = true, features = ["derive"] }
tokio = { workspace = true, features = ["time"] }
serde_json.workspace = true

View File

@@ -1,8 +1,7 @@
use alloy_consensus::Sealable;
use alloy_primitives::B256;
use reth_node_api::{
BuiltPayload, ConsensusEngineHandle, EngineApiMessageVersion, ExecutionPayload, NodePrimitives,
PayloadTypes,
BuiltPayload, ConsensusEngineHandle, ExecutionPayload, NodePrimitives, PayloadTypes,
};
use reth_primitives_traits::{Block, SealedBlock};
use reth_tracing::tracing::warn;
@@ -131,10 +130,7 @@ where
safe_block_hash,
finalized_block_hash,
};
let _ = self
.engine_handle
.fork_choice_updated(state, None, EngineApiMessageVersion::V3)
.await;
let _ = self.engine_handle.fork_choice_updated(state, None).await;
}
}
}

View File

@@ -79,10 +79,16 @@ where
target: "consensus::debug-client",
%err,
url=%self.url,
"Failed to subscribe to blocks",
"Failed to subscribe to blocks, retrying in 5s",
);
}) else {
return
// Exit if the receiver has been dropped (e.g. during shutdown) so we
// don't keep retrying after the consumer is gone.
if tx.is_closed() {
return;
}
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
continue
};
while let Some(res) = stream.next().await {

View File

@@ -1,5 +1,6 @@
//! Utilities for end-to-end tests.
use alloy_rpc_types_engine::PayloadAttributes;
use node::NodeTestContext;
use reth_chainspec::ChainSpec;
use reth_db::{test_utils::TempDatabase, DatabaseEnv};
@@ -48,7 +49,11 @@ pub async fn setup<N>(
num_nodes: usize,
chain_spec: Arc<N::ChainSpec>,
is_dev: bool,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
+ Send
+ Sync
+ Copy
+ 'static,
) -> eyre::Result<(Vec<NodeHelperType<N>>, Wallet)>
where
N: NodeBuilderHelper,
@@ -65,7 +70,11 @@ pub async fn setup_engine<N>(
chain_spec: Arc<N::ChainSpec>,
is_dev: bool,
tree_config: reth_node_api::TreeConfig,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
+ Send
+ Sync
+ Copy
+ 'static,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
Wallet,
@@ -90,7 +99,11 @@ pub async fn setup_engine_with_connection<N>(
chain_spec: Arc<N::ChainSpec>,
is_dev: bool,
tree_config: reth_node_api::TreeConfig,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes + Send + Sync + Copy + 'static,
attributes_generator: impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
+ Send
+ Sync
+ Copy
+ 'static,
connect_nodes: bool,
) -> eyre::Result<(
Vec<NodeHelperType<N, BlockchainProvider<NodeTypesWithDBAdapter<N, TmpDB>>>>,
@@ -133,11 +146,8 @@ pub type NodeHelperType<N, Provider = BlockchainProvider<NodeTypesWithDBAdapter<
pub trait NodeBuilderHelper
where
Self: Default
+ NodeTypesForProvider<
Payload: PayloadTypes<
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
>,
> + Node<
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
+ Node<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
ComponentsBuilder: NodeComponentsBuilder<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
@@ -158,11 +168,8 @@ where
impl<T> NodeBuilderHelper for T where
Self: Default
+ NodeTypesForProvider<
Payload: PayloadTypes<
PayloadBuilderAttributes: From<reth_payload_builder::EthPayloadBuilderAttributes>,
>,
> + Node<
+ NodeTypesForProvider<Payload: PayloadTypes<PayloadAttributes: From<PayloadAttributes>>>
+ Node<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,
ComponentsBuilder: NodeComponentsBuilder<
TmpNodeAdapter<Self, BlockchainProvider<NodeTypesWithDBAdapter<Self, TmpDB>>>,

View File

@@ -9,13 +9,10 @@ use futures_util::Future;
use jsonrpsee::{core::client::ClientT, http_client::HttpClient};
use reth_chainspec::EthereumHardforks;
use reth_network_api::test_utils::PeersHandleProvider;
use reth_node_api::{
Block, BlockBody, BlockTy, EngineApiMessageVersion, FullNodeComponents, PayloadTypes,
PrimitivesTy,
};
use reth_node_api::{Block, BlockBody, BlockTy, FullNodeComponents, PayloadTypes, PrimitivesTy};
use reth_node_builder::{rpc::RethRpcAddOns, FullNode, NodeTypes};
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes};
use reth_payload_primitives::BuiltPayload;
use reth_provider::{
BlockReader, BlockReaderIdExt, CanonStateNotificationStream, CanonStateSubscriptions,
HeaderProvider, StageCheckpointReader,
@@ -58,7 +55,7 @@ where
/// Creates a new test node
pub async fn new(
node: FullNode<Node, AddOns>,
attributes_generator: impl Fn(u64) -> Payload::PayloadBuilderAttributes + Send + Sync + 'static,
attributes_generator: impl Fn(u64) -> Payload::PayloadAttributes + Send + Sync + 'static,
) -> eyre::Result<Self> {
Ok(Self {
inner: node.clone(),
@@ -106,17 +103,50 @@ where
Ok(chain)
}
/// Returns the current forkchoice state of the node.
pub fn current_forkchoice_state(&self) -> eyre::Result<ForkchoiceState> {
let latest_header =
self.inner.provider.sealed_header_by_number_or_tag(BlockNumberOrTag::Latest)?.unwrap();
if latest_header.number() == 0 {
return Ok(ForkchoiceState::same_hash(latest_header.hash()));
}
Ok(ForkchoiceState {
head_block_hash: latest_header.hash(),
safe_block_hash: self
.inner
.provider
.sealed_header_by_number_or_tag(BlockNumberOrTag::Safe)?
.unwrap()
.hash(),
finalized_block_hash: self
.inner
.provider
.sealed_header_by_number_or_tag(BlockNumberOrTag::Finalized)?
.unwrap()
.hash(),
})
}
/// Creates a new payload from given attributes generator
/// expects a payload attribute event and waits until the payload is built.
///
/// It triggers the resolve payload via engine api and expects the built payload event.
pub async fn new_payload(&mut self) -> eyre::Result<Payload::BuiltPayload> {
// trigger new payload building draining the pool
let eth_attr = self.payload.new_payload().await.unwrap();
let eth_attr = self.payload.next_attributes();
let payload_id = self
.inner
.add_ons_handle
.beacon_engine_handle
.fork_choice_updated(self.current_forkchoice_state()?, Some(eth_attr.clone()))
.await?
.payload_id
.unwrap();
// first event is the payload attributes
self.payload.expect_attr_event(eth_attr.clone()).await?;
self.payload.expect_attr_event(eth_attr).await?;
// wait for the payload builder to have finished building
self.payload.wait_for_built_payload(eth_attr.payload_id()).await;
self.payload.wait_for_built_payload(payload_id).await;
// ensure we're also receiving the built payload as event
Ok(self.payload.expect_built_payload().await?)
}
@@ -265,7 +295,6 @@ where
finalized_block_hash: current_head,
},
None,
EngineApiMessageVersion::default(),
)
.await?;

View File

@@ -1,8 +1,8 @@
use futures_util::StreamExt;
use reth_node_api::{BlockBody, PayloadKind};
use reth_node_api::{BlockBody, PayloadAttributes, PayloadKind};
use reth_payload_builder::{PayloadBuilderHandle, PayloadId};
use reth_payload_builder_primitives::Events;
use reth_payload_primitives::{BuiltPayload, PayloadBuilderAttributes, PayloadTypes};
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
use tokio_stream::wrappers::BroadcastStream;
/// Helper for payload operations
@@ -12,14 +12,14 @@ pub struct PayloadTestContext<T: PayloadTypes> {
payload_builder: PayloadBuilderHandle<T>,
pub timestamp: u64,
#[debug(skip)]
attributes_generator: Box<dyn Fn(u64) -> T::PayloadBuilderAttributes + Send + Sync>,
attributes_generator: Box<dyn Fn(u64) -> T::PayloadAttributes + Send + Sync>,
}
impl<T: PayloadTypes> PayloadTestContext<T> {
/// Creates a new payload helper
pub async fn new(
payload_builder: PayloadBuilderHandle<T>,
attributes_generator: impl Fn(u64) -> T::PayloadBuilderAttributes + Send + Sync + 'static,
attributes_generator: impl Fn(u64) -> T::PayloadAttributes + Send + Sync + 'static,
) -> eyre::Result<Self> {
let payload_events = payload_builder.subscribe().await?;
let payload_event_stream = payload_events.into_stream();
@@ -32,19 +32,14 @@ impl<T: PayloadTypes> PayloadTestContext<T> {
})
}
/// Creates a new payload job from static attributes
pub async fn new_payload(&mut self) -> eyre::Result<T::PayloadBuilderAttributes> {
/// Generates the next payload attributes
pub fn next_attributes(&mut self) -> T::PayloadAttributes {
self.timestamp += 1;
let attributes = (self.attributes_generator)(self.timestamp);
self.payload_builder.send_new_payload(attributes.clone()).await.unwrap()?;
Ok(attributes)
(self.attributes_generator)(self.timestamp)
}
/// Asserts that the next event is a payload attributes event
pub async fn expect_attr_event(
&mut self,
attrs: T::PayloadBuilderAttributes,
) -> eyre::Result<()> {
pub async fn expect_attr_event(&mut self, attrs: T::PayloadAttributes) -> eyre::Result<()> {
let first_event = self.payload_event_stream.next().await.unwrap()?;
if let Events::Attributes(attr) = first_event {
assert_eq!(attrs.timestamp(), attr.timestamp());

View File

@@ -33,7 +33,7 @@ type NodeConfigModifier<C> = Box<dyn Fn(NodeConfig<C>) -> NodeConfig<C> + Send +
pub struct E2ETestSetupBuilder<N, F>
where
N: NodeBuilderHelper,
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
+ Send
+ Sync
+ Copy
@@ -50,7 +50,7 @@ where
impl<N, F> E2ETestSetupBuilder<N, F>
where
N: NodeBuilderHelper,
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
+ Send
+ Sync
+ Copy
@@ -207,7 +207,7 @@ where
impl<N, F> std::fmt::Debug for E2ETestSetupBuilder<N, F>
where
N: NodeBuilderHelper,
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
F: Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes
+ Send
+ Sync
+ Copy

View File

@@ -1,6 +1,7 @@
//! Setup utilities for importing RLP chain data before starting nodes.
use crate::{node::NodeTestContext, NodeHelperType, Wallet};
use alloy_rpc_types_engine::PayloadAttributes;
use reth_chainspec::ChainSpec;
use reth_cli_commands::import_core::{import_blocks_from_file, ImportConfig};
use reth_config::Config;
@@ -59,11 +60,7 @@ pub async fn setup_engine_with_chain_import(
is_dev: bool,
tree_config: TreeConfig,
rlp_path: &Path,
attributes_generator: impl Fn(u64) -> reth_payload_builder::EthPayloadBuilderAttributes
+ Send
+ Sync
+ Copy
+ 'static,
attributes_generator: impl Fn(u64) -> PayloadAttributes + Send + Sync + Copy + 'static,
) -> eyre::Result<ChainImportResult> {
let runtime = reth_tasks::Runtime::test();
@@ -273,10 +270,10 @@ pub fn load_forkchoice_state(path: &Path) -> eyre::Result<alloy_rpc_types_engine
mod tests {
use super::*;
use crate::test_rlp_utils::{create_fcu_json, generate_test_blocks, write_blocks_to_rlp};
use alloy_rpc_types_engine::PayloadAttributes;
use reth_chainspec::{ChainSpecBuilder, MAINNET};
use reth_db::mdbx::DatabaseArguments;
use reth_ethereum_primitives::Block;
use reth_payload_builder::EthPayloadBuilderAttributes;
use reth_primitives_traits::SealedBlock;
use reth_provider::{
test_utils::MockNodeTypesWithDB, BlockHashReader, BlockNumReader, BlockReaderIdExt,
@@ -569,7 +566,7 @@ mod tests {
false,
TreeConfig::default(),
&rlp_path,
|_| EthPayloadBuilderAttributes::default(),
|_| PayloadAttributes::default(),
)
.await
.expect("Failed to setup nodes with chain import");

View File

@@ -10,7 +10,6 @@ use reth_ethereum_primitives::Block;
use reth_network_p2p::sync::{NetworkSyncUpdater, SyncState};
use reth_node_api::{EngineTypes, NodeTypes, PayloadTypes, TreeConfig};
use reth_node_core::primitives::RecoveredBlock;
use reth_payload_builder::EthPayloadBuilderAttributes;
use revm::state::EvmState;
use std::{marker::PhantomData, path::Path, sync::Arc};
use tokio::{
@@ -264,15 +263,12 @@ where
let chain_spec =
self.chain_spec.clone().ok_or_else(|| eyre!("Chain specification is required"))?;
let attributes_generator = move |timestamp| {
let attributes = PayloadAttributes {
timestamp,
prev_randao: B256::ZERO,
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
};
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
let attributes_generator = move |timestamp| PayloadAttributes {
timestamp,
prev_randao: B256::ZERO,
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
};
crate::setup_import::setup_engine_with_chain_import(
@@ -288,23 +284,19 @@ where
/// Create a static attributes generator that doesn't capture any instance data
fn create_static_attributes_generator<N>(
) -> impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes
+ Copy
+ use<N, I>
) -> impl Fn(u64) -> <<N as NodeTypes>::Payload as PayloadTypes>::PayloadAttributes + Copy + use<N, I>
where
N: NodeBuilderHelper<Payload = I>,
{
move |timestamp| {
let attributes = PayloadAttributes {
PayloadAttributes {
timestamp,
prev_randao: B256::ZERO,
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
};
<<N as NodeTypes>::Payload as PayloadTypes>::PayloadBuilderAttributes::from(
EthPayloadBuilderAttributes::new(B256::ZERO, attributes),
)
}
.into()
}
}

View File

@@ -19,7 +19,6 @@ use reth_e2e_test_utils::{
};
use reth_node_api::TreeConfig;
use reth_node_ethereum::{EthEngineTypes, EthereumNode};
use reth_payload_builder::EthPayloadBuilderAttributes;
use std::sync::Arc;
use tempfile::TempDir;
use tracing::debug;
@@ -371,7 +370,7 @@ async fn test_setup_builder_with_custom_tree_config() -> Result<()> {
);
let (nodes, _wallet) = E2ETestSetupBuilder::<EthereumNode, _>::new(1, chain_spec, |_| {
EthPayloadBuilderAttributes::default()
PayloadAttributes::default()
})
.with_tree_config_modifier(|config| {
config.with_persistence_threshold(0).with_memory_block_buffer_target(5)

View File

@@ -3,6 +3,7 @@
use alloy_consensus::BlockHeader;
use alloy_eips::eip2718::Encodable2718;
use alloy_primitives::{Address, Bytes, TxKind, B256, U256};
use alloy_rpc_types_engine::PayloadAttributes;
use alloy_rpc_types_eth::{Transaction, TransactionInput, TransactionReceipt, TransactionRequest};
use eyre::Result;
use jsonrpsee::core::client::ClientT;
@@ -10,7 +11,6 @@ use reth_chainspec::{ChainSpec, ChainSpecBuilder, MAINNET};
use reth_db::tables;
use reth_e2e_test_utils::{transaction::TransactionTestContext, wallet, E2ETestSetupBuilder};
use reth_node_ethereum::EthereumNode;
use reth_payload_builder::EthPayloadBuilderAttributes;
use reth_provider::RocksDBProviderFactory;
use std::{sync::Arc, time::Duration};
@@ -83,15 +83,14 @@ fn test_chain_spec() -> Arc<ChainSpec> {
}
/// Returns test payload attributes for the given timestamp.
fn test_attributes_generator(timestamp: u64) -> EthPayloadBuilderAttributes {
let attributes = alloy_rpc_types_engine::PayloadAttributes {
const fn test_attributes_generator(timestamp: u64) -> PayloadAttributes {
PayloadAttributes {
timestamp,
prev_randao: B256::ZERO,
suggested_fee_recipient: alloy_primitives::Address::ZERO,
withdrawals: Some(vec![]),
parent_beacon_block_root: Some(B256::ZERO),
};
EthPayloadBuilderAttributes::new(B256::ZERO, attributes)
}
}
/// Smoke test: node boots with `RocksDB` routing enabled.

View File

@@ -0,0 +1,39 @@
[package]
name = "reth-execution-cache"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true
description = "Cross-block execution cache for payload processing"
[lints]
workspace = true
[dependencies]
alloy-primitives.workspace = true
fixed-cache = { workspace = true, features = ["stats"] }
metrics.workspace = true
parking_lot.workspace = true
reth-errors.workspace = true
reth-metrics = { workspace = true, features = ["common"] }
reth-primitives-traits = { workspace = true, features = ["std"] }
reth-provider.workspace = true
reth-revm.workspace = true
reth-trie.workspace = true
tracing.workspace = true
[dev-dependencies]
alloy-primitives = { workspace = true, features = ["rand"] }
reth-provider = { workspace = true, features = ["test-utils"] }
reth-revm = { workspace = true, features = ["test-utils"] }
revm-state.workspace = true
[features]
test-utils = [
"reth-primitives-traits/test-utils",
"reth-revm/test-utils",
"reth-provider/test-utils",
"reth-trie/test-utils",
]

View File

@@ -87,7 +87,7 @@ type FixedCache<K, V, H = DefaultHashBuilder> = fixed_cache::Cache<K, V, H, Epoc
/// The const generic `PREWARM` controls whether every cache miss is populated. This is only
/// relevant for pre-warm transaction execution with the intention to pre-populate the cache with
/// data for regular block execution. During regular block execution the cache doesn't need to be
/// populated because the actual EVM database [`State`](revm::database::State) also caches
/// populated because the actual EVM database `State` also caches
/// internally during block execution and the cache is then updated after the block with the entire
/// [`BundleState`] output of that block which contains all accessed accounts, code, storage. See
/// also [`ExecutionCache::insert_state`].
@@ -230,7 +230,7 @@ impl CachedStateMetrics {
}
/// Records a new execution cache creation with its duration.
pub(crate) fn record_cache_creation(&self, duration: Duration) {
pub fn record_cache_creation(&self, duration: Duration) {
self.execution_cache_created_total.increment(1);
self.execution_cache_creation_duration_seconds.record(duration.as_secs_f64());
}
@@ -254,51 +254,63 @@ pub struct CacheStats {
}
impl CacheStats {
pub(crate) fn record_account_hit(&self) {
/// Records an account cache hit.
pub fn record_account_hit(&self) {
self.account_hits.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_account_miss(&self) {
/// Records an account cache miss.
pub fn record_account_miss(&self) {
self.account_misses.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn account_hits(&self) -> usize {
/// Returns the number of account cache hits.
pub fn account_hits(&self) -> usize {
self.account_hits.load(Ordering::Relaxed)
}
pub(crate) fn account_misses(&self) -> usize {
/// Returns the number of account cache misses.
pub fn account_misses(&self) -> usize {
self.account_misses.load(Ordering::Relaxed)
}
pub(crate) fn record_storage_hit(&self) {
/// Records a storage cache hit.
pub fn record_storage_hit(&self) {
self.storage_hits.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_storage_miss(&self) {
/// Records a storage cache miss.
pub fn record_storage_miss(&self) {
self.storage_misses.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn storage_hits(&self) -> usize {
/// Returns the number of storage cache hits.
pub fn storage_hits(&self) -> usize {
self.storage_hits.load(Ordering::Relaxed)
}
pub(crate) fn storage_misses(&self) -> usize {
/// Returns the number of storage cache misses.
pub fn storage_misses(&self) -> usize {
self.storage_misses.load(Ordering::Relaxed)
}
pub(crate) fn record_code_hit(&self) {
/// Records a code cache hit.
pub fn record_code_hit(&self) {
self.code_hits.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_code_miss(&self) {
/// Records a code cache miss.
pub fn record_code_miss(&self) {
self.code_misses.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn code_hits(&self) -> usize {
/// Returns the number of code cache hits.
pub fn code_hits(&self) -> usize {
self.code_hits.load(Ordering::Relaxed)
}
pub(crate) fn code_misses(&self) -> usize {
/// Returns the number of code cache misses.
pub fn code_misses(&self) -> usize {
self.code_misses.load(Ordering::Relaxed)
}
}
@@ -318,7 +330,7 @@ impl CacheStats {
///
/// Collisions (evicting a different key) don't change size since they replace an existing entry.
#[derive(Debug)]
pub(crate) struct CacheStatsHandler {
pub struct CacheStatsHandler {
collisions: AtomicU64,
size: AtomicUsize,
capacity: usize,
@@ -326,42 +338,42 @@ pub(crate) struct CacheStatsHandler {
impl CacheStatsHandler {
/// Creates a new stats handler with all counters initialized to zero.
pub(crate) const fn new(capacity: usize) -> Self {
pub const fn new(capacity: usize) -> Self {
Self { collisions: AtomicU64::new(0), size: AtomicUsize::new(0), capacity }
}
/// Returns the number of cache collisions.
pub(crate) fn collisions(&self) -> u64 {
pub fn collisions(&self) -> u64 {
self.collisions.load(Ordering::Relaxed)
}
/// Returns the current size (number of entries).
pub(crate) fn size(&self) -> usize {
pub fn size(&self) -> usize {
self.size.load(Ordering::Relaxed)
}
/// Returns the capacity (maximum number of entries).
pub(crate) const fn capacity(&self) -> usize {
pub const fn capacity(&self) -> usize {
self.capacity
}
/// Increments the size counter. Called on cache insert.
pub(crate) fn increment_size(&self) {
pub fn increment_size(&self) {
let _ = self.size.fetch_add(1, Ordering::Relaxed);
}
/// Decrements the size counter. Called on cache remove.
pub(crate) fn decrement_size(&self) {
pub fn decrement_size(&self) {
let _ = self.size.fetch_sub(1, Ordering::Relaxed);
}
/// Resets size to zero. Called on cache clear.
pub(crate) fn reset_size(&self) {
pub fn reset_size(&self) {
self.size.store(0, Ordering::Relaxed);
}
/// Resets collision counter to zero (but not size).
pub(crate) fn reset_stats(&self) {
pub fn reset_stats(&self) {
self.collisions.store(0, Ordering::Relaxed);
}
}
@@ -762,12 +774,12 @@ impl ExecutionCache {
}
/// Insert code into cache.
fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
pub fn insert_code(&self, hash: B256, code: Option<Bytecode>) {
self.0.code_cache.insert(hash, code);
}
/// Insert account into cache.
fn insert_account(&self, address: Address, account: Option<Account>) {
pub fn insert_account(&self, address: Address, account: Option<Account>) {
self.0.account_cache.insert(address, account);
}
@@ -840,7 +852,6 @@ impl ExecutionCache {
}
self.0.account_cache.remove(addr);
self.0.account_stats.decrement_size();
continue;
}
@@ -869,7 +880,7 @@ impl ExecutionCache {
///
/// We do not clear the bytecodes cache, because its mapping can never change, as it's
/// `keccak256(bytecode) => bytecode`.
pub(crate) fn clear(&self) {
pub fn clear(&self) {
self.0.storage_cache.clear();
self.0.account_cache.clear();
@@ -879,7 +890,7 @@ impl ExecutionCache {
/// Updates the provided metrics with the current stats from the cache's stats handlers,
/// and resets the hit/miss/collision counters.
pub(crate) fn update_metrics(&self, metrics: &CachedStateMetrics) {
pub fn update_metrics(&self, metrics: &CachedStateMetrics) {
metrics.code_cache_size.set(self.0.code_stats.size() as f64);
metrics.code_cache_capacity.set(self.0.code_stats.capacity() as f64);
metrics.code_cache_collisions.set(self.0.code_stats.collisions() as f64);
@@ -964,7 +975,7 @@ impl SavedCache {
///
/// Note: This can be expensive with large cached state. Use
/// `with_disable_cache_metrics(true)` to skip.
pub(crate) fn update_metrics(&self) {
pub fn update_metrics(&self) {
if self.disable_cache_metrics {
return
}
@@ -973,15 +984,16 @@ impl SavedCache {
/// Clears all caches, resetting them to empty state,
/// and updates the hash of the block this cache belongs to.
pub(crate) fn clear_with_hash(&mut self, hash: B256) {
pub fn clear_with_hash(&mut self, hash: B256) {
self.hash = hash;
self.caches.clear();
}
}
#[cfg(test)]
#[cfg(any(test, feature = "test-utils"))]
impl SavedCache {
fn clone_guard_for_test(&self) -> Arc<()> {
/// Clones the usage guard for testing availability tracking.
pub fn clone_guard_for_test(&self) -> Arc<()> {
self.usage_guard.clone()
}
}
@@ -1222,6 +1234,33 @@ mod tests {
assert!(caches.0.account_cache.get(&addr2).is_some());
}
#[test]
fn test_insert_state_destroyed_uncached_account_keeps_size_zero() {
let caches = ExecutionCache::new(1000);
assert_eq!(caches.0.account_stats.size(), 0);
let addr = Address::random();
let bundle = BundleState {
state: HashMap::from_iter([(
addr,
BundleAccount::new(
None, // No original info
None, // Destroyed
Default::default(),
AccountStatus::Destroyed,
),
)]),
contracts: Default::default(),
reverts: Default::default(),
state_size: 0,
reverts_size: 0,
};
assert!(caches.insert_state(&bundle).is_ok());
assert_eq!(caches.0.account_stats.size(), 0);
assert!(caches.0.account_cache.get(&addr).is_none());
}
#[test]
fn test_code_cache_capacity_with_default_budget() {
// Default cross-block cache is 4 GB; code gets 5.56% = ~228 MB.

View File

@@ -0,0 +1,227 @@
//! Cross-block execution cache for payload processing.
//!
//! This crate provides the core caching infrastructure used during block execution:
//! - [`ExecutionCache`]: Fixed-size concurrent caches for accounts, storage, and bytecode
//! - [`SavedCache`]: An execution cache snapshot associated with a specific block hash
//! - [`PayloadExecutionCache`]: Thread-safe wrapper for sharing cached state across payload
//! processing tasks
#![doc(
html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png",
html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256",
issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/"
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(not(test), warn(unused_crate_dependencies))]
mod cached_state;
pub use cached_state::*;
use alloy_primitives::B256;
use metrics::{Counter, Histogram};
use parking_lot::RwLock;
use reth_metrics::Metrics;
use reth_primitives_traits::FastInstant as Instant;
use std::{sync::Arc, time::Duration};
use tracing::{debug, instrument, warn};
/// A guarded, thread-safe cache of execution state that tracks the most recent block's caches.
///
/// This is the cross-block cache used to accelerate sequential payload processing.
/// When a new block arrives, its parent's cached state can be reused to avoid
/// redundant database lookups.
///
/// This process assumes that payloads are received sequentially.
///
/// ## Cache Safety
///
/// **CRITICAL**: Cache update operations require exclusive access. All concurrent cache users
/// (such as prewarming tasks) must be terminated before calling
/// [`PayloadExecutionCache::update_with_guard`], otherwise the cache may be corrupted or cleared.
#[derive(Clone, Debug, Default)]
pub struct PayloadExecutionCache {
/// Guarded cloneable cache identified by a block hash.
inner: Arc<RwLock<Option<SavedCache>>>,
/// Metrics for cache operations.
metrics: PayloadExecutionCacheMetrics,
}
impl PayloadExecutionCache {
/// Returns the cache for `parent_hash` if it's available for use.
///
/// A cache is considered available when:
/// - It exists and matches the requested parent hash
/// - No other tasks are currently using it (checked via Arc reference count)
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
pub fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
let start = Instant::now();
let mut cache = self.inner.write();
let elapsed = start.elapsed();
self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
if elapsed.as_millis() > 5 {
warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
}
if let Some(c) = cache.as_mut() {
let cached_hash = c.executed_block_hash();
// Check that the cache hash matches the parent hash of the current block. It won't
// match in case it's a fork block.
let hash_matches = cached_hash == parent_hash;
// Check `is_available()` to ensure no other tasks (e.g., prewarming) currently hold
// a reference to this cache. We can only reuse it when we have exclusive access.
let available = c.is_available();
let usage_count = c.usage_count();
debug!(
target: "engine::caching",
%cached_hash,
%parent_hash,
hash_matches,
available,
usage_count,
"Existing cache found"
);
if available {
if !hash_matches {
// Fork block: clear and update the hash on the ORIGINAL before cloning.
// This prevents the canonical chain from matching on the stale hash
// and picking up polluted data if the fork block fails.
c.clear_with_hash(parent_hash);
}
return Some(c.clone())
} else if hash_matches {
self.metrics.execution_cache_in_use.increment(1);
}
} else {
debug!(target: "engine::caching", %parent_hash, "No cache found");
}
None
}
/// Waits until the execution cache becomes available for use.
///
/// This acquires a write lock to ensure exclusive access, then immediately releases it.
/// This is useful for synchronization before starting payload processing.
///
/// Returns the time spent waiting for the lock.
pub fn wait_for_availability(&self) -> Duration {
let start = Instant::now();
// Acquire write lock to wait for any current holders to finish
let _guard = self.inner.write();
let elapsed = start.elapsed();
if elapsed.as_millis() > 5 {
debug!(
target: "engine::tree::payload_processor",
blocked_for=?elapsed,
"Waited for execution cache to become available"
);
}
elapsed
}
/// Updates the cache with a closure that has exclusive access to the guard.
/// This ensures that all cache operations happen atomically.
///
/// ## CRITICAL SAFETY REQUIREMENT
///
/// **Before calling this method, you MUST ensure there are no other active cache users.**
/// This includes:
/// - No running prewarming task instances that could write to the cache
/// - No concurrent transactions that might access the cached state
/// - All prewarming operations must be completed or cancelled
///
/// Violating this requirement can result in cache corruption, incorrect state data,
/// and potential consensus failures.
pub fn update_with_guard<F>(&self, update_fn: F)
where
F: FnOnce(&mut Option<SavedCache>),
{
let mut guard = self.inner.write();
update_fn(&mut guard);
}
}
/// Metrics for [`PayloadExecutionCache`] operations.
#[derive(Metrics, Clone)]
#[metrics(scope = "consensus.engine.beacon")]
struct PayloadExecutionCacheMetrics {
/// Counter for when the execution cache was unavailable because other threads
/// (e.g., prewarming) are still using it.
execution_cache_in_use: Counter,
/// Time spent waiting for execution cache mutex to become available.
execution_cache_wait_duration: Histogram,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_checkout_blocks_second() {
let cache = PayloadExecutionCache::default();
let hash = B256::from([1u8; 32]);
cache.update_with_guard(|slot| {
*slot = Some(SavedCache::new(
hash,
ExecutionCache::new(1_000),
CachedStateMetrics::zeroed(),
))
});
let first = cache.get_cache_for(hash);
assert!(first.is_some());
let second = cache.get_cache_for(hash);
assert!(second.is_none());
}
#[test]
fn checkout_available_after_drop() {
let cache = PayloadExecutionCache::default();
let hash = B256::from([2u8; 32]);
cache.update_with_guard(|slot| {
*slot = Some(SavedCache::new(
hash,
ExecutionCache::new(1_000),
CachedStateMetrics::zeroed(),
))
});
let checked_out = cache.get_cache_for(hash);
assert!(checked_out.is_some());
drop(checked_out);
let second = cache.get_cache_for(hash);
assert!(second.is_some());
}
#[test]
fn hash_mismatch_clears_and_retags() {
let cache = PayloadExecutionCache::default();
let hash_a = B256::from([0xAA; 32]);
let hash_b = B256::from([0xBB; 32]);
cache.update_with_guard(|slot| {
*slot = Some(SavedCache::new(
hash_a,
ExecutionCache::new(1_000),
CachedStateMetrics::zeroed(),
))
});
let checked_out = cache.get_cache_for(hash_b);
assert!(checked_out.is_some());
assert_eq!(checked_out.unwrap().executed_block_hash(), hash_b);
}
#[test]
fn empty_cache_returns_none() {
let cache = PayloadExecutionCache::default();
assert!(cache.get_cache_for(B256::ZERO).is_none());
}
}

View File

@@ -32,14 +32,6 @@ futures-util.workspace = true
# misc
eyre.workspace = true
tracing.workspace = true
op-alloy-rpc-types-engine = { workspace = true, optional = true }
[lints]
workspace = true
[features]
op = [
"dep:op-alloy-rpc-types-engine",
"reth-payload-primitives/op",
"reth-primitives-traits/op",
]

View File

@@ -6,9 +6,7 @@ use eyre::OptionExt;
use futures_util::{stream::Fuse, Stream, StreamExt};
use reth_engine_primitives::ConsensusEngineHandle;
use reth_payload_builder::PayloadBuilderHandle;
use reth_payload_primitives::{
BuiltPayload, EngineApiMessageVersion, PayloadAttributesBuilder, PayloadKind, PayloadTypes,
};
use reth_payload_primitives::{BuiltPayload, PayloadAttributesBuilder, PayloadKind, PayloadTypes};
use reth_primitives_traits::{HeaderTy, SealedHeaderFor};
use reth_storage_api::BlockReader;
use reth_transaction_pool::TransactionPool;
@@ -214,10 +212,7 @@ where
/// Sends a FCU to the engine.
async fn update_forkchoice_state(&self) -> eyre::Result<()> {
let state = self.forkchoice_state();
let res = self
.to_engine
.fork_choice_updated(state, None, EngineApiMessageVersion::default())
.await?;
let res = self.to_engine.fork_choice_updated(state, None).await?;
if !res.is_valid() {
eyre::bail!("Invalid fork choice update {state:?}: {res:?}")
@@ -234,7 +229,6 @@ where
.fork_choice_updated(
self.forkchoice_state(),
Some(self.payload_attributes_builder.build(&self.last_header)),
EngineApiMessageVersion::default(),
)
.await?;

View File

@@ -60,56 +60,3 @@ where
}
}
}
#[cfg(feature = "op")]
impl<ChainSpec>
PayloadAttributesBuilder<op_alloy_rpc_types_engine::OpPayloadAttributes, ChainSpec::Header>
for LocalPayloadAttributesBuilder<ChainSpec>
where
ChainSpec: EthChainSpec + EthereumHardforks + 'static,
{
fn build(
&self,
parent: &SealedHeader<ChainSpec::Header>,
) -> op_alloy_rpc_types_engine::OpPayloadAttributes {
use alloy_primitives::B64;
use reth_chainspec::BaseFeeParams;
use std::env;
/// Dummy system transaction for dev mode.
/// OP Mainnet transaction at index 0 in block 124665056.
///
/// <https://optimistic.etherscan.io/tx/0x312e290cf36df704a2217b015d6455396830b0ce678b860ebfcc30f41403d7b1>
const TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056: [u8; 251] = alloy_primitives::hex!(
"7ef8f8a0683079df94aa5b9cf86687d739a60a9b4f0835e520ec4d664e2e415dca17a6df94deaddeaddeaddeaddeaddeaddeaddeaddead00019442000000000000000000000000000000000000158080830f424080b8a4440a5e200000146b000f79c500000000000000040000000066d052e700000000013ad8a3000000000000000000000000000000000000000000000000000000003ef1278700000000000000000000000000000000000000000000000000000000000000012fdf87b89884a61e74b322bbcf60386f543bfae7827725efaaf0ab1de2294a590000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f32985"
);
// Configure EIP-1559 parameters for dev mode. These can be overridden via environment
// variables (OP_DEV_EIP1559_DENOMINATOR, OP_DEV_EIP1559_ELASTICITY, OP_DEV_GAS_LIMIT),
// otherwise defaults from Optimism's BaseFeeParams are used. The parameters are encoded
// as an 8-byte value (denominator + elasticity) required by Optimism's Jovian fork.
let default_eip_1559_params = BaseFeeParams::optimism();
let denominator = env::var("OP_DEV_EIP1559_DENOMINATOR")
.ok()
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(default_eip_1559_params.max_change_denominator as u32);
let elasticity = env::var("OP_DEV_EIP1559_ELASTICITY")
.ok()
.and_then(|v| v.parse::<u32>().ok())
.unwrap_or(default_eip_1559_params.elasticity_multiplier as u32);
let gas_limit = env::var("OP_DEV_GAS_LIMIT").ok().and_then(|v| v.parse::<u64>().ok());
let mut eip1559_bytes = [0u8; 8];
eip1559_bytes[0..4].copy_from_slice(&denominator.to_be_bytes());
eip1559_bytes[4..8].copy_from_slice(&elasticity.to_be_bytes());
let eip_1559_params = Some(B64::from(eip1559_bytes));
op_alloy_rpc_types_engine::OpPayloadAttributes {
payload_attributes: self.build(parent),
transactions: Some(vec![TX_SET_L1_BLOCK_OP_MAINNET_BLOCK_124665056.into()]),
no_tx_pool: None,
gas_limit,
eip_1559_params,
min_base_fee: Some(0),
}
}
}

View File

@@ -6,6 +6,9 @@ use core::time::Duration;
/// Triggers persistence when the number of canonical blocks in memory exceeds this threshold.
pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
/// Maximum canonical-minus-persisted gap before engine API processing is stalled.
pub const DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD: u64 = 16;
/// How close to the canonical head we persist blocks.
pub const DEFAULT_MEMORY_BLOCK_BUFFER_TARGET: u64 = 0;
@@ -44,6 +47,16 @@ const DEFAULT_MAX_INVALID_HEADER_CACHE_LENGTH: u32 = 256;
const DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE: usize = 4;
const DEFAULT_CROSS_BLOCK_CACHE_SIZE: usize = default_cross_block_cache_size();
const fn assert_backpressure_threshold_invariant(
persistence_threshold: u64,
persistence_backpressure_threshold: u64,
) {
debug_assert!(
persistence_backpressure_threshold > persistence_threshold,
"persistence_backpressure_threshold must be greater than persistence_threshold",
);
}
const fn default_cross_block_cache_size() -> usize {
if cfg!(test) {
1024 * 1024 // 1 MB in tests
@@ -82,6 +95,8 @@ pub struct TreeConfig {
///
/// Note: this should be less than or equal to `persistence_threshold`.
memory_block_buffer_target: u64,
/// Maximum canonical-minus-persisted gap before engine API processing is stalled.
persistence_backpressure_threshold: u64,
/// Number of pending blocks that cannot be executed due to missing parent and
/// are kept in cache.
block_buffer_limit: u32,
@@ -151,6 +166,10 @@ pub struct TreeConfig {
/// computation is spawned in parallel and whichever finishes first is used.
/// If `None`, the timeout fallback is disabled.
state_root_task_timeout: Option<Duration>,
/// Whether to share execution cache with the payload builder.
share_execution_cache_with_payload_builder: bool,
/// Whether to share sparse trie with the payload builder.
share_sparse_trie_with_payload_builder: bool,
/// Maximum random jitter applied before each proof computation (trie-debug only).
/// When set, each proof worker sleeps for a random duration up to this value
/// before starting a proof calculation.
@@ -160,9 +179,14 @@ pub struct TreeConfig {
impl Default for TreeConfig {
fn default() -> Self {
assert_backpressure_threshold_invariant(
DEFAULT_PERSISTENCE_THRESHOLD,
DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
);
Self {
persistence_threshold: DEFAULT_PERSISTENCE_THRESHOLD,
memory_block_buffer_target: DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
persistence_backpressure_threshold: DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
block_buffer_limit: DEFAULT_BLOCK_BUFFER_LIMIT,
max_invalid_header_cache_length: DEFAULT_MAX_INVALID_HEADER_CACHE_LENGTH,
max_execute_block_batch_size: DEFAULT_MAX_EXECUTE_BLOCK_BATCH_SIZE,
@@ -186,6 +210,8 @@ impl Default for TreeConfig {
slow_block_threshold: None,
disable_sparse_trie_cache_pruning: false,
state_root_task_timeout: Some(DEFAULT_STATE_ROOT_TASK_TIMEOUT),
share_execution_cache_with_payload_builder: false,
share_sparse_trie_with_payload_builder: false,
#[cfg(feature = "trie-debug")]
proof_jitter: None,
}
@@ -198,6 +224,7 @@ impl TreeConfig {
pub const fn new(
persistence_threshold: u64,
memory_block_buffer_target: u64,
persistence_backpressure_threshold: u64,
block_buffer_limit: u32,
max_invalid_header_cache_length: u32,
max_execute_block_batch_size: usize,
@@ -220,10 +247,17 @@ impl TreeConfig {
sparse_trie_max_hot_accounts: usize,
slow_block_threshold: Option<Duration>,
state_root_task_timeout: Option<Duration>,
share_execution_cache_with_payload_builder: bool,
share_sparse_trie_with_payload_builder: bool,
) -> Self {
assert_backpressure_threshold_invariant(
persistence_threshold,
persistence_backpressure_threshold,
);
Self {
persistence_threshold,
memory_block_buffer_target,
persistence_backpressure_threshold,
block_buffer_limit,
max_invalid_header_cache_length,
max_execute_block_batch_size,
@@ -247,6 +281,8 @@ impl TreeConfig {
slow_block_threshold,
disable_sparse_trie_cache_pruning: false,
state_root_task_timeout,
share_execution_cache_with_payload_builder,
share_sparse_trie_with_payload_builder,
#[cfg(feature = "trie-debug")]
proof_jitter: None,
}
@@ -262,6 +298,11 @@ impl TreeConfig {
self.memory_block_buffer_target
}
/// Return the persistence backpressure threshold.
pub const fn persistence_backpressure_threshold(&self) -> u64 {
self.persistence_backpressure_threshold
}
/// Return the block buffer limit.
pub const fn block_buffer_limit(&self) -> u32 {
self.block_buffer_limit
@@ -358,6 +399,10 @@ impl TreeConfig {
/// Setter for persistence threshold.
pub const fn with_persistence_threshold(mut self, persistence_threshold: u64) -> Self {
self.persistence_threshold = persistence_threshold;
assert_backpressure_threshold_invariant(
self.persistence_threshold,
self.persistence_backpressure_threshold,
);
self
}
@@ -370,6 +415,19 @@ impl TreeConfig {
self
}
/// Setter for persistence backpressure threshold.
pub const fn with_persistence_backpressure_threshold(
mut self,
persistence_backpressure_threshold: u64,
) -> Self {
self.persistence_backpressure_threshold = persistence_backpressure_threshold;
assert_backpressure_threshold_invariant(
self.persistence_threshold,
self.persistence_backpressure_threshold,
);
self
}
/// Setter for block buffer limit.
pub const fn with_block_buffer_limit(mut self, block_buffer_limit: u32) -> Self {
self.block_buffer_limit = block_buffer_limit;
@@ -559,6 +617,35 @@ impl TreeConfig {
self
}
/// Returns whether to share execution cache with the payload builder.
pub const fn share_execution_cache_with_payload_builder(&self) -> bool {
self.share_execution_cache_with_payload_builder
}
/// Returns whether to share sparse trie with the payload builder.
pub const fn share_sparse_trie_with_payload_builder(&self) -> bool {
self.share_sparse_trie_with_payload_builder
}
/// Setter for whether to share execution cache with the payload builder.
pub const fn with_share_execution_cache_with_payload_builder(
mut self,
share_execution_cache_with_payload_builder: bool,
) -> Self {
self.share_execution_cache_with_payload_builder =
share_execution_cache_with_payload_builder;
self
}
/// Setter for whether to share sparse trie with the payload builder.
pub const fn with_share_sparse_trie_with_payload_builder(
mut self,
share_sparse_trie_with_payload_builder: bool,
) -> Self {
self.share_sparse_trie_with_payload_builder = share_sparse_trie_with_payload_builder;
self
}
/// Returns the proof jitter duration, if configured (trie-debug only).
#[cfg(feature = "trie-debug")]
pub const fn proof_jitter(&self) -> Option<Duration> {
@@ -572,3 +659,18 @@ impl TreeConfig {
self
}
}
#[cfg(test)]
mod tests {
use super::TreeConfig;
#[test]
#[should_panic(
expected = "persistence_backpressure_threshold must be greater than persistence_threshold"
)]
fn rejects_backpressure_threshold_at_or_below_persistence_threshold() {
let _ = TreeConfig::default()
.with_persistence_threshold(4)
.with_persistence_backpressure_threshold(4);
}
}

View File

@@ -86,13 +86,14 @@ where
}
}
/// Information about a slow block detected after persistence.
/// Information about a slow block detected after execution or persistence.
#[derive(Clone, Debug)]
pub struct SlowBlockInfo {
/// The timing statistics for the slow block.
pub stats: Box<ExecutionTimingStats>,
/// The commit duration for the batch containing this block.
pub commit_duration: Duration,
/// `None` when emitted immediately after execution (before persistence).
pub commit_duration: Option<Duration>,
/// The total duration (execution + `state_root` + commit).
/// Note: `state_read` is a subset of execution and is not added separately.
pub total_duration: Duration,

View File

@@ -117,7 +117,19 @@ pub trait EngineTypes:
+ 'static;
}
/// Type that validates the payloads processed by the engine API.
/// Validates engine API requests at the RPC layer, before payloads and attributes
/// are forwarded to the engine for processing.
///
/// - [`validate_version_specific_fields`](Self::validate_version_specific_fields): Enforced in each
/// `engine_newPayloadVN` RPC handler to verify the payload contains the correct fields for the
/// engine API version (e.g., blob fields in V3+, requests in V4+).
///
/// - [`ensure_well_formed_attributes`](Self::ensure_well_formed_attributes): Enforced in
/// `engine_forkchoiceUpdatedVN` RPC handlers to validate payload attributes are well-formed for
/// the given version before forwarding to the engine.
///
/// After this validation passes, the engine performs the full consensus validation
/// pipeline (header, pre-execution, execution, post-execution).
pub trait EngineApiValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static {
/// Validates the presence or exclusion of fork-specific fields based on the payload attributes
/// and the message version.
@@ -136,6 +148,34 @@ pub trait EngineApiValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static
}
/// Type that validates an [`ExecutionPayload`].
///
/// This trait handles validation at the engine API boundary — converting payloads
/// into blocks and validating payload attributes for block building.
///
/// # Methods and when they're used
///
/// - [`convert_payload_to_block`](Self::convert_payload_to_block): Used during `engine_newPayload`
/// processing to decode the payload into a [`SealedBlock`]. Also used to validate payload
/// structure during backfill buffering. In the engine tree, this runs concurrently with state
/// setup on a background thread.
///
/// - [`ensure_well_formed_payload`](Self::ensure_well_formed_payload): Converts payload and
/// recovers transaction signatures. Used when recovered senders are needed immediately.
///
/// - [`validate_payload_attributes_against_header`](Self::validate_payload_attributes_against_header):
/// Enforced as part of the engine's `forkchoiceUpdated` handling when payload attributes
/// are provided. Gates whether a payload build job is started.
///
/// - [`validate_block_post_execution_with_hashed_state`](Self::validate_block_post_execution_with_hashed_state):
/// Called after block execution in the engine's payload validation pipeline.
/// No-op on L1, used by L2s (e.g., OP Stack) for additional post-execution checks.
///
/// # Relationship to consensus traits
///
/// This trait does NOT replace the consensus traits (`Consensus`, `FullConsensus` from
/// `reth-consensus`). Those handle the actual consensus rule
/// validation (header checks, pre/post-execution). This trait handles engine API-specific
/// concerns: payload encoding/decoding and attribute validation.
#[auto_impl::auto_impl(&, Arc)]
pub trait PayloadValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static {
/// The block type used by the engine.
@@ -191,6 +231,11 @@ pub trait PayloadValidator<Types: PayloadTypes>: Send + Sync + Unpin + 'static {
/// > of a block referenced by forkchoiceState.headBlockHash.
///
/// See also: <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#specification-1>
///
/// Enforced as part of the engine's `forkchoiceUpdated` handling when the consensus layer
/// provides payload attributes. If this returns an error, the forkchoice state update itself
/// is NOT rolled back, but no payload build job is started — the response includes
/// `INVALID_PAYLOAD_ATTRIBUTES`.
fn validate_payload_attributes_against_header(
&self,
attr: &Types::PayloadAttributes,

View File

@@ -14,7 +14,7 @@ use core::{
use futures::{future::Either, FutureExt, TryFutureExt};
use reth_errors::RethResult;
use reth_payload_builder_primitives::PayloadBuilderError;
use reth_payload_primitives::{EngineApiMessageVersion, PayloadTypes};
use reth_payload_primitives::PayloadTypes;
use std::time::Duration;
use tokio::sync::{mpsc::UnboundedSender, oneshot};
@@ -162,6 +162,30 @@ pub struct NewPayloadTimings {
pub sparse_trie_wait: Option<Duration>,
}
/// Additional data for big block payloads that merge multiple real blocks.
///
/// This is used by the `reth_newPayload` endpoint to pass environment switches
/// and prior block hashes needed for correct multi-segment execution.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BigBlockData<ExecutionData> {
/// Environment switches at block boundaries.
/// Each entry is `(cumulative_tx_count, execution_data_of_next_block)`.
///
/// The first entry at index 0 represents the **original unmutated** base block's
/// `ExecutionData`, which must be used to derive the initial EVM environment.
pub env_switches: Vec<(usize, ExecutionData)>,
/// Block number → real block hash for blocks covered by previous big blocks in a sequence.
/// When replaying chained big blocks, the BLOCKHASH opcode needs real hashes for blocks
/// that were merged into earlier big blocks (and thus not individually persisted).
pub prior_block_hashes: Vec<(u64, alloy_primitives::B256)>,
}
impl<T> Default for BigBlockData<T> {
fn default() -> Self {
Self { env_switches: Vec::new(), prior_block_hashes: Vec::new() }
}
}
/// A message for the beacon engine from other components of the node (engine RPC API invoked by the
/// consensus layer).
#[derive(Debug)]
@@ -195,8 +219,6 @@ pub enum BeaconEngineMessage<Payload: PayloadTypes> {
state: ForkchoiceState,
/// The payload attributes for block building.
payload_attrs: Option<Payload::PayloadAttributes>,
/// The Engine API Version.
version: EngineApiMessageVersion,
/// The sender for returning forkchoice updated result.
tx: oneshot::Sender<RethResult<OnForkChoiceUpdated>>,
},
@@ -297,10 +319,9 @@ where
&self,
state: ForkchoiceState,
payload_attrs: Option<Payload::PayloadAttributes>,
version: EngineApiMessageVersion,
) -> Result<ForkchoiceUpdated, BeaconForkChoiceUpdateError> {
Ok(self
.send_fork_choice_updated(state, payload_attrs, version)
.send_fork_choice_updated(state, payload_attrs)
.map_err(|_| BeaconForkChoiceUpdateError::EngineUnavailable)
.await?
.map_err(BeaconForkChoiceUpdateError::internal)?
@@ -313,14 +334,12 @@ where
&self,
state: ForkchoiceState,
payload_attrs: Option<Payload::PayloadAttributes>,
version: EngineApiMessageVersion,
) -> oneshot::Receiver<RethResult<OnForkChoiceUpdated>> {
let (tx, rx) = oneshot::channel();
let _ = self.to_engine.send(BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx,
version,
});
rx
}

View File

@@ -17,6 +17,7 @@ reth-chainspec = { workspace = true, optional = true }
reth-consensus.workspace = true
reth-db.workspace = true
reth-engine-primitives = { workspace = true, features = ["std"] }
reth-execution-cache.workspace = true
reth-errors.workspace = true
reth-execution-types.workspace = true
reth-evm = { workspace = true, features = ["metrics"] }
@@ -52,7 +53,6 @@ revm-primitives.workspace = true
futures.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread", "sync", "macros"] }
fixed-cache.workspace = true
moka = { workspace = true, features = ["sync"] }
# metrics
@@ -132,6 +132,7 @@ test-utils = [
"reth-node-ethereum/test-utils",
"reth-evm-ethereum/test-utils",
"reth-tasks/test-utils",
"reth-execution-cache/test-utils",
]
trie-debug = [
"reth-trie-sparse/trie-debug",

View File

@@ -21,7 +21,7 @@ use reth_payload_builder::PayloadBuilderHandle;
use reth_primitives_traits::NodePrimitives;
use reth_provider::{
providers::{BlockchainProvider, ProviderNodeTypes},
ProviderFactory, StorageSettingsCache,
ProviderFactory,
};
use reth_prune::PrunerWithFactory;
use reth_stages_api::{MetricEventsSender, Pipeline};
@@ -81,7 +81,6 @@ where
C: ConfigureEvm<Primitives = N::Primitives> + 'static,
{
let downloader = BasicBlockDownloader::new(client, consensus.clone());
let use_hashed_state = provider.cached_storage_settings().use_hashed_state();
let persistence_handle =
PersistenceHandle::<N::Primitives>::spawn_service(provider, pruner, sync_metrics_tx);
@@ -99,7 +98,6 @@ where
engine_kind,
evm_config,
changeset_cache,
use_hashed_state,
runtime,
);

View File

@@ -171,6 +171,10 @@ pub struct EngineMetrics {
pub(crate) executed_new_block_cache_miss: Counter,
/// Histogram of persistence operation durations (in seconds)
pub(crate) persistence_duration: Histogram,
/// Whether the engine loop is currently stalled on persistence backpressure.
pub(crate) backpressure_active: Gauge,
/// Time spent blocked waiting on persistence because backpressure was active.
pub(crate) backpressure_stall_duration: Histogram,
/// Tracks the how often we failed to deliver a newPayload response.
///
/// This effectively tracks how often the message sender dropped the channel and indicates a CL
@@ -448,11 +452,17 @@ impl NewPayloadStatusMetrics {
Ok(outcome) => match outcome.outcome.status {
PayloadStatusEnum::Valid => {
self.new_payload_valid.increment(1);
self.new_payload_total_gas.record(gas_used as f64);
self.new_payload_total_gas_last.set(gas_used as f64);
let gas_per_second = gas_used as f64 / elapsed.as_secs_f64();
self.new_payload_gas_per_second.record(gas_per_second);
self.new_payload_gas_per_second_last.set(gas_per_second);
if !outcome.already_seen {
self.new_payload_total_gas.record(gas_used as f64);
self.new_payload_total_gas_last.set(gas_used as f64);
let gas_per_second = gas_used as f64 / elapsed.as_secs_f64();
self.new_payload_gas_per_second.record(gas_per_second);
self.new_payload_gas_per_second_last.set(gas_per_second);
self.new_payload_latency.record(elapsed);
self.new_payload_last.set(elapsed);
self.gas_bucket.record(gas_used, elapsed);
}
}
PayloadStatusEnum::Syncing => self.new_payload_syncing.increment(1),
PayloadStatusEnum::Accepted => self.new_payload_accepted.increment(1),
@@ -461,9 +471,6 @@ impl NewPayloadStatusMetrics {
Err(_) => self.new_payload_error.increment(1),
}
self.new_payload_messages.increment(1);
self.new_payload_latency.record(elapsed);
self.new_payload_last.set(elapsed);
self.gas_bucket.record(gas_used, elapsed);
if let Some(latest_forkchoice_updated_at) = latest_forkchoice_updated_at.take() {
self.forkchoice_updated_new_payload_time_diff
.record(start - latest_forkchoice_updated_at);

View File

@@ -23,10 +23,8 @@ use reth_engine_primitives::{
};
use reth_errors::{ConsensusError, ProviderResult};
use reth_evm::ConfigureEvm;
use reth_payload_builder::PayloadBuilderHandle;
use reth_payload_primitives::{
BuiltPayload, EngineApiMessageVersion, NewPayloadError, PayloadBuilderAttributes, PayloadTypes,
};
use reth_payload_builder::{BuildNewPayload, PayloadBuilderHandle};
use reth_payload_primitives::{BuiltPayload, NewPayloadError, PayloadTypes};
use reth_primitives_traits::{
FastInstant as Instant, NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader,
};
@@ -52,7 +50,6 @@ use tokio::sync::{
use tracing::*;
mod block_buffer;
mod cached_state;
pub mod error;
pub mod instrumented_state;
mod invalid_headers;
@@ -67,13 +64,15 @@ mod trie_updates;
use crate::{persistence::PersistenceResult, tree::error::AdvancePersistenceError};
pub use block_buffer::BlockBuffer;
pub use cached_state::{CachedStateMetrics, CachedStateProvider, ExecutionCache, SavedCache};
pub use invalid_headers::InvalidHeaderCache;
pub use metrics::EngineApiMetrics;
pub use payload_processor::*;
pub use payload_validator::{BasicEngineValidator, EngineValidator};
pub use persistence_state::PersistenceState;
pub use reth_engine_primitives::TreeConfig;
pub use reth_execution_cache::{
CachedStateMetrics, CachedStateProvider, ExecutionCache, PayloadExecutionCache, SavedCache,
};
pub mod state;
@@ -180,12 +179,15 @@ pub struct TreeOutcome<T> {
pub outcome: T,
/// An optional event to tell the caller to do something.
pub event: Option<TreeEvent>,
/// Whether the block was already seen, meaning no real execution happened during this
/// `newPayload` call.
pub already_seen: bool,
}
impl<T> TreeOutcome<T> {
/// Create new tree outcome.
pub const fn new(outcome: T) -> Self {
Self { outcome, event: None }
Self { outcome, event: None, already_seen: false }
}
/// Set event on the outcome.
@@ -193,6 +195,31 @@ impl<T> TreeOutcome<T> {
self.event = Some(event);
self
}
/// Set the `already_seen` flag on the outcome.
pub const fn with_already_seen(mut self, value: bool) -> Self {
self.already_seen = value;
self
}
}
/// Result of trying to insert a new payload in [`EngineApiTreeHandler`].
#[derive(Debug)]
pub struct TryInsertPayloadResult {
/// - `Valid`: Payload successfully validated and inserted
/// - `Syncing`: Parent missing, payload buffered for later
/// - Error status: Payload is invalid
pub status: PayloadStatus,
/// Whether the block was already seen
pub already_seen: bool,
}
impl TryInsertPayloadResult {
/// Convert the result into a [`TreeOutcome`].
#[inline]
pub fn into_outcome(self) -> TreeOutcome<PayloadStatus> {
TreeOutcome::new(self.status).with_already_seen(self.already_seen)
}
}
/// Events that are triggered by Tree Chain
@@ -277,9 +304,6 @@ where
/// Stored here (not in `ExecutedBlock`) to avoid leaking observability concerns into the block
/// type. Entries are removed when blocks are persisted or invalidated.
execution_timing_stats: HashMap<B256, Box<ExecutionTimingStats>>,
/// Whether the node uses hashed state as canonical storage (v2 mode).
/// Cached at construction to avoid threading `StorageSettingsCache` bounds everywhere.
use_hashed_state: bool,
/// Task runtime for spawning blocking work on named, reusable threads.
runtime: reth_tasks::Runtime,
}
@@ -308,7 +332,6 @@ where
.field("evm_config", &self.evm_config)
.field("changeset_cache", &self.changeset_cache)
.field("execution_timing_stats", &self.execution_timing_stats.len())
.field("use_hashed_state", &self.use_hashed_state)
.field("runtime", &self.runtime)
.finish()
}
@@ -349,7 +372,6 @@ where
engine_kind: EngineApiKind,
evm_config: C,
changeset_cache: ChangesetCache,
use_hashed_state: bool,
runtime: reth_tasks::Runtime,
) -> Self {
let (incoming_tx, incoming) = crossbeam_channel::unbounded();
@@ -373,7 +395,6 @@ where
evm_config,
changeset_cache,
execution_timing_stats: HashMap::new(),
use_hashed_state,
runtime,
}
}
@@ -395,7 +416,6 @@ where
kind: EngineApiKind,
evm_config: C,
changeset_cache: ChangesetCache,
use_hashed_state: bool,
runtime: reth_tasks::Runtime,
) -> (Sender<FromEngine<EngineApiRequest<T, N>, N::Block>>, UnboundedReceiver<EngineApiEvent<N>>)
{
@@ -429,7 +449,6 @@ where
kind,
evm_config,
changeset_cache,
use_hashed_state,
runtime,
);
let incoming = task.incoming_tx.clone();
@@ -453,12 +472,79 @@ where
self.incoming_tx.clone()
}
/// How many blocks the canonical tip is ahead of the last persisted block. A large gap means
/// persistence is falling behind execution.
const fn persistence_gap(&self) -> u64 {
self.state
.tree_state
.canonical_block_number()
.saturating_sub(self.persistence_state.last_persisted_block.number)
}
/// Returns `true` when the main loop should stop draining the tree input channel.
///
/// This is the case when persistence is already running and the gap between the canonical tip
/// and the last persisted block has reached the configured threshold.
const fn should_backpressure(&self) -> bool {
self.persistence_state.in_progress() &&
self.persistence_gap() >= self.config.persistence_backpressure_threshold()
}
/// Run the engine API handler.
///
/// This will block the current thread and process incoming messages.
pub fn run(mut self) {
loop {
match self.wait_for_event() {
// Each iteration has three phases:
//
// 1. Non-blocking poll for persistence completion. If the background flush already
// landed, absorb the result now so the gap calculation below is fresh.
// 2. Decide how to wait for the next event. When the canonical-to-persisted gap exceeds
// the backpressure threshold we only block on the persistence receiver, leaving new
// engine requests sitting in the unbounded upstream channel.
// 3. Handle the event (engine message or persistence completion) and kick off a new
// persistence cycle if the threshold is met again.
//
// The net effect: when the persistence gap exceeds the threshold, we stop
// processing incoming messages and let them queue in the channel. This is only a
// soft form of backpressure: it delays replies and, more importantly, prevents
// executing further blocks that would pile up in the persistence queue - where each
// block carries heavier state (eg. trie updates) than the raw payload sitting in the
// engine channel.
//
// Standard Ethereum CLs won't truly back off - the engine API has no
// backpressure semantics, and CLs typically timeout after ≈8s and resend - so
// this cannot prevent the incoming channel from growing under sustained load.
// But it shifts the bottleneck to the lighter-weight incoming queue rather than
// the costlier persistence pipeline. Other clients that respect reply latency
// can treat the delayed responses as a signal to chill out.
match self.try_poll_persistence() {
Ok(true) => {
if let Err(err) = self.advance_persistence() {
error!(target: "engine::tree", %err, "Advancing persistence failed");
return
}
continue;
}
Ok(false) => {}
Err(err) => {
error!(target: "engine::tree", %err, "Polling persistence failed");
return
}
}
let event = if self.should_backpressure() {
self.metrics.engine.backpressure_active.set(1.0);
let stall_start = Instant::now();
let event = self.wait_for_persistence_event();
self.metrics.engine.backpressure_stall_duration.record(stall_start.elapsed());
event
} else {
self.metrics.engine.backpressure_active.set(0.0);
self.wait_for_event()
};
match event {
LoopEvent::EngineMessage(msg) => {
debug!(target: "engine::tree", %msg, "received new engine message");
match self.on_engine_message(msg) {
@@ -493,6 +579,24 @@ where
}
}
/// Blocks until the in-flight persistence task completes, used when we are under
/// backpressure.
///
/// Unlike `wait_for_event`, this deliberately does not read from the tree input channel. Any
/// requests sent to the tree remain queued upstream until persistence catches up.
fn wait_for_persistence_event(&mut self) -> LoopEvent<T, N> {
let maybe_persistence = self.persistence_state.rx.take();
if let Some((persistence_rx, start_time, _action)) = maybe_persistence {
match persistence_rx.recv() {
Ok(result) => LoopEvent::PersistenceComplete { result, start_time },
Err(_) => LoopEvent::Disconnected,
}
} else {
self.wait_for_event()
}
}
/// Blocks until the next event is ready: either an incoming engine message or a persistence
/// completion (if one is in progress).
///
@@ -639,13 +743,12 @@ where
// record pre-execution phase duration
self.metrics.block_validation.record_payload_validation(start.elapsed().as_secs_f64());
let status = if self.backfill_sync_state.is_idle() {
self.try_insert_payload(payload)?
let mut outcome = if self.backfill_sync_state.is_idle() {
self.try_insert_payload(payload)?.into_outcome()
} else {
self.try_buffer_payload(payload)?
TreeOutcome::new(self.try_buffer_payload(payload)?)
};
let mut outcome = TreeOutcome::new(status);
// if the block is valid and it is the current sync target head, make it canonical
if outcome.outcome.is_valid() && self.is_sync_target_head(block_hash) {
// Only create the canonical event if this block isn't already the canonical head
@@ -663,16 +766,11 @@ where
}
/// Processes a payload during normal sync operation.
///
/// Returns:
/// - `Valid`: Payload successfully validated and inserted
/// - `Syncing`: Parent missing, payload buffered for later
/// - Error status: Payload is invalid
#[instrument(level = "debug", target = "engine::tree", skip_all)]
fn try_insert_payload(
&mut self,
payload: T::ExecutionData,
) -> Result<PayloadStatus, InsertBlockFatalError> {
) -> Result<TryInsertPayloadResult, InsertBlockFatalError> {
let block_hash = payload.block_hash();
let num_hash = payload.num_hash();
let parent_hash = payload.parent_hash();
@@ -680,31 +778,40 @@ where
match self.insert_payload(payload) {
Ok(status) => {
let status = match status {
let (status, already_seen) = match status {
InsertPayloadOk::Inserted(BlockStatus::Valid) => {
latest_valid_hash = Some(block_hash);
self.try_connect_buffered_blocks(num_hash)?;
PayloadStatusEnum::Valid
(PayloadStatusEnum::Valid, false)
}
InsertPayloadOk::AlreadySeen(BlockStatus::Valid) => {
latest_valid_hash = Some(block_hash);
PayloadStatusEnum::Valid
(PayloadStatusEnum::Valid, true)
}
InsertPayloadOk::Inserted(BlockStatus::Disconnected { .. }) => {
(PayloadStatusEnum::Syncing, false)
}
InsertPayloadOk::Inserted(BlockStatus::Disconnected { .. }) |
InsertPayloadOk::AlreadySeen(BlockStatus::Disconnected { .. }) => {
// not known to be invalid, but we don't know anything else
PayloadStatusEnum::Syncing
(PayloadStatusEnum::Syncing, true)
}
};
Ok(PayloadStatus::new(status, latest_valid_hash))
Ok(TryInsertPayloadResult {
status: PayloadStatus::new(status, latest_valid_hash),
already_seen,
})
}
Err(error) => {
let status = match error {
InsertPayloadError::Block(error) => self.on_insert_block_error(error)?,
InsertPayloadError::Payload(error) => {
self.on_new_payload_error(error, num_hash, parent_hash)?
}
};
Ok(TryInsertPayloadResult { status, already_seen: false })
}
Err(error) => match error {
InsertPayloadError::Block(error) => Ok(self.on_insert_block_error(error)?),
InsertPayloadError::Payload(error) => {
Ok(self.on_new_payload_error(error, num_hash, parent_hash)?)
}
},
}
}
@@ -1001,7 +1108,6 @@ where
&mut self,
state: ForkchoiceState,
attrs: Option<T::PayloadAttributes>,
version: EngineApiMessageVersion,
) -> ProviderResult<TreeOutcome<OnForkChoiceUpdated>> {
trace!(target: "engine::tree", ?attrs, "invoked forkchoice update");
@@ -1014,13 +1120,13 @@ where
}
// Return early if we are on the correct fork
if let Some(result) = self.handle_canonical_head(state, &attrs, version)? {
if let Some(result) = self.handle_canonical_head(state, &attrs)? {
return Ok(result);
}
// Attempt to apply a chain update when the head differs from our canonical chain.
// This handles reorgs and chain extensions by making the specified head canonical.
if let Some(result) = self.apply_chain_update(state, &attrs, version)? {
if let Some(result) = self.apply_chain_update(state, &attrs)? {
return Ok(result);
}
@@ -1071,7 +1177,6 @@ where
&self,
state: ForkchoiceState,
attrs: &Option<T::PayloadAttributes>, // Changed to reference
version: EngineApiMessageVersion,
) -> ProviderResult<Option<TreeOutcome<OnForkChoiceUpdated>>> {
// Process the forkchoice update by trying to make the head block canonical
//
@@ -1109,7 +1214,7 @@ where
ProviderError::HeaderNotFound(state.head_block_hash.into())
})?;
// Clone only when we actually need to process the attributes
let updated = self.process_payload_attributes(attr.clone(), &tip, state, version);
let updated = self.process_payload_attributes(attr.clone(), &tip, state);
return Ok(Some(TreeOutcome::new(updated)));
}
@@ -1132,7 +1237,6 @@ where
&mut self,
state: ForkchoiceState,
attrs: &Option<T::PayloadAttributes>,
version: EngineApiMessageVersion,
) -> ProviderResult<Option<TreeOutcome<OnForkChoiceUpdated>>> {
// Check if the head is already part of the canonical chain
if let Ok(Some(canonical_header)) = self.find_canonical_header(state.head_block_hash) {
@@ -1155,12 +1259,8 @@ where
if let Some(attr) = attrs {
debug!(target: "engine::tree", head = canonical_header.number(), "handling payload attributes for canonical head");
// Clone only when we actually need to process the attributes
let updated = self.process_payload_attributes(
attr.clone(),
&canonical_header,
state,
version,
);
let updated =
self.process_payload_attributes(attr.clone(), &canonical_header, state);
return Ok(Some(TreeOutcome::new(updated)));
}
}
@@ -1191,7 +1291,7 @@ where
if let Some(attr) = attrs {
// Clone only when we actually need to process the attributes
let updated = self.process_payload_attributes(attr.clone(), &tip, state, version);
let updated = self.process_payload_attributes(attr.clone(), &tip, state);
return Ok(Some(TreeOutcome::new(updated)));
}
@@ -1305,7 +1405,8 @@ where
fn persist_until_complete(&mut self) -> Result<(), AdvancePersistenceError> {
loop {
// Wait for any in-progress persistence to complete (blocking)
if let Some((rx, start_time, _action)) = self.persistence_state.rx.take() {
if let Some((rx, start_time, action)) = self.persistence_state.rx.take() {
debug!(target: "engine::tree", ?action, "waiting for in-flight persistence");
let result = rx.recv().map_err(|_| AdvancePersistenceError::ChannelClosed)?;
self.on_persistence_complete(result, start_time)?;
}
@@ -1325,8 +1426,7 @@ where
/// Tries to poll for a completed persistence task (non-blocking).
///
/// Returns `true` if a persistence task was completed, `false` otherwise.
#[cfg(test)]
pub fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
fn try_poll_persistence(&mut self) -> Result<bool, AdvancePersistenceError> {
let Some((rx, start_time, action)) = self.persistence_state.rx.take() else {
return Ok(false);
};
@@ -1464,17 +1564,11 @@ where
}
EngineApiRequest::Beacon(request) => {
match request {
BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx,
version,
} => {
BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx } => {
let has_attrs = payload_attrs.is_some();
let start = Instant::now();
let mut output =
self.on_forkchoice_updated(state, payload_attrs, version);
let mut output = self.on_forkchoice_updated(state, payload_attrs);
if let Ok(res) = &mut output {
// track last received forkchoice state
@@ -1870,7 +1964,7 @@ where
if total_duration > threshold.expect("checked above") {
self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
stats,
commit_duration: commit_dur,
commit_duration: Some(commit_dur),
total_duration,
}));
}
@@ -2519,12 +2613,7 @@ where
self.update_reorg_metrics(old.len(), old_first);
self.reinsert_reorged_blocks(new.clone());
// When use_hashed_state is enabled, skip reinserting the old chain — the
// bundle state references plain state reverts which don't exist.
if !self.use_hashed_state {
self.reinsert_reorged_blocks(old.clone());
}
self.reinsert_reorged_blocks(old.clone());
}
// update the tracked in-memory state with the new chain
@@ -2688,10 +2777,7 @@ where
// try to append the block
match self.insert_block(block) {
Ok(
InsertPayloadOk::Inserted(BlockStatus::Valid) |
InsertPayloadOk::AlreadySeen(BlockStatus::Valid),
) => {
Ok(InsertPayloadOk::Inserted(BlockStatus::Valid)) => {
return self.on_valid_downloaded_block(block_num_hash);
}
Ok(InsertPayloadOk::Inserted(BlockStatus::Disconnected { head, missing_ancestor })) => {
@@ -2849,8 +2935,19 @@ where
let (executed, timing_stats) = execute(&mut self.payload_validator, input, ctx)?;
// Store timing stats for detailed block logging after persistence
// Emit slow block event immediately after execution so it appears even when
// persistence hasn't completed yet (e.g. blocks arriving faster than persistence).
if let Some(stats) = timing_stats {
if let Some(threshold) = self.config.slow_block_threshold() {
let total_duration = stats.execution_duration + stats.state_hash_duration;
if total_duration > threshold {
self.emit_event(ConsensusEngineEvent::SlowBlock(SlowBlockInfo {
stats: stats.clone(),
commit_duration: None,
total_duration,
}));
}
}
self.execution_timing_stats.insert(executed.recovered_block().hash(), stats);
}
@@ -3052,17 +3149,26 @@ where
/// Validates the payload attributes with respect to the header and fork choice state.
///
/// This is called during `engine_forkchoiceUpdated` when the CL provides payload attributes,
/// indicating it wants the EL to start building a new block.
///
/// Runs [`PayloadValidator::validate_payload_attributes_against_header`](reth_engine_primitives::PayloadValidator::validate_payload_attributes_against_header) to ensure
/// `payloadAttributes.timestamp > headBlock.timestamp` per the Engine API spec.
///
/// If validation passes, sends the attributes to the payload builder to start a new
/// payload job. If it fails, returns `INVALID_PAYLOAD_ATTRIBUTES` without rolling back
/// the forkchoice update.
///
/// Note: At this point, the fork choice update is considered to be VALID, however, we can still
/// return an error if the payload attributes are invalid.
fn process_payload_attributes(
&self,
attrs: T::PayloadAttributes,
attributes: T::PayloadAttributes,
head: &N::BlockHeader,
state: ForkchoiceState,
version: EngineApiMessageVersion,
) -> OnForkChoiceUpdated {
if let Err(err) =
self.payload_validator.validate_payload_attributes_against_header(&attrs, head)
self.payload_validator.validate_payload_attributes_against_header(&attributes, head)
{
warn!(target: "engine::tree", %err, ?head, "Invalid payload attributes");
return OnForkChoiceUpdated::invalid_payload_attributes()
@@ -3072,34 +3178,47 @@ where
// forkchoiceState.headBlockHash and identified via buildProcessId value if
// payloadAttributes is not null and the forkchoice state has been updated successfully.
// The build process is specified in the Payload building section.
match <T::PayloadBuilderAttributes as PayloadBuilderAttributes>::try_new(
state.head_block_hash,
attrs,
version as u8,
) {
Ok(attributes) => {
// send the payload to the builder and return the receiver for the pending payload
// id, initiating payload job is handled asynchronously
let pending_payload_id = self.payload_builder.send_new_payload(attributes);
// Client software MUST respond to this method call in the following way:
// {
// payloadStatus: {
// status: VALID,
// latestValidHash: forkchoiceState.headBlockHash,
// validationError: null
// },
// payloadId: buildProcessId
// }
//
// if the payload is deemed VALID and the build process has begun.
OnForkChoiceUpdated::updated_with_pending_payload_id(
PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)),
pending_payload_id,
)
}
Err(_) => OnForkChoiceUpdated::invalid_payload_attributes(),
}
let cache = if self.config.share_execution_cache_with_payload_builder() {
self.payload_validator.cache_for(state.head_block_hash)
} else {
None
};
let trie_handle = if self.config.share_sparse_trie_with_payload_builder() {
self.payload_validator.sparse_trie_handle_for(
state.head_block_hash,
head.state_root(),
&self.state,
)
} else {
None
};
// send the payload to the builder and return the receiver for the pending payload
// id, initiating payload job is handled asynchronously
let pending_payload_id = self.payload_builder.send_new_payload(BuildNewPayload {
parent_hash: state.head_block_hash,
attributes,
cache,
trie_handle,
});
// Client software MUST respond to this method call in the following way:
// {
// payloadStatus: {
// status: VALID,
// latestValidHash: forkchoiceState.headBlockHash,
// validationError: null
// },
// payloadId: buildProcessId
// }
//
// if the payload is deemed VALID and the build process has begun.
OnForkChoiceUpdated::updated_with_pending_payload_id(
PayloadStatus::new(PayloadStatusEnum::Valid, Some(state.head_block_hash)),
pending_payload_id,
)
}
/// Remove all blocks up to __and including__ the given block number.

View File

@@ -2,22 +2,16 @@
use super::precompile_cache::PrecompileCacheMap;
use crate::tree::{
cached_state::{CachedStateMetrics, ExecutionCache, SavedCache},
payload_processor::{
prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmMode, PrewarmTaskEvent},
sparse_trie::StateRootComputeOutcome,
},
payload_processor::prewarm::{PrewarmCacheTask, PrewarmContext, PrewarmMode, PrewarmTaskEvent},
sparse_trie::SparseTrieCacheTask,
CacheWaitDurations, StateProviderBuilder, TreeConfig, WaitForCaches,
CacheWaitDurations, CachedStateMetrics, ExecutionCache, PayloadExecutionCache, SavedCache,
StateProviderBuilder, TreeConfig, WaitForCaches,
};
use alloy_eip7928::BlockAccessList;
use alloy_eips::{eip1898::BlockWithParent, eip4895::Withdrawal};
use alloy_evm::block::StateChangeSource;
use alloy_primitives::B256;
use crossbeam_channel::{Receiver as CrossbeamReceiver, Sender as CrossbeamSender};
use metrics::{Counter, Histogram};
use multiproof::*;
use parking_lot::RwLock;
use prewarm::PrewarmMetrics;
use rayon::prelude::*;
use reth_evm::{
@@ -26,12 +20,11 @@ use reth_evm::{
ConfigureEvm, ConvertTx, EvmEnvFor, ExecutableTxIterator, ExecutableTxTuple, OnStateHook,
SpecFor, TxEnvFor,
};
use reth_metrics::Metrics;
use reth_primitives_traits::{FastInstant as Instant, NodePrimitives};
use reth_provider::{
BlockExecutionOutput, BlockReader, DatabaseProviderROFactory, StateProviderFactory, StateReader,
};
use reth_revm::{db::BundleState, state::EvmState};
use reth_revm::db::BundleState;
use reth_tasks::{utils::increase_thread_priority, ForEachOrdered, Runtime};
use reth_trie::{hashed_cursor::HashedCursorFactory, trie_cursor::TrieCursorFactory};
use reth_trie_parallel::{
@@ -48,9 +41,8 @@ use std::{
mpsc::{self, channel},
Arc,
},
time::Duration,
};
use tracing::{debug, debug_span, instrument, warn, Span};
use tracing::{debug, debug_span, instrument, trace, warn, Span};
pub mod bal;
pub mod multiproof;
@@ -275,12 +267,18 @@ where
let span = Span::current();
let state_root_handle = self.spawn_state_root(multiproof_provider_factory, &env, config);
let halve_workers = env.transaction_count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD;
let state_root_handle = self.spawn_state_root(
multiproof_provider_factory,
env.parent_state_root,
halve_workers,
config,
);
let prewarm_handle = self.spawn_caching_with(
env,
prewarm_rx,
provider_builder,
Some(state_root_handle.to_multi_proof.clone()),
Some(state_root_handle.updates_tx().clone()),
bal,
);
@@ -326,11 +324,15 @@ where
/// state root.
///
/// The state hook **must** be dropped after execution to signal the end of state updates.
///
/// When `halve_workers` is true, the proof worker pool is halved (for small blocks where
/// fewer transactions produce fewer state changes and most workers would be idle).
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip_all)]
pub fn spawn_state_root<F>(
&mut self,
&self,
multiproof_provider_factory: F,
env: &ExecutionEnv<Evm>,
parent_state_root: B256,
halve_workers: bool,
config: &TreeConfig,
) -> StateRootHandle
where
@@ -340,12 +342,11 @@ where
+ Sync
+ 'static,
{
let (to_multi_proof, from_multi_proof) = crossbeam_channel::unbounded();
let (updates_tx, from_multi_proof) = crossbeam_channel::unbounded();
let task_ctx = ProofTaskCtx::new(multiproof_provider_factory);
#[cfg(feature = "trie-debug")]
let task_ctx = task_ctx.with_proof_jitter(config.proof_jitter());
let halve_workers = env.transaction_count <= Self::SMALL_BLOCK_PROOF_WORKER_TX_THRESHOLD;
let proof_handle = ProofWorkerHandle::new(&self.executor, task_ctx, halve_workers);
let (state_root_tx, state_root_rx) = channel();
@@ -354,11 +355,11 @@ where
proof_handle,
state_root_tx,
from_multi_proof,
env.parent_state_root,
parent_state_root,
config.multiproof_chunk_size(),
);
StateRootHandle::new(to_multi_proof, state_root_rx)
StateRootHandle::new(parent_state_root, updates_tx, state_root_rx)
}
/// Transaction count threshold below which proof workers are halved, since fewer transactions
@@ -449,9 +450,9 @@ where
tx
});
let _ = execute_tx.send(tx);
debug!(target: "engine::tree::payload_processor", idx, "yielded transaction");
});
});
trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
});
});
}
(prewarm_rx, execute_rx)
@@ -469,7 +470,7 @@ where
env: ExecutionEnv<Evm>,
transactions: mpsc::Receiver<(usize, impl ExecutableTxFor<Evm> + Clone + Send + 'static)>,
provider_builder: StateProviderBuilder<N, P>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
to_multi_proof: Option<CrossbeamSender<StateRootMessage>>,
bal: Option<Arc<BlockAccessList>>,
) -> CacheTaskHandle<N::Receipt>
where
@@ -524,7 +525,7 @@ where
/// If the given hash is different then what is recently cached, then this will create a new
/// instance.
#[instrument(level = "debug", target = "engine::caching", skip(self))]
fn cache_for(&self, parent_hash: B256) -> SavedCache {
pub fn cache_for(&self, parent_hash: B256) -> SavedCache {
if let Some(cache) = self.execution_cache.get_cache_for(parent_hash) {
debug!("reusing execution cache");
cache
@@ -546,7 +547,7 @@ where
&self,
proof_worker_handle: ProofWorkerHandle,
state_root_tx: mpsc::Sender<Result<StateRootComputeOutcome, ParallelStateRootError>>,
from_multi_proof: CrossbeamReceiver<MultiProofMessage>,
from_multi_proof: CrossbeamReceiver<StateRootMessage>,
parent_state_root: B256,
chunk_size: usize,
) {
@@ -742,68 +743,7 @@ fn convert_serial<RawTx, Tx, TxEnv, InnerTx, Recovered, Err, C>(
let _ = prewarm_tx.send((idx, tx.clone()));
}
let _ = execute_tx.send(tx);
debug!(target: "engine::tree::payload_processor", idx, "yielded transaction");
}
}
/// Handle to a background state root computation task.
///
/// Unlike [`PayloadHandle`], this does not include transaction iteration or cache prewarming.
/// It only provides access to the state root computation via [`Self::state_hook`] and
/// [`Self::state_root`].
///
/// Created by [`PayloadProcessor::spawn_state_root`].
#[derive(Debug)]
pub struct StateRootHandle {
/// Channel for evm state updates to the multiproof pipeline.
to_multi_proof: CrossbeamSender<MultiProofMessage>,
/// Receiver for the computed state root.
state_root_rx: Option<mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>>>,
}
impl StateRootHandle {
/// Creates a new state root handle.
pub const fn new(
to_multi_proof: CrossbeamSender<MultiProofMessage>,
state_root_rx: mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>>,
) -> Self {
Self { to_multi_proof, state_root_rx: Some(state_root_rx) }
}
/// Returns a state hook that streams state updates to the background state root task.
///
/// The hook must be dropped after execution completes to signal the end of state updates.
pub fn state_hook(&self) -> impl OnStateHook {
let to_multi_proof = StateHookSender::new(self.to_multi_proof.clone());
move |source: StateChangeSource, state: &EvmState| {
let _ =
to_multi_proof.send(MultiProofMessage::StateUpdate(source.into(), state.clone()));
}
}
/// Awaits the state root computation result.
///
/// # Panics
///
/// If called more than once.
pub fn state_root(&mut self) -> Result<StateRootComputeOutcome, ParallelStateRootError> {
self.state_root_rx
.take()
.expect("state_root already taken")
.recv()
.map_err(|_| ParallelStateRootError::Other("sparse trie task dropped".to_string()))?
}
/// Takes the state root receiver for use with custom waiting logic (e.g., timeouts).
///
/// # Panics
///
/// If called more than once.
pub const fn take_state_root_rx(
&mut self,
) -> mpsc::Receiver<Result<StateRootComputeOutcome, ParallelStateRootError>> {
self.state_root_rx.take().expect("state_root already taken")
trace!(target: "engine::tree::payload_processor", idx, "yielded transaction");
}
}
@@ -961,148 +901,6 @@ impl<R> Drop for CacheTaskHandle<R> {
}
}
/// Shared access to most recently used cache.
///
/// This cache is intended to used for processing the payload in the following manner:
/// - Get Cache if the payload's parent block matches the parent block
/// - Update cache upon successful payload execution
///
/// This process assumes that payloads are received sequentially.
///
/// ## Cache Safety
///
/// **CRITICAL**: Cache update operations require exclusive access. All concurrent cache users
/// (such as prewarming tasks) must be terminated before calling
/// [`PayloadExecutionCache::update_with_guard`], otherwise the cache may be corrupted or cleared.
///
/// ## Cache vs Prewarming Distinction
///
/// **[`PayloadExecutionCache`]**:
/// - Stores parent block's execution state after completion
/// - Used to fetch parent data for next block's execution
/// - Must be exclusively accessed during save operations
///
/// **[`PrewarmCacheTask`]**:
/// - Speculatively loads accounts/storage that might be used in transaction execution
/// - Prepares data for state root proof computation
/// - Runs concurrently but must not interfere with cache saves
#[derive(Clone, Debug, Default)]
pub struct PayloadExecutionCache {
/// Guarded cloneable cache identified by a block hash.
inner: Arc<RwLock<Option<SavedCache>>>,
/// Metrics for cache operations.
metrics: ExecutionCacheMetrics,
}
impl PayloadExecutionCache {
/// Returns the cache for `parent_hash` if it's available for use.
///
/// A cache is considered available when:
/// - It exists and matches the requested parent hash
/// - No other tasks are currently using it (checked via Arc reference count)
#[instrument(level = "debug", target = "engine::tree::payload_processor", skip(self))]
pub(crate) fn get_cache_for(&self, parent_hash: B256) -> Option<SavedCache> {
let start = Instant::now();
let mut cache = self.inner.write();
let elapsed = start.elapsed();
self.metrics.execution_cache_wait_duration.record(elapsed.as_secs_f64());
if elapsed.as_millis() > 5 {
warn!(blocked_for=?elapsed, "Blocked waiting for execution cache mutex");
}
if let Some(c) = cache.as_mut() {
let cached_hash = c.executed_block_hash();
// Check that the cache hash matches the parent hash of the current block. It won't
// match in case it's a fork block.
let hash_matches = cached_hash == parent_hash;
// Check `is_available()` to ensure no other tasks (e.g., prewarming) currently hold
// a reference to this cache. We can only reuse it when we have exclusive access.
let available = c.is_available();
let usage_count = c.usage_count();
debug!(
target: "engine::caching",
%cached_hash,
%parent_hash,
hash_matches,
available,
usage_count,
"Existing cache found"
);
if available {
if !hash_matches {
// Fork block: clear and update the hash on the ORIGINAL before cloning.
// This prevents the canonical chain from matching on the stale hash
// and picking up polluted data if the fork block fails.
c.clear_with_hash(parent_hash);
}
return Some(c.clone())
} else if hash_matches {
self.metrics.execution_cache_in_use.increment(1);
}
} else {
debug!(target: "engine::caching", %parent_hash, "No cache found");
}
None
}
/// Waits until the execution cache becomes available for use.
///
/// This acquires a write lock to ensure exclusive access, then immediately releases it.
/// This is useful for synchronization before starting payload processing.
///
/// Returns the time spent waiting for the lock.
pub fn wait_for_availability(&self) -> Duration {
let start = Instant::now();
// Acquire write lock to wait for any current holders to finish
let _guard = self.inner.write();
let elapsed = start.elapsed();
if elapsed.as_millis() > 5 {
debug!(
target: "engine::tree::payload_processor",
blocked_for=?elapsed,
"Waited for execution cache to become available"
);
}
elapsed
}
/// Updates the cache with a closure that has exclusive access to the guard.
/// This ensures that all cache operations happen atomically.
///
/// ## CRITICAL SAFETY REQUIREMENT
///
/// **Before calling this method, you MUST ensure there are no other active cache users.**
/// This includes:
/// - No running [`PrewarmCacheTask`] instances that could write to the cache
/// - No concurrent transactions that might access the cached state
/// - All prewarming operations must be completed or cancelled
///
/// Violating this requirement can result in cache corruption, incorrect state data,
/// and potential consensus failures.
pub fn update_with_guard<F>(&self, update_fn: F)
where
F: FnOnce(&mut Option<SavedCache>),
{
let mut guard = self.inner.write();
update_fn(&mut guard);
}
}
/// Metrics for execution cache operations.
#[derive(Metrics, Clone)]
#[metrics(scope = "consensus.engine.beacon")]
pub(crate) struct ExecutionCacheMetrics {
/// Counter for when the execution cache was unavailable because other threads
/// (e.g., prewarming) are still using it.
pub(crate) execution_cache_in_use: Counter,
/// Time spent waiting for execution cache mutex to become available.
pub(crate) execution_cache_wait_duration: Histogram,
}
/// EVM context required to execute a block.
#[derive(Debug, Clone)]
pub struct ExecutionEnv<Evm: ConfigureEvm> {
@@ -1149,11 +947,10 @@ where
#[cfg(test)]
mod tests {
use super::PayloadExecutionCache;
use crate::tree::{
cached_state::{CachedStateMetrics, ExecutionCache, SavedCache},
payload_processor::{evm_state_to_hashed_post_state, ExecutionEnv, PayloadProcessor},
precompile_cache::PrecompileCacheMap,
CachedStateMetrics, ExecutionCache, PayloadExecutionCache, SavedCache,
StateProviderBuilder, TreeConfig,
};
use alloy_eips::eip1898::{BlockNumHash, BlockWithParent};

View File

@@ -1,120 +1,17 @@
//! Multiproof task related functionality.
use alloy_evm::block::StateChangeSource;
use alloy_primitives::{keccak256, B256};
use crossbeam_channel::Sender as CrossbeamSender;
use derive_more::derive::Deref;
use metrics::{Gauge, Histogram};
use reth_metrics::Metrics;
use reth_revm::state::EvmState;
use reth_trie::{HashedPostState, HashedStorage};
use reth_trie_common::MultiProofTargetsV2;
use std::sync::Arc;
use tracing::trace;
/// Source of state changes, either from EVM execution or from a Block Access List.
#[derive(Clone, Copy)]
pub enum Source {
/// State changes from EVM execution.
Evm(StateChangeSource),
/// State changes from Block Access List (EIP-7928).
BlockAccessList,
}
impl std::fmt::Debug for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Evm(source) => source.fmt(f),
Self::BlockAccessList => f.write_str("BlockAccessList"),
}
}
}
impl From<StateChangeSource> for Source {
fn from(source: StateChangeSource) -> Self {
Self::Evm(source)
}
}
pub use reth_trie_parallel::state_root_task::{
evm_state_to_hashed_post_state, Source, StateHookSender, StateRootComputeOutcome,
StateRootHandle, StateRootMessage,
};
/// The default max targets, for limiting the number of account and storage proof targets to be
/// fetched by a single worker. If exceeded, chunking is forced regardless of worker availability.
pub(crate) const DEFAULT_MAX_TARGETS_FOR_CHUNKING: usize = 300;
/// Messages used internally by the multi proof task.
#[derive(Debug)]
pub enum MultiProofMessage {
/// Prefetch proof targets
PrefetchProofs(MultiProofTargetsV2),
/// New state update from transaction execution with its source
StateUpdate(Source, EvmState),
/// Pre-hashed state update from BAL conversion that can be applied directly without proofs.
HashedStateUpdate(HashedPostState),
/// Block Access List (EIP-7928; BAL) containing complete state changes for the block.
///
/// When received, the task generates a single state update from the BAL and processes it.
/// No further messages are expected after receiving this variant.
BlockAccessList(Arc<alloy_eip7928::BlockAccessList>),
/// Signals state update stream end.
///
/// This is triggered by block execution, indicating that no additional state updates are
/// expected.
FinishedStateUpdates,
}
/// A wrapper for the sender that signals completion when dropped.
///
/// This type is intended to be used in combination with the evm executor statehook.
/// This should trigger once the block has been executed (after) the last state update has been
/// sent. This triggers the exit condition of the multi proof task.
#[derive(Deref, Debug)]
pub struct StateHookSender(CrossbeamSender<MultiProofMessage>);
impl StateHookSender {
/// Creates a new [`StateHookSender`] wrapping the given channel sender.
pub const fn new(inner: CrossbeamSender<MultiProofMessage>) -> Self {
Self(inner)
}
}
impl Drop for StateHookSender {
fn drop(&mut self) {
// Send completion signal when the sender is dropped
let _ = self.0.send(MultiProofMessage::FinishedStateUpdates);
}
}
pub(crate) fn evm_state_to_hashed_post_state(update: EvmState) -> HashedPostState {
let mut hashed_state = HashedPostState::with_capacity(update.len());
for (address, account) in update {
if account.is_touched() {
let hashed_address = keccak256(address);
trace!(target: "engine::tree::payload_processor::multiproof", ?address, ?hashed_address, "Adding account to state update");
let destroyed = account.is_selfdestructed();
let info = if destroyed { None } else { Some(account.info.into()) };
hashed_state.accounts.insert(hashed_address, info);
let mut changed_storage_iter = account
.storage
.into_iter()
.filter(|(_slot, value)| value.is_changed())
.map(|(slot, value)| (keccak256(B256::from(slot)), value.present_value))
.peekable();
if destroyed {
hashed_state.storages.insert(hashed_address, HashedStorage::new(true));
} else if changed_storage_iter.peek().is_some() {
hashed_state
.storages
.insert(hashed_address, HashedStorage::from_iter(false, changed_storage_iter));
}
}
}
hashed_state
}
#[derive(Metrics, Clone)]
#[metrics(scope = "tree.root")]
pub(crate) struct MultiProofTaskMetrics {
@@ -134,6 +31,12 @@ pub(crate) struct MultiProofTaskMetrics {
pub into_trie_for_reuse_duration_histogram: Histogram,
/// Time spent waiting for preserved sparse trie cache to become available.
pub sparse_trie_cache_wait_duration_histogram: Histogram,
/// Histogram for sparse trie task idle time in seconds (waiting for updates or proof
/// results). Excludes the final wait after the channel is closed.
pub sparse_trie_idle_time_seconds: Histogram,
/// Histogram for hashing task idle time in seconds (waiting for messages from execution).
/// Excludes the final wait after the channel is closed.
pub hashing_task_idle_time_seconds: Histogram,
/// Number of account leaf updates applied without needing a new proof (cache hits).
pub sparse_trie_account_cache_hits: Histogram,

View File

@@ -2,8 +2,9 @@
use alloy_primitives::B256;
use parking_lot::Mutex;
use reth_primitives_traits::FastInstant as Instant;
use reth_trie_sparse::{ConfigurableSparseTrie, SparseStateTrie};
use std::{sync::Arc, time::Instant};
use std::sync::Arc;
use tracing::debug;
/// Type alias for the sparse trie type used in preservation.

View File

@@ -12,10 +12,9 @@
//! 3. When actual block execution happens, it benefits from the warmed cache
use crate::tree::{
cached_state::{CachedStateProvider, SavedCache},
payload_processor::{bal, multiproof::MultiProofMessage, PayloadExecutionCache},
payload_processor::{bal, multiproof::StateRootMessage},
precompile_cache::{CachedPrecompile, PrecompileCacheMap},
ExecutionEnv, StateProviderBuilder,
CachedStateProvider, ExecutionEnv, PayloadExecutionCache, SavedCache, StateProviderBuilder,
};
use alloy_consensus::transaction::TxHashRef;
use alloy_eip7928::BlockAccessList;
@@ -70,7 +69,7 @@ where
/// Context provided to execution tasks
ctx: PrewarmContext<N, P, Evm>,
/// Sender to emit evm state outcome messages, if any.
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
to_multi_proof: Option<CrossbeamSender<StateRootMessage>>,
/// Receiver for events produced by tx execution
actions_rx: Receiver<PrewarmTaskEvent<N::Receipt>>,
/// Parent span for tracing
@@ -88,7 +87,7 @@ where
executor: Runtime,
execution_cache: PayloadExecutionCache,
ctx: PrewarmContext<N, P, Evm>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
to_multi_proof: Option<CrossbeamSender<StateRootMessage>>,
) -> (Self, Sender<PrewarmTaskEvent<N::Receipt>>) {
let (actions_tx, actions_rx) = channel();
@@ -122,7 +121,7 @@ where
&self,
pending: mpsc::Receiver<(usize, Tx)>,
actions_tx: Sender<PrewarmTaskEvent<N::Receipt>>,
to_multi_proof: Option<CrossbeamSender<MultiProofMessage>>,
to_multi_proof: Option<CrossbeamSender<StateRootMessage>>,
) where
Tx: ExecutableTxFor<Evm> + Send + 'static,
{
@@ -182,7 +181,7 @@ where
!withdrawals.is_empty()
{
let targets = multiproof_targets_from_withdrawals(withdrawals);
let _ = to_multi_proof.send(MultiProofMessage::PrefetchProofs(targets));
let _ = to_multi_proof.send(StateRootMessage::PrefetchProofs(targets));
}
});
@@ -202,7 +201,7 @@ where
ctx: &PrewarmContext<N, P, Evm>,
index: usize,
tx: Tx,
to_multi_proof: Option<&CrossbeamSender<MultiProofMessage>>,
to_multi_proof: Option<&CrossbeamSender<StateRootMessage>>,
) where
Tx: ExecutableTxFor<Evm>,
{
@@ -249,7 +248,7 @@ where
let (targets, storage_targets) = multiproof_targets_from_state(res.state);
ctx.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));
let _ = to_multi_proof.send(StateRootMessage::PrefetchProofs(targets));
}
}
@@ -403,8 +402,8 @@ where
storages = hashed_state.storages.len(),
"Converted BAL to hashed post state"
);
let _ = to_multi_proof.send(MultiProofMessage::HashedStateUpdate(hashed_state));
let _ = to_multi_proof.send(MultiProofMessage::FinishedStateUpdates);
let _ = to_multi_proof.send(StateRootMessage::HashedStateUpdate(hashed_state));
let _ = to_multi_proof.send(StateRootMessage::FinishedStateUpdates);
}
Err(err) => {
warn!(

View File

@@ -4,8 +4,8 @@ use std::sync::Arc;
use crate::tree::{
multiproof::{
dispatch_with_chunking, evm_state_to_hashed_post_state, MultiProofMessage,
DEFAULT_MAX_TARGETS_FOR_CHUNKING,
dispatch_with_chunking, evm_state_to_hashed_post_state, StateRootComputeOutcome,
StateRootMessage, DEFAULT_MAX_TARGETS_FOR_CHUNKING,
},
payload_processor::multiproof::MultiProofTaskMetrics,
};
@@ -26,8 +26,6 @@ use reth_trie_parallel::{
},
root::ParallelStateRootError,
};
#[cfg(feature = "trie-debug")]
use reth_trie_sparse::debug_recorder::TrieDebugRecorder;
use reth_trie_sparse::{
errors::SparseTrieResult, ConfigurableSparseTrie, DeferredDrops, LeafUpdate,
RevealableSparseTrie, SparseStateTrie, SparseTrie,
@@ -118,7 +116,7 @@ where
/// Creates a new sparse trie, pre-populating with an existing [`SparseStateTrie`].
pub(super) fn new_with_trie(
executor: &Runtime,
updates: CrossbeamReceiver<MultiProofMessage>,
updates: CrossbeamReceiver<StateRootMessage>,
proof_worker_handle: ProofWorkerHandle,
metrics: MultiProofTaskMetrics,
trie: SparseStateTrie<A, S>,
@@ -128,9 +126,10 @@ where
let (hashed_state_tx, hashed_state_rx) = crossbeam_channel::unbounded();
let parent_span = tracing::Span::current();
let hashing_metrics = metrics.clone();
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)
Self::run_hashing_task(updates, hashed_state_tx, hashing_metrics)
});
Self {
@@ -163,31 +162,44 @@ where
/// Runs the hashing task that drains updates from the channel and converts them to
/// `HashedPostState` in parallel.
fn run_hashing_task(
updates: CrossbeamReceiver<MultiProofMessage>,
updates: CrossbeamReceiver<StateRootMessage>,
hashed_state_tx: CrossbeamSender<SparseTrieTaskMessage>,
metrics: MultiProofTaskMetrics,
) {
let mut total_idle_time = std::time::Duration::ZERO;
let mut idle_start = Instant::now();
while let Ok(message) = updates.recv() {
total_idle_time += idle_start.elapsed();
let msg = match message {
MultiProofMessage::PrefetchProofs(targets) => {
StateRootMessage::PrefetchProofs(targets) => {
SparseTrieTaskMessage::PrefetchProofs(targets)
}
MultiProofMessage::StateUpdate(_, state) => {
StateRootMessage::StateUpdate(_, state) => {
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)
}
MultiProofMessage::FinishedStateUpdates => {
StateRootMessage::FinishedStateUpdates => {
SparseTrieTaskMessage::FinishedStateUpdates
}
MultiProofMessage::BlockAccessList(_) => continue,
MultiProofMessage::HashedStateUpdate(state) => {
StateRootMessage::BlockAccessList(_) => {
idle_start = Instant::now();
continue;
}
StateRootMessage::HashedStateUpdate(state) => {
SparseTrieTaskMessage::HashedState(state)
}
};
if hashed_state_tx.send(msg).is_err() {
break;
}
idle_start = Instant::now();
}
metrics.hashing_task_idle_time_seconds.record(total_idle_time.as_secs_f64());
}
/// Prunes and shrinks the trie for reuse in the next payload built on top of this one.
@@ -247,13 +259,14 @@ where
pub(super) fn run(&mut self) -> Result<StateRootComputeOutcome, ParallelStateRootError> {
let now = Instant::now();
let mut total_idle_time = std::time::Duration::ZERO;
let mut idle_start = 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 wake = Instant::now();
let update = match message {
Ok(m) => m,
@@ -264,11 +277,17 @@ where
}
};
total_idle_time += wake.duration_since(idle_start);
self.metrics
.sparse_trie_channel_wait_duration_histogram
.record(wake.duration_since(t));
self.on_message(update);
self.pending_updates += 1;
}
recv(self.proof_result_rx) -> message => {
let phase_end = Instant::now();
total_idle_time += phase_end.duration_since(idle_start);
self.metrics
.sparse_trie_channel_wait_duration_histogram
.record(phase_end.duration_since(t));
@@ -331,8 +350,12 @@ where
// Make sure to dispatch targets if we've accumulated a lot of them.
self.dispatch_pending_targets();
}
idle_start = Instant::now();
}
self.metrics.sparse_trie_idle_time_seconds.record(total_idle_time.as_secs_f64());
debug!(target: "engine::root", "All proofs processed, ending calculation");
let start = Instant::now();
@@ -838,20 +861,6 @@ enum SparseTrieTaskMessage {
FinishedStateUpdates,
}
/// Outcome of the state root computation, including the state root itself with
/// the trie updates.
#[derive(Debug, Clone)]
pub struct StateRootComputeOutcome {
/// The state root.
pub state_root: B256,
/// The trie updates.
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")]
pub debug_recorders: Vec<(Option<B256>, TrieDebugRecorder)>,
}
#[cfg(test)]
mod tests {
use super::*;
@@ -882,11 +891,12 @@ mod tests {
SparseTrieCacheTask::<ArenaParallelSparseTrie, ArenaParallelSparseTrie>::run_hashing_task(
updates_rx,
hashed_state_tx,
MultiProofTaskMetrics::default(),
);
});
updates_tx.send(MultiProofMessage::HashedStateUpdate(hashed_state)).unwrap();
updates_tx.send(MultiProofMessage::FinishedStateUpdates).unwrap();
updates_tx.send(StateRootMessage::HashedStateUpdate(hashed_state)).unwrap();
updates_tx.send(StateRootMessage::FinishedStateUpdates).unwrap();
drop(updates_tx);
let SparseTrieTaskMessage::HashedState(received) = hashed_state_rx.recv().unwrap() else {

View File

@@ -1,14 +1,51 @@
//! Types and traits for validating blocks and payloads.
//!
//! # Validation pipeline
//!
//! When the engine processes a new payload (`engine_newPayload`), validation happens in phases:
//!
//! ## Phase 1 Payload conversion
//! [`PayloadValidator::convert_payload_to_block`] decodes the execution payload (RLP, hashing)
//! into a [`SealedBlock`]. This runs on a background thread concurrently with state setup.
//!
//! ## Phase 2 Pre-execution consensus
//! - [`HeaderValidator::validate_header`] — standalone header checks (hash, gas, base fee,
//! fork-specific fields)
//! - [`Consensus::validate_block_pre_execution`] — body vs header (tx root, ommer hash, withdrawals
//! root)
//! - [`HeaderValidator::validate_header_against_parent`] — sequential checks (block number,
//! timestamp, gas limit, base fee vs parent)
//!
//! ## Phase 3 Execution
//! Block transactions are executed via the EVM. Receipt roots are computed incrementally.
//!
//! ## Phase 4 Post-execution consensus
//! - [`FullConsensus::validate_block_post_execution`] — gas used, receipt root, logs bloom,
//! requests hash
//! - [`PayloadValidator::validate_block_post_execution_with_hashed_state`] — network-specific
//! (no-op on L1, used by OP Stack)
//!
//! ## Payload attributes validation (`engine_forkchoiceUpdated`)
//! When the CL provides payload attributes to start building a block:
//! - [`PayloadValidator::validate_payload_attributes_against_header`] — ensures timestamp ordering
//!
//! If validation passes, a payload build job is started. If it fails,
//! `INVALID_PAYLOAD_ATTRIBUTES` is returned without rolling back the forkchoice update.
//!
//! [`HeaderValidator::validate_header`]: reth_consensus::HeaderValidator::validate_header
//! [`Consensus::validate_block_pre_execution`]: reth_consensus::Consensus::validate_block_pre_execution
//! [`HeaderValidator::validate_header_against_parent`]: reth_consensus::HeaderValidator::validate_header_against_parent
//! [`FullConsensus::validate_block_post_execution`]: reth_consensus::FullConsensus::validate_block_post_execution
//! [`SealedBlock`]: reth_primitives_traits::SealedBlock
use crate::tree::{
cached_state::{CacheStats, CachedStateProvider},
error::{InsertBlockError, InsertBlockErrorKind, InsertPayloadError},
instrumented_state::{InstrumentedStateProvider, StateProviderStats},
multiproof::{StateRootComputeOutcome, StateRootHandle},
payload_processor::PayloadProcessor,
precompile_cache::{CachedPrecompile, CachedPrecompileMetrics, PrecompileCacheMap},
sparse_trie::StateRootComputeOutcome,
CacheWaitDurations, EngineApiMetrics, EngineApiTreeState, ExecutionEnv, PayloadHandle,
StateProviderBuilder, StateProviderDatabase, TreeConfig, WaitForCaches,
CacheWaitDurations, CachedStateProvider, EngineApiMetrics, EngineApiTreeState, ExecutionEnv,
PayloadHandle, StateProviderBuilder, StateProviderDatabase, TreeConfig, WaitForCaches,
};
use alloy_consensus::transaction::{Either, TxHashRef};
use alloy_eip7928::BlockAccessList;
@@ -31,6 +68,7 @@ use reth_evm::{
block::BlockExecutor, execute::ExecutableTxFor, ConfigureEvm, EvmEnvFor, ExecutionCtxFor,
OnStateHook, SpecFor,
};
use reth_execution_cache::{CacheStats, SavedCache};
use reth_payload_primitives::{
BuiltPayload, InvalidPayloadAttributesError, NewPayloadError, PayloadTypes,
};
@@ -563,14 +601,14 @@ where
let block = convert_to_block(input)?;
let transaction_root = is_payload.then(|| {
let block = block.clone();
let body = block.body().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()
body.calculate_tx_root()
})
});
let block = block.with_senders(senders);
@@ -1903,6 +1941,17 @@ pub trait EngineValidator<
/// This is invoked when blocks are inserted via `InsertExecutedBlock` (e.g., locally built
/// blocks by sequencers) to allow implementations to update internal state such as caches.
fn on_inserted_executed_block(&self, block: ExecutedBlock<N>);
/// Returns [`SavedCache`] for the given block hash.
fn cache_for(&self, _block_hash: B256) -> Option<SavedCache>;
/// Spawns a sparse trie pipeline and returns a handle for the payload builder.
fn sparse_trie_handle_for(
&self,
parent_hash: B256,
parent_state_root: B256,
state: &EngineApiTreeState<N>,
) -> Option<StateRootHandle>;
}
impl<N, Types, P, Evm, V> EngineValidator<Types> for BasicEngineValidator<P, Evm, V>
@@ -1966,6 +2015,31 @@ where
&block.execution_output.state,
);
}
fn cache_for(&self, block_hash: B256) -> Option<SavedCache> {
Some(self.payload_processor.cache_for(block_hash))
}
fn sparse_trie_handle_for(
&self,
parent_hash: B256,
parent_state_root: B256,
state: &EngineApiTreeState<N>,
) -> Option<StateRootHandle> {
let (lazy_overlay, anchor_hash) = Self::get_parent_lazy_overlay(parent_hash, state);
let overlay_factory =
OverlayStateProviderFactory::new(self.provider.clone(), self.changeset_cache.clone())
.with_block_hash(Some(anchor_hash))
.with_lazy_overlay(lazy_overlay);
Some(self.payload_processor.spawn_state_root(
overlay_factory,
parent_state_root,
// Full proof workers — tx count unknown at FCU time (block built incrementally)
false,
&self.config,
))
}
}
impl<P, Evm, V> WaitForCaches for BasicEngineValidator<P, Evm, V>

View File

@@ -222,7 +222,6 @@ impl TestHarness {
EngineApiKind::Ethereum,
evm_config,
changeset_cache,
provider.cached_storage_settings().use_hashed_state(),
reth_tasks::Runtime::test(),
);
@@ -306,7 +305,6 @@ impl TestHarness {
state: fcu_state,
payload_attrs: None,
tx,
version: EngineApiMessageVersion::default(),
}
.into(),
))
@@ -606,7 +604,6 @@ async fn test_engine_request_during_backfill() {
},
payload_attrs: None,
tx,
version: EngineApiMessageVersion::default(),
}
.into(),
))
@@ -694,6 +691,74 @@ async fn test_holesky_payload() {
assert!(resp.is_syncing());
}
#[test]
fn test_backpressure_waits_for_persistence_before_reading_incoming() {
let blocks: Vec<_> = TestBlockBuilder::eth().get_executed_blocks(1..4).collect();
let mut test_harness = TestHarness::new(MAINNET.clone()).with_blocks(blocks.clone());
test_harness.tree.config = test_harness
.tree
.config
.with_persistence_threshold(0)
.with_persistence_backpressure_threshold(1);
let (persist_tx, persist_rx) = crossbeam_channel::bounded(1);
let persisted = blocks.last().unwrap().recovered_block().num_hash();
test_harness.tree.persistence_state.start_save(persisted, persist_rx);
assert!(test_harness.tree.should_backpressure());
let (tx, mut rx) = oneshot::channel();
test_harness
.to_tree_tx
.send(FromEngine::Request(
BeaconEngineMessage::ForkchoiceUpdated {
state: ForkchoiceState {
head_block_hash: B256::random(),
safe_block_hash: B256::random(),
finalized_block_hash: B256::random(),
},
payload_attrs: None,
tx,
}
.into(),
))
.unwrap();
test_harness.to_tree_tx.send(FromEngine::DownloadedBlocks(vec![])).unwrap();
assert_eq!(test_harness.tree.incoming.len(), 2);
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(10));
persist_tx
.send(PersistenceResult {
last_block: Some(persisted),
commit_duration: Some(Duration::ZERO),
})
.unwrap();
});
let event = test_harness.tree.wait_for_persistence_event();
assert!(matches!(event, super::LoopEvent::PersistenceComplete { .. }));
assert_eq!(test_harness.tree.incoming.len(), 2);
let super::LoopEvent::PersistenceComplete { result, start_time } = event else {
unreachable!()
};
test_harness.tree.on_persistence_complete(result, start_time).unwrap();
let super::LoopEvent::EngineMessage(message) = test_harness.tree.wait_for_event() else {
panic!("expected queued engine message")
};
let _ = test_harness.tree.on_engine_message(message).unwrap();
let msg = rx.try_recv();
assert!(msg.is_ok());
assert_eq!(test_harness.tree.incoming.len(), 1);
let super::LoopEvent::EngineMessage(message) = test_harness.tree.wait_for_event() else {
panic!("expected queued engine message")
};
let _ = test_harness.tree.on_engine_message(message).unwrap();
assert_eq!(test_harness.tree.incoming.len(), 0);
}
#[tokio::test]
async fn test_tree_state_on_new_head_reorg() {
reth_tracing::init_test_tracing();
@@ -1092,7 +1157,6 @@ async fn test_fcu_with_canonical_ancestor_updates_latest_block() {
},
payload_attrs: None,
tx,
version: EngineApiMessageVersion::default(),
}
.into(),
))
@@ -1808,10 +1872,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: B256::ZERO,
};
let result = test_harness
.tree
.handle_canonical_head(state, &None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.handle_canonical_head(state, &None).unwrap();
assert!(result.is_some(), "Should return outcome for canonical head");
let outcome = result.unwrap();
let fcu_result = outcome.outcome.await.unwrap();
@@ -1824,10 +1885,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: B256::ZERO,
};
let result = test_harness
.tree
.handle_canonical_head(non_canonical_state, &None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.handle_canonical_head(non_canonical_state, &None).unwrap();
assert!(result.is_none(), "Non-canonical head should return None");
}
@@ -1850,10 +1908,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: B256::ZERO,
};
let result = test_harness
.tree
.apply_chain_update(state, &None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.apply_chain_update(state, &None).unwrap();
assert!(result.is_some(), "Should apply chain update for new head");
let outcome = result.unwrap();
let fcu_result = outcome.outcome.await.unwrap();
@@ -1866,10 +1921,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: B256::ZERO,
};
let result = test_harness
.tree
.apply_chain_update(missing_state, &None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.apply_chain_update(missing_state, &None).unwrap();
assert!(result.is_none(), "Missing block should return None");
}
@@ -1923,10 +1975,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: canonical_head,
};
let result = test_harness
.tree
.on_forkchoice_updated(state, None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.on_forkchoice_updated(state, None).unwrap();
let fcu_result = result.outcome.await.unwrap();
assert!(fcu_result.payload_status.is_valid());
@@ -1937,10 +1986,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: B256::ZERO,
};
let result = test_harness
.tree
.on_forkchoice_updated(missing_state, None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.on_forkchoice_updated(missing_state, None).unwrap();
let fcu_result = result.outcome.await.unwrap();
assert!(fcu_result.payload_status.is_syncing());
assert!(result.event.is_some(), "Should trigger download event for missing block");
@@ -1953,10 +1999,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: B256::ZERO,
};
let result = test_harness
.tree
.on_forkchoice_updated(state, None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.on_forkchoice_updated(state, None).unwrap();
let fcu_result = result.outcome.await.unwrap();
assert!(fcu_result.payload_status.is_syncing(), "Should return syncing during backfill");
}
@@ -2004,10 +2047,7 @@ mod forkchoice_updated_tests {
finalized_block_hash: B256::ZERO,
};
let result = test_harness
.tree
.handle_canonical_head(state, &None, EngineApiMessageVersion::default())
.unwrap();
let result = test_harness.tree.handle_canonical_head(state, &None).unwrap();
assert!(result.is_some(), "OpStack should handle canonical head");
}

View File

@@ -62,12 +62,7 @@ impl EngineMessageStore {
fs::create_dir_all(&self.path)?; // ensure that store path had been created
let timestamp = received_at.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis();
match msg {
BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx: _tx,
version: _version,
} => {
BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx: _tx } => {
let filename = format!("{}-fcu-{}.json", timestamp, state.head_block_hash);
fs::write(
self.path.join(filename),

View File

@@ -14,7 +14,7 @@ use reth_evm::{
execute::{BlockBuilder, BlockBuilderOutcome},
ConfigureEvm,
};
use reth_payload_primitives::{BuiltPayload, EngineApiMessageVersion, PayloadTypes};
use reth_payload_primitives::{BuiltPayload, PayloadTypes};
use reth_primitives_traits::{
block::Block as _, BlockBody as _, BlockTy, HeaderTy, SealedBlock, SignedTransaction,
};
@@ -202,32 +202,18 @@ where
state: reorg_forkchoice_state,
payload_attrs: None,
tx: reorg_fcu_tx,
version: EngineApiMessageVersion::default(),
},
]);
*this.state = EngineReorgState::Reorg { queue };
continue
}
(
Some(BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx,
version,
}),
_,
) => {
(Some(BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx }), _) => {
// Record last forkchoice state forwarded to the engine.
// We do not care if it's valid since engine should be able to handle
// reorgs that rely on invalid forkchoice state.
*this.last_forkchoice_state = Some(state);
*this.forkchoice_states_forwarded += 1;
Some(BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx,
version,
})
Some(BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx })
}
(item, _) => item,
};
@@ -316,7 +302,7 @@ where
cumulative_gas_used += gas_used;
}
let BlockBuilderOutcome { block, .. } = builder.finish(&state_provider)?;
let BlockBuilderOutcome { block, .. } = builder.finish(&state_provider, None)?;
Ok(block.into_sealed_block())
}

View File

@@ -45,12 +45,7 @@ where
loop {
let next = ready!(this.stream.poll_next_unpin(cx));
let item = match next {
Some(BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx,
version,
}) => {
Some(BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx }) => {
if this.skipped < this.threshold {
*this.skipped += 1;
tracing::warn!(target: "engine::stream::skip_fcu", ?state, ?payload_attrs, threshold=this.threshold, skipped=this.skipped, "Skipping FCU");
@@ -58,12 +53,7 @@ where
continue
}
*this.skipped = 0;
Some(BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs,
tx,
version,
})
Some(BeaconEngineMessage::ForkchoiceUpdated { state, payload_attrs, tx })
}
next => next,
};

View File

@@ -52,11 +52,20 @@ impl<Http: HttpClient + Clone> EraClient<Http> {
const CHECKSUMS: &'static str = "checksums.txt";
/// Constructs [`EraClient`] using `client` to download from `url` into `folder`.
///
/// The file type is auto-detected from the URL. Use
/// [`with_era_type`](Self::with_era_type) to override.
pub fn new(client: Http, url: Url, folder: impl Into<Box<Path>>) -> Self {
let era_type = EraFileType::from_url(url.as_str());
Self { client, url, folder: folder.into(), era_type }
}
/// Override the auto-detected [`EraFileType`].
pub const fn with_era_type(mut self, era_type: EraFileType) -> Self {
self.era_type = era_type;
self
}
/// Performs a GET request on `url` and stores the response body into a file located within
/// the `folder`.
pub async fn download_to_file(&mut self, url: impl IntoUrl) -> eyre::Result<Box<Path>> {
@@ -367,4 +376,19 @@ mod tests {
assert_eq!(actual_number, expected_number);
}
#[test]
fn test_with_era_type_overrides_auto_detection() {
// URL without "era1" auto-detects as Era
let client = EraClient::new(
Client::new(),
Url::from_str("https://example.com/").unwrap(),
PathBuf::new(),
);
assert_eq!(client.era_type, EraFileType::Era);
// with_era_type overrides to Era1
let client = client.with_era_type(EraFileType::Era1);
assert_eq!(client.era_type, EraFileType::Era1);
}
}

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